Home | History | Annotate | Download | only in userdictionary
      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 
     17 package com.android.providers.userdictionary;
     18 
     19 import java.io.ByteArrayInputStream;
     20 import java.io.ByteArrayOutputStream;
     21 import java.io.DataInputStream;
     22 import java.io.DataOutputStream;
     23 import java.io.EOFException;
     24 import java.io.FileInputStream;
     25 import java.io.FileOutputStream;
     26 import java.io.IOException;
     27 import java.util.Objects;
     28 import java.util.NoSuchElementException;
     29 import java.util.StringTokenizer;
     30 import java.util.zip.CRC32;
     31 import java.util.zip.GZIPInputStream;
     32 import java.util.zip.GZIPOutputStream;
     33 
     34 import android.app.backup.BackupDataInput;
     35 import android.app.backup.BackupDataOutput;
     36 import android.app.backup.BackupAgentHelper;
     37 import android.content.ContentValues;
     38 import android.database.Cursor;
     39 import android.net.Uri;
     40 import android.os.ParcelFileDescriptor;
     41 import android.provider.UserDictionary.Words;
     42 import android.text.TextUtils;
     43 import android.util.Log;
     44 
     45 import libcore.io.IoUtils;
     46 
     47 /**
     48  * Performs backup and restore of the User Dictionary.
     49  */
     50 public 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