blob: 48fb957b01a449b16fb071c9c4bdaf243185f36a [file] [log] [blame]
/*
* 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;
}
}