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.NoSuchElementException; 28import java.util.StringTokenizer; 29import java.util.zip.CRC32; 30import java.util.zip.GZIPInputStream; 31import java.util.zip.GZIPOutputStream; 32 33import android.app.backup.BackupDataInput; 34import android.app.backup.BackupDataOutput; 35import android.app.backup.BackupAgentHelper; 36import android.content.ContentValues; 37import android.database.Cursor; 38import android.net.Uri; 39import android.os.ParcelFileDescriptor; 40import android.provider.UserDictionary.Words; 41import android.text.TextUtils; 42import android.util.Log; 43 44/** 45 * Performs backup and restore of the User Dictionary. 46 */ 47public class DictionaryBackupAgent extends BackupAgentHelper { 48 49 private static final String KEY_DICTIONARY = "userdictionary"; 50 51 private static final int STATE_DICTIONARY = 0; 52 private static final int STATE_SIZE = 1; 53 54 private static final String SEPARATOR = "|"; 55 56 private static final byte[] EMPTY_DATA = new byte[0]; 57 58 private static final String TAG = "DictionaryBackupAgent"; 59 60 private static final int COLUMN_WORD = 1; 61 private static final int COLUMN_FREQUENCY = 2; 62 private static final int COLUMN_LOCALE = 3; 63 private static final int COLUMN_APPID = 4; 64 65 private static final String[] PROJECTION = { 66 Words._ID, 67 Words.WORD, 68 Words.FREQUENCY, 69 Words.LOCALE, 70 Words.APP_ID 71 }; 72 73 @Override 74 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 75 ParcelFileDescriptor newState) throws IOException { 76 77 byte[] userDictionaryData = getDictionary(); 78 79 long[] stateChecksums = readOldChecksums(oldState); 80 81 stateChecksums[STATE_DICTIONARY] = 82 writeIfChanged(stateChecksums[STATE_DICTIONARY], KEY_DICTIONARY, 83 userDictionaryData, data); 84 85 writeNewChecksums(stateChecksums, newState); 86 } 87 88 @Override 89 public void onRestore(BackupDataInput data, int appVersionCode, 90 ParcelFileDescriptor newState) throws IOException { 91 92 while (data.readNextHeader()) { 93 final String key = data.getKey(); 94 final int size = data.getDataSize(); 95 if (KEY_DICTIONARY.equals(key)) { 96 restoreDictionary(data, Words.CONTENT_URI); 97 } else { 98 data.skipEntityData(); 99 } 100 } 101 } 102 103 private long[] readOldChecksums(ParcelFileDescriptor oldState) throws IOException { 104 long[] stateChecksums = new long[STATE_SIZE]; 105 106 DataInputStream dataInput = new DataInputStream( 107 new FileInputStream(oldState.getFileDescriptor())); 108 for (int i = 0; i < STATE_SIZE; i++) { 109 try { 110 stateChecksums[i] = dataInput.readLong(); 111 } catch (EOFException eof) { 112 break; 113 } 114 } 115 dataInput.close(); 116 return stateChecksums; 117 } 118 119 private void writeNewChecksums(long[] checksums, ParcelFileDescriptor newState) 120 throws IOException { 121 DataOutputStream dataOutput = new DataOutputStream( 122 new FileOutputStream(newState.getFileDescriptor())); 123 for (int i = 0; i < STATE_SIZE; i++) { 124 dataOutput.writeLong(checksums[i]); 125 } 126 dataOutput.close(); 127 } 128 129 private long writeIfChanged(long oldChecksum, String key, byte[] data, 130 BackupDataOutput output) { 131 CRC32 checkSummer = new CRC32(); 132 checkSummer.update(data); 133 long newChecksum = checkSummer.getValue(); 134 if (oldChecksum == newChecksum) { 135 return oldChecksum; 136 } 137 try { 138 output.writeEntityHeader(key, data.length); 139 output.writeEntityData(data, data.length); 140 } catch (IOException ioe) { 141 // Bail 142 } 143 return newChecksum; 144 } 145 146 private byte[] getDictionary() { 147 Cursor cursor = getContentResolver().query(Words.CONTENT_URI, PROJECTION, 148 null, null, Words.WORD); 149 if (cursor == null) return EMPTY_DATA; 150 if (!cursor.moveToFirst()) { 151 Log.e(TAG, "Couldn't read from the cursor"); 152 cursor.close(); 153 return EMPTY_DATA; 154 } 155 byte[] sizeBytes = new byte[4]; 156 ByteArrayOutputStream baos = new ByteArrayOutputStream(cursor.getCount() * 10); 157 try { 158 GZIPOutputStream gzip = new GZIPOutputStream(baos); 159 while (!cursor.isAfterLast()) { 160 String name = cursor.getString(COLUMN_WORD); 161 int frequency = cursor.getInt(COLUMN_FREQUENCY); 162 String locale = cursor.getString(COLUMN_LOCALE); 163 int appId = cursor.getInt(COLUMN_APPID); 164 String out = name + "|" + frequency + "|" + locale + "|" + appId; 165 byte[] line = out.getBytes(); 166 writeInt(sizeBytes, 0, line.length); 167 gzip.write(sizeBytes); 168 gzip.write(line); 169 cursor.moveToNext(); 170 } 171 gzip.finish(); 172 } catch (IOException ioe) { 173 Log.e(TAG, "Couldn't compress the dictionary:\n" + ioe); 174 return EMPTY_DATA; 175 } finally { 176 cursor.close(); 177 } 178 return baos.toByteArray(); 179 } 180 181 private void restoreDictionary(BackupDataInput data, Uri contentUri) { 182 ContentValues cv = new ContentValues(2); 183 byte[] dictCompressed = new byte[data.getDataSize()]; 184 byte[] dictionary = null; 185 try { 186 data.readEntityData(dictCompressed, 0, dictCompressed.length); 187 GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(dictCompressed)); 188 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 189 byte[] tempData = new byte[1024]; 190 int got; 191 while ((got = gzip.read(tempData)) > 0) { 192 baos.write(tempData, 0, got); 193 } 194 gzip.close(); 195 dictionary = baos.toByteArray(); 196 } catch (IOException ioe) { 197 Log.e(TAG, "Couldn't read and uncompress entity data:\n" + ioe); 198 return; 199 } 200 int pos = 0; 201 while (pos + 4 < dictionary.length) { 202 int length = readInt(dictionary, pos); 203 pos += 4; 204 if (pos + length > dictionary.length) { 205 Log.e(TAG, "Insufficient data"); 206 } 207 String line = new String(dictionary, pos, length); 208 pos += length; 209 StringTokenizer st = new StringTokenizer(line, SEPARATOR); 210 String word; 211 String frequency; 212 try { 213 word = st.nextToken(); 214 frequency = st.nextToken(); 215 String locale = null; 216 String appid = null; 217 if (st.hasMoreTokens()) locale = st.nextToken(); 218 if ("null".equalsIgnoreCase(locale)) locale = null; 219 if (st.hasMoreTokens()) appid = st.nextToken(); 220 int frequencyInt = Integer.parseInt(frequency); 221 int appidInt = appid != null? Integer.parseInt(appid) : 0; 222 223 if (!TextUtils.isEmpty(frequency)) { 224 cv.clear(); 225 cv.put(Words.WORD, word); 226 cv.put(Words.FREQUENCY, frequencyInt); 227 cv.put(Words.LOCALE, locale); 228 cv.put(Words.APP_ID, appidInt); 229 // Remove duplicate first 230 getContentResolver().delete(contentUri, Words.WORD + "=?", new String[] {word}); 231 getContentResolver().insert(contentUri, cv); 232 } 233 } catch (NoSuchElementException nsee) { 234 Log.e(TAG, "Token format error\n" + nsee); 235 } catch (NumberFormatException nfe) { 236 Log.e(TAG, "Number format error\n" + nfe); 237 } 238 } 239 } 240 241 /** 242 * Write an int in BigEndian into the byte array. 243 * @param out byte array 244 * @param pos current pos in array 245 * @param value integer to write 246 * @return the index after adding the size of an int (4) 247 */ 248 private int writeInt(byte[] out, int pos, int value) { 249 out[pos + 0] = (byte) ((value >> 24) & 0xFF); 250 out[pos + 1] = (byte) ((value >> 16) & 0xFF); 251 out[pos + 2] = (byte) ((value >> 8) & 0xFF); 252 out[pos + 3] = (byte) ((value >> 0) & 0xFF); 253 return pos + 4; 254 } 255 256 private int readInt(byte[] in, int pos) { 257 int result = 258 ((in[pos ] & 0xFF) << 24) | 259 ((in[pos + 1] & 0xFF) << 16) | 260 ((in[pos + 2] & 0xFF) << 8) | 261 ((in[pos + 3] & 0xFF) << 0); 262 return result; 263 } 264} 265