• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.providers.userdictionary;
18 
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.DataInputStream;
22 import java.io.DataOutputStream;
23 import java.io.EOFException;
24 import java.io.FileInputStream;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.util.Objects;
28 import java.util.NoSuchElementException;
29 import java.util.StringTokenizer;
30 import java.util.zip.CRC32;
31 import java.util.zip.GZIPInputStream;
32 import java.util.zip.GZIPOutputStream;
33 
34 import android.app.backup.BackupDataInput;
35 import android.app.backup.BackupDataOutput;
36 import android.app.backup.BackupAgentHelper;
37 import android.content.ContentValues;
38 import android.database.Cursor;
39 import android.net.Uri;
40 import android.os.ParcelFileDescriptor;
41 import android.provider.UserDictionary.Words;
42 import android.text.TextUtils;
43 import android.util.Log;
44 
45 import libcore.io.IoUtils;
46 
47 /**
48  * Performs backup and restore of the User Dictionary.
49  */
50 public 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
onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)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
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)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 
readOldChecksums(ParcelFileDescriptor oldState)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 
writeNewChecksums(long[] checksums, ParcelFileDescriptor newState)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 
writeIfChanged(long oldChecksum, String key, byte[] data, BackupDataOutput output)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 
getDictionary()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 
restoreDictionary(BackupDataInput data, Uri contentUri)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      */
writeInt(byte[] out, int pos, int value)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 
readInt(byte[] in, int pos)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