1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.userdictionary;
18
19import java.io.ByteArrayInputStream;
20import java.io.ByteArrayOutputStream;
21import java.io.DataInputStream;
22import java.io.DataOutputStream;
23import java.io.EOFException;
24import java.io.FileInputStream;
25import java.io.FileOutputStream;
26import java.io.IOException;
27import java.util.Objects;
28import java.util.NoSuchElementException;
29import java.util.StringTokenizer;
30import java.util.zip.CRC32;
31import java.util.zip.GZIPInputStream;
32import java.util.zip.GZIPOutputStream;
33
34import android.app.backup.BackupDataInput;
35import android.app.backup.BackupDataOutput;
36import android.app.backup.BackupAgentHelper;
37import android.content.ContentValues;
38import android.database.Cursor;
39import android.net.Uri;
40import android.os.ParcelFileDescriptor;
41import android.provider.UserDictionary.Words;
42import android.text.TextUtils;
43import android.util.Log;
44
45import libcore.io.IoUtils;
46
47/**
48 * Performs backup and restore of the User Dictionary.
49 */
50public class DictionaryBackupAgent extends BackupAgentHelper {
51
52    private static final String KEY_DICTIONARY = "userdictionary";
53
54    private static final int STATE_DICTIONARY = 0;
55    private static final int STATE_SIZE = 1;
56
57    private static final String SEPARATOR = "|";
58
59    private static final byte[] EMPTY_DATA = new byte[0];
60
61    private static final String TAG = "DictionaryBackupAgent";
62
63    private static final int COLUMN_WORD = 1;
64    private static final int COLUMN_FREQUENCY = 2;
65    private static final int COLUMN_LOCALE = 3;
66    private static final int COLUMN_APPID = 4;
67    private static final int COLUMN_SHORTCUT = 5;
68
69    private static final String[] PROJECTION = {
70        Words._ID,
71        Words.WORD,
72        Words.FREQUENCY,
73        Words.LOCALE,
74        Words.APP_ID,
75        Words.SHORTCUT
76    };
77
78    @Override
79    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
80            ParcelFileDescriptor newState) throws IOException {
81
82        byte[] userDictionaryData = getDictionary();
83
84        long[] stateChecksums = readOldChecksums(oldState);
85
86        stateChecksums[STATE_DICTIONARY] =
87                writeIfChanged(stateChecksums[STATE_DICTIONARY], KEY_DICTIONARY,
88                        userDictionaryData, data);
89
90        writeNewChecksums(stateChecksums, newState);
91    }
92
93    @Override
94    public void onRestore(BackupDataInput data, int appVersionCode,
95            ParcelFileDescriptor newState) throws IOException {
96
97        while (data.readNextHeader()) {
98            final String key = data.getKey();
99            final int size = data.getDataSize();
100            if (KEY_DICTIONARY.equals(key)) {
101                restoreDictionary(data, Words.CONTENT_URI);
102            } else {
103                data.skipEntityData();
104            }
105        }
106    }
107
108    private long[] readOldChecksums(ParcelFileDescriptor oldState) throws IOException {
109        long[] stateChecksums = new long[STATE_SIZE];
110
111        DataInputStream dataInput = new DataInputStream(
112                new FileInputStream(oldState.getFileDescriptor()));
113        for (int i = 0; i < STATE_SIZE; i++) {
114            try {
115                stateChecksums[i] = dataInput.readLong();
116            } catch (EOFException eof) {
117                break;
118            }
119        }
120        dataInput.close();
121        return stateChecksums;
122    }
123
124    private void writeNewChecksums(long[] checksums, ParcelFileDescriptor newState)
125            throws IOException {
126        DataOutputStream dataOutput = new DataOutputStream(
127                new FileOutputStream(newState.getFileDescriptor()));
128        for (int i = 0; i < STATE_SIZE; i++) {
129            dataOutput.writeLong(checksums[i]);
130        }
131        dataOutput.close();
132    }
133
134    private long writeIfChanged(long oldChecksum, String key, byte[] data,
135            BackupDataOutput output) {
136        CRC32 checkSummer = new CRC32();
137        checkSummer.update(data);
138        long newChecksum = checkSummer.getValue();
139        if (oldChecksum == newChecksum) {
140            return oldChecksum;
141        }
142        try {
143            output.writeEntityHeader(key, data.length);
144            output.writeEntityData(data, data.length);
145        } catch (IOException ioe) {
146            // Bail
147        }
148        return newChecksum;
149    }
150
151    private byte[] getDictionary() {
152        Cursor cursor = getContentResolver().query(Words.CONTENT_URI, PROJECTION,
153                null, null, Words.WORD);
154        if (cursor == null) return EMPTY_DATA;
155        if (!cursor.moveToFirst()) {
156            Log.e(TAG, "Couldn't read from the cursor");
157            cursor.close();
158            return EMPTY_DATA;
159        }
160        byte[] sizeBytes = new byte[4];
161        ByteArrayOutputStream baos = new ByteArrayOutputStream(cursor.getCount() * 10);
162        GZIPOutputStream gzip = null;
163        try {
164            gzip = new GZIPOutputStream(baos);
165            while (!cursor.isAfterLast()) {
166                String name = cursor.getString(COLUMN_WORD);
167                int frequency = cursor.getInt(COLUMN_FREQUENCY);
168                String locale = cursor.getString(COLUMN_LOCALE);
169                int appId = cursor.getInt(COLUMN_APPID);
170                String shortcut = cursor.getString(COLUMN_SHORTCUT);
171                if (TextUtils.isEmpty(shortcut)) shortcut = "";
172                // TODO: escape the string
173                String out = name + SEPARATOR + frequency + SEPARATOR + locale + SEPARATOR + appId
174                        + SEPARATOR + shortcut;
175                byte[] line = out.getBytes();
176                writeInt(sizeBytes, 0, line.length);
177                gzip.write(sizeBytes);
178                gzip.write(line);
179                cursor.moveToNext();
180            }
181            gzip.finish();
182        } catch (IOException ioe) {
183            Log.e(TAG, "Couldn't compress the dictionary:\n" + ioe);
184            return EMPTY_DATA;
185        } finally {
186            IoUtils.closeQuietly(gzip);
187            cursor.close();
188        }
189        return baos.toByteArray();
190    }
191
192    private void restoreDictionary(BackupDataInput data, Uri contentUri) {
193        ContentValues cv = new ContentValues(2);
194        byte[] dictCompressed = new byte[data.getDataSize()];
195        byte[] dictionary = null;
196        try {
197            data.readEntityData(dictCompressed, 0, dictCompressed.length);
198            GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(dictCompressed));
199            ByteArrayOutputStream baos = new ByteArrayOutputStream();
200            byte[] tempData = new byte[1024];
201            int got;
202            while ((got = gzip.read(tempData)) > 0) {
203                baos.write(tempData, 0, got);
204            }
205            gzip.close();
206            dictionary = baos.toByteArray();
207        } catch (IOException ioe) {
208            Log.e(TAG, "Couldn't read and uncompress entity data:\n" + ioe);
209            return;
210        }
211        int pos = 0;
212        while (pos + 4 < dictionary.length) {
213            int length = readInt(dictionary, pos);
214            pos += 4;
215            if (pos + length > dictionary.length) {
216                Log.e(TAG, "Insufficient data");
217            }
218            String line = new String(dictionary, pos, length);
219            pos += length;
220            // TODO: unescape the string
221            StringTokenizer st = new StringTokenizer(line, SEPARATOR);
222            String previousWord = null;
223            String previousShortcut = null;
224            try {
225                final String word = st.nextToken();
226                final String frequency = st.nextToken();
227                String locale = null;
228                String appid = null;
229                String shortcut = null;
230                if (st.hasMoreTokens()) locale = st.nextToken();
231                if ("null".equalsIgnoreCase(locale)) locale = null;
232                if (st.hasMoreTokens()) appid = st.nextToken();
233                if (st.hasMoreTokens()) shortcut = st.nextToken();
234                if (TextUtils.isEmpty(shortcut)) shortcut = null;
235                int frequencyInt = Integer.parseInt(frequency);
236                int appidInt = appid != null? Integer.parseInt(appid) : 0;
237                // It seems there are cases where the same word is duplicated over and over
238                // many thousand times. To avoid killing the battery in this case, we skip this
239                // word if it's the same as the previous one. This is not meant to catch all
240                // duplicate words as there is no order guarantee, but only to save round
241                // trip to the database in the above case which can dramatically improve
242                // performance and battery use of the restore.
243                // Also, word and frequency are never supposed to be empty or null, but better
244                // safe than sorry.
245                if ((Objects.equals(word, previousWord)
246                        && Objects.equals(shortcut, previousShortcut))
247                        || TextUtils.isEmpty(frequency) || TextUtils.isEmpty(word)) {
248                    continue;
249                }
250                previousWord = word;
251                previousShortcut = shortcut;
252
253                cv.clear();
254                cv.put(Words.WORD, word);
255                cv.put(Words.FREQUENCY, frequencyInt);
256                cv.put(Words.LOCALE, locale);
257                cv.put(Words.APP_ID, appidInt);
258                cv.put(Words.SHORTCUT, shortcut);
259                // Remove any duplicate first
260                if (null != shortcut) {
261                    getContentResolver().delete(contentUri, Words.WORD + "=? and "
262                            + Words.SHORTCUT + "=?", new String[] {word, shortcut});
263                } else {
264                    getContentResolver().delete(contentUri, Words.WORD + "=? and "
265                            + Words.SHORTCUT + " is null", new String[0]);
266                }
267                getContentResolver().insert(contentUri, cv);
268            } catch (NoSuchElementException nsee) {
269                Log.e(TAG, "Token format error\n" + nsee);
270            } catch (NumberFormatException nfe) {
271                Log.e(TAG, "Number format error\n" + nfe);
272            }
273        }
274    }
275
276    /**
277     * Write an int in BigEndian into the byte array.
278     * @param out byte array
279     * @param pos current pos in array
280     * @param value integer to write
281     * @return the index after adding the size of an int (4)
282     */
283    private int writeInt(byte[] out, int pos, int value) {
284        out[pos + 0] = (byte) ((value >> 24) & 0xFF);
285        out[pos + 1] = (byte) ((value >> 16) & 0xFF);
286        out[pos + 2] = (byte) ((value >>  8) & 0xFF);
287        out[pos + 3] = (byte) ((value >>  0) & 0xFF);
288        return pos + 4;
289    }
290
291    private int readInt(byte[] in, int pos) {
292        int result =
293                ((in[pos    ] & 0xFF) << 24) |
294                ((in[pos + 1] & 0xFF) << 16) |
295                ((in[pos + 2] & 0xFF) <<  8) |
296                ((in[pos + 3] & 0xFF) <<  0);
297        return result;
298    }
299}
300