diff options
author | Jean-Baptiste Queru <jbq@google.com> | 2009-11-15 12:07:03 -0800 |
---|---|---|
committer | Jean-Baptiste Queru <jbq@google.com> | 2009-11-15 12:07:03 -0800 |
commit | ed49e6be0c0b1ad83dc6965a6dd3d293fe05b9ff (patch) | |
tree | 69dcac174d58dacb62573aee90adbfc278e5f68e | |
parent | 865b14b5faf8df8c090efa181ee5c5641f362fc9 (diff) | |
parent | d5b53851cf45a0b81af92d70551118d08601fe62 (diff) | |
download | UserDictionaryProvider-ed49e6be0c0b1ad83dc6965a6dd3d293fe05b9ff.tar.gz |
merge from eclair
-rw-r--r-- | AndroidManifest.xml | 8 | ||||
-rw-r--r-- | src/com/android/providers/userdictionary/DictionaryBackupAgent.java | 264 | ||||
-rw-r--r-- | src/com/android/providers/userdictionary/UserDictionaryProvider.java | 13 |
3 files changed, 281 insertions, 4 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9585043..7eab984 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -18,7 +18,13 @@ package="com.android.providers.userdictionary" android:sharedUserId="android.uid.shared"> - <application android:process="android.process.acore"> + <uses-permission android:name="android.permission.BACKUP_DATA" /> + + <application android:process="android.process.acore" + android:allowClearUserData="false" + android:backupAgent="DictionaryBackupAgent" + android:killAfterRestore="false" + > <provider android:name="UserDictionaryProvider" android:authorities="user_dictionary" android:syncable="false" android:multiprocess="false" android:readPermission="android.permission.READ_USER_DICTIONARY" diff --git a/src/com/android/providers/userdictionary/DictionaryBackupAgent.java b/src/com/android/providers/userdictionary/DictionaryBackupAgent.java new file mode 100644 index 0000000..fc3783a --- /dev/null +++ b/src/com/android/providers/userdictionary/DictionaryBackupAgent.java @@ -0,0 +1,264 @@ +/* + * 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.NoSuchElementException; +import java.util.StringTokenizer; +import java.util.zip.CRC32; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import android.backup.BackupDataInput; +import android.backup.BackupDataOutput; +import android.backup.BackupHelperAgent; +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; + +/** + * Performs backup and restore of the User Dictionary. + */ +public class DictionaryBackupAgent extends BackupHelperAgent { + + 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 String[] PROJECTION = { + Words._ID, + Words.WORD, + Words.FREQUENCY, + Words.LOCALE, + Words.APP_ID + }; + + @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); + try { + GZIPOutputStream 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 out = name + "|" + frequency + "|" + locale + "|" + appId; + 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 { + 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; + StringTokenizer st = new StringTokenizer(line, SEPARATOR); + String word; + String frequency; + try { + word = st.nextToken(); + frequency = st.nextToken(); + String locale = null; + String appid = null; + if (st.hasMoreTokens()) locale = st.nextToken(); + if ("null".equalsIgnoreCase(locale)) locale = null; + if (st.hasMoreTokens()) appid = st.nextToken(); + int frequencyInt = Integer.parseInt(frequency); + int appidInt = appid != null? Integer.parseInt(appid) : 0; + + if (!TextUtils.isEmpty(frequency)) { + cv.clear(); + cv.put(Words.WORD, word); + cv.put(Words.FREQUENCY, frequencyInt); + cv.put(Words.LOCALE, locale); + cv.put(Words.APP_ID, appidInt); + // Remove duplicate first + getContentResolver().delete(contentUri, Words.WORD + "=?", new String[] {word}); + 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; + } +} diff --git a/src/com/android/providers/userdictionary/UserDictionaryProvider.java b/src/com/android/providers/userdictionary/UserDictionaryProvider.java index c1928c2..59a0c24 100644 --- a/src/com/android/providers/userdictionary/UserDictionaryProvider.java +++ b/src/com/android/providers/userdictionary/UserDictionaryProvider.java @@ -17,6 +17,9 @@ package com.android.providers.userdictionary; +import java.util.HashMap; + +import android.backup.BackupManager; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; @@ -33,8 +36,6 @@ import android.provider.UserDictionary.Words; import android.text.TextUtils; import android.util.Log; -import java.util.HashMap; - /** * Provides access to a database of user defined words. Each item has a word and a frequency. */ @@ -58,7 +59,9 @@ public class UserDictionaryProvider extends ContentProvider { private static final int WORDS = 1; private static final int WORD_ID = 2; - + + private BackupManager mBackupManager; + /** * This class helps open, create, and upgrade the database file. */ @@ -93,6 +96,7 @@ public class UserDictionaryProvider extends ContentProvider { @Override public boolean onCreate() { mOpenHelper = new DatabaseHelper(getContext()); + mBackupManager = new BackupManager(getContext()); return true; } @@ -181,6 +185,7 @@ public class UserDictionaryProvider extends ContentProvider { if (rowId > 0) { Uri wordUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(wordUri, null); + mBackupManager.dataChanged(); return wordUri; } @@ -207,6 +212,7 @@ public class UserDictionaryProvider extends ContentProvider { } getContext().getContentResolver().notifyChange(uri, null); + mBackupManager.dataChanged(); return count; } @@ -230,6 +236,7 @@ public class UserDictionaryProvider extends ContentProvider { } getContext().getContentResolver().notifyChange(uri, null); + mBackupManager.dataChanged(); return count; } |