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