| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.providers.userdictionary; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.EOFException; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.Objects; |
| import java.util.NoSuchElementException; |
| import java.util.StringTokenizer; |
| import java.util.zip.CRC32; |
| import java.util.zip.GZIPInputStream; |
| import java.util.zip.GZIPOutputStream; |
| |
| import android.app.backup.BackupDataInput; |
| import android.app.backup.BackupDataOutput; |
| import android.app.backup.BackupAgentHelper; |
| import android.content.ContentValues; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.ParcelFileDescriptor; |
| import android.provider.UserDictionary.Words; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import libcore.io.IoUtils; |
| |
| /** |
| * Performs backup and restore of the User Dictionary. |
| */ |
| public class DictionaryBackupAgent extends BackupAgentHelper { |
| |
| private static final String KEY_DICTIONARY = "userdictionary"; |
| |
| private static final int STATE_DICTIONARY = 0; |
| private static final int STATE_SIZE = 1; |
| |
| private static final String SEPARATOR = "|"; |
| |
| private static final byte[] EMPTY_DATA = new byte[0]; |
| |
| private static final String TAG = "DictionaryBackupAgent"; |
| |
| private static final int COLUMN_WORD = 1; |
| private static final int COLUMN_FREQUENCY = 2; |
| private static final int COLUMN_LOCALE = 3; |
| private static final int COLUMN_APPID = 4; |
| private static final int COLUMN_SHORTCUT = 5; |
| |
| private static final String[] PROJECTION = { |
| Words._ID, |
| Words.WORD, |
| Words.FREQUENCY, |
| Words.LOCALE, |
| Words.APP_ID, |
| Words.SHORTCUT |
| }; |
| |
| @Override |
| public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, |
| ParcelFileDescriptor newState) throws IOException { |
| |
| byte[] userDictionaryData = getDictionary(); |
| |
| long[] stateChecksums = readOldChecksums(oldState); |
| |
| stateChecksums[STATE_DICTIONARY] = |
| writeIfChanged(stateChecksums[STATE_DICTIONARY], KEY_DICTIONARY, |
| userDictionaryData, data); |
| |
| writeNewChecksums(stateChecksums, newState); |
| } |
| |
| @Override |
| public void onRestore(BackupDataInput data, int appVersionCode, |
| ParcelFileDescriptor newState) throws IOException { |
| |
| while (data.readNextHeader()) { |
| final String key = data.getKey(); |
| final int size = data.getDataSize(); |
| if (KEY_DICTIONARY.equals(key)) { |
| restoreDictionary(data, Words.CONTENT_URI); |
| } else { |
| data.skipEntityData(); |
| } |
| } |
| } |
| |
| private long[] readOldChecksums(ParcelFileDescriptor oldState) throws IOException { |
| long[] stateChecksums = new long[STATE_SIZE]; |
| |
| DataInputStream dataInput = new DataInputStream( |
| new FileInputStream(oldState.getFileDescriptor())); |
| for (int i = 0; i < STATE_SIZE; i++) { |
| try { |
| stateChecksums[i] = dataInput.readLong(); |
| } catch (EOFException eof) { |
| break; |
| } |
| } |
| dataInput.close(); |
| return stateChecksums; |
| } |
| |
| private void writeNewChecksums(long[] checksums, ParcelFileDescriptor newState) |
| throws IOException { |
| DataOutputStream dataOutput = new DataOutputStream( |
| new FileOutputStream(newState.getFileDescriptor())); |
| for (int i = 0; i < STATE_SIZE; i++) { |
| dataOutput.writeLong(checksums[i]); |
| } |
| dataOutput.close(); |
| } |
| |
| private long writeIfChanged(long oldChecksum, String key, byte[] data, |
| BackupDataOutput output) { |
| CRC32 checkSummer = new CRC32(); |
| checkSummer.update(data); |
| long newChecksum = checkSummer.getValue(); |
| if (oldChecksum == newChecksum) { |
| return oldChecksum; |
| } |
| try { |
| output.writeEntityHeader(key, data.length); |
| output.writeEntityData(data, data.length); |
| } catch (IOException ioe) { |
| // Bail |
| } |
| return newChecksum; |
| } |
| |
| private byte[] getDictionary() { |
| Cursor cursor = getContentResolver().query(Words.CONTENT_URI, PROJECTION, |
| null, null, Words.WORD); |
| if (cursor == null) return EMPTY_DATA; |
| if (!cursor.moveToFirst()) { |
| Log.e(TAG, "Couldn't read from the cursor"); |
| cursor.close(); |
| return EMPTY_DATA; |
| } |
| byte[] sizeBytes = new byte[4]; |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(cursor.getCount() * 10); |
| GZIPOutputStream gzip = null; |
| try { |
| gzip = new GZIPOutputStream(baos); |
| while (!cursor.isAfterLast()) { |
| String name = cursor.getString(COLUMN_WORD); |
| int frequency = cursor.getInt(COLUMN_FREQUENCY); |
| String locale = cursor.getString(COLUMN_LOCALE); |
| int appId = cursor.getInt(COLUMN_APPID); |
| String shortcut = cursor.getString(COLUMN_SHORTCUT); |
| if (TextUtils.isEmpty(shortcut)) shortcut = ""; |
| // TODO: escape the string |
| String out = name + SEPARATOR + frequency + SEPARATOR + locale + SEPARATOR + appId |
| + SEPARATOR + shortcut; |
| byte[] line = out.getBytes(); |
| writeInt(sizeBytes, 0, line.length); |
| gzip.write(sizeBytes); |
| gzip.write(line); |
| cursor.moveToNext(); |
| } |
| gzip.finish(); |
| } catch (IOException ioe) { |
| Log.e(TAG, "Couldn't compress the dictionary:\n" + ioe); |
| return EMPTY_DATA; |
| } finally { |
| IoUtils.closeQuietly(gzip); |
| cursor.close(); |
| } |
| return baos.toByteArray(); |
| } |
| |
| private void restoreDictionary(BackupDataInput data, Uri contentUri) { |
| ContentValues cv = new ContentValues(2); |
| byte[] dictCompressed = new byte[data.getDataSize()]; |
| byte[] dictionary = null; |
| try { |
| data.readEntityData(dictCompressed, 0, dictCompressed.length); |
| GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(dictCompressed)); |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| byte[] tempData = new byte[1024]; |
| int got; |
| while ((got = gzip.read(tempData)) > 0) { |
| baos.write(tempData, 0, got); |
| } |
| gzip.close(); |
| dictionary = baos.toByteArray(); |
| } catch (IOException ioe) { |
| Log.e(TAG, "Couldn't read and uncompress entity data:\n" + ioe); |
| return; |
| } |
| int pos = 0; |
| while (pos + 4 < dictionary.length) { |
| int length = readInt(dictionary, pos); |
| pos += 4; |
| if (pos + length > dictionary.length) { |
| Log.e(TAG, "Insufficient data"); |
| } |
| String line = new String(dictionary, pos, length); |
| pos += length; |
| // TODO: unescape the string |
| StringTokenizer st = new StringTokenizer(line, SEPARATOR); |
| String previousWord = null; |
| String previousShortcut = null; |
| try { |
| final String word = st.nextToken(); |
| final String frequency = st.nextToken(); |
| String locale = null; |
| String appid = null; |
| String shortcut = null; |
| if (st.hasMoreTokens()) locale = st.nextToken(); |
| if ("null".equalsIgnoreCase(locale)) locale = null; |
| if (st.hasMoreTokens()) appid = st.nextToken(); |
| if (st.hasMoreTokens()) shortcut = st.nextToken(); |
| if (TextUtils.isEmpty(shortcut)) shortcut = null; |
| int frequencyInt = Integer.parseInt(frequency); |
| int appidInt = appid != null? Integer.parseInt(appid) : 0; |
| // It seems there are cases where the same word is duplicated over and over |
| // many thousand times. To avoid killing the battery in this case, we skip this |
| // word if it's the same as the previous one. This is not meant to catch all |
| // duplicate words as there is no order guarantee, but only to save round |
| // trip to the database in the above case which can dramatically improve |
| // performance and battery use of the restore. |
| // Also, word and frequency are never supposed to be empty or null, but better |
| // safe than sorry. |
| if ((Objects.equals(word, previousWord) |
| && Objects.equals(shortcut, previousShortcut)) |
| || TextUtils.isEmpty(frequency) || TextUtils.isEmpty(word)) { |
| continue; |
| } |
| previousWord = word; |
| previousShortcut = shortcut; |
| |
| cv.clear(); |
| cv.put(Words.WORD, word); |
| cv.put(Words.FREQUENCY, frequencyInt); |
| cv.put(Words.LOCALE, locale); |
| cv.put(Words.APP_ID, appidInt); |
| cv.put(Words.SHORTCUT, shortcut); |
| // Remove any duplicate first |
| if (null != shortcut) { |
| getContentResolver().delete(contentUri, Words.WORD + "=? and " |
| + Words.SHORTCUT + "=?", new String[] {word, shortcut}); |
| } else { |
| getContentResolver().delete(contentUri, Words.WORD + "=? and " |
| + Words.SHORTCUT + " is null", new String[0]); |
| } |
| getContentResolver().insert(contentUri, cv); |
| } catch (NoSuchElementException nsee) { |
| Log.e(TAG, "Token format error\n" + nsee); |
| } catch (NumberFormatException nfe) { |
| Log.e(TAG, "Number format error\n" + nfe); |
| } |
| } |
| } |
| |
| /** |
| * Write an int in BigEndian into the byte array. |
| * @param out byte array |
| * @param pos current pos in array |
| * @param value integer to write |
| * @return the index after adding the size of an int (4) |
| */ |
| private int writeInt(byte[] out, int pos, int value) { |
| out[pos + 0] = (byte) ((value >> 24) & 0xFF); |
| out[pos + 1] = (byte) ((value >> 16) & 0xFF); |
| out[pos + 2] = (byte) ((value >> 8) & 0xFF); |
| out[pos + 3] = (byte) ((value >> 0) & 0xFF); |
| return pos + 4; |
| } |
| |
| private int readInt(byte[] in, int pos) { |
| int result = |
| ((in[pos ] & 0xFF) << 24) | |
| ((in[pos + 1] & 0xFF) << 16) | |
| ((in[pos + 2] & 0xFF) << 8) | |
| ((in[pos + 3] & 0xFF) << 0); |
| return result; |
| } |
| } |