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