1/*
2 * Copyright (C) 2011 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 */
16package com.android.browser.provider;
17
18import android.content.ContentProvider;
19import android.content.ContentUris;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.UriMatcher;
23import android.database.Cursor;
24import android.database.DatabaseUtils;
25import android.database.sqlite.SQLiteDatabase;
26import android.database.sqlite.SQLiteOpenHelper;
27import android.database.sqlite.SQLiteQueryBuilder;
28import android.net.Uri;
29import android.os.FileUtils;
30import android.provider.BrowserContract;
31import android.text.TextUtils;
32
33import java.io.File;
34
35public class SnapshotProvider extends ContentProvider {
36
37    public static interface Snapshots {
38
39        public static final Uri CONTENT_URI = Uri.withAppendedPath(
40                SnapshotProvider.AUTHORITY_URI, "snapshots");
41        public static final String _ID = "_id";
42        @Deprecated
43        public static final String VIEWSTATE = "view_state";
44        public static final String BACKGROUND = "background";
45        public static final String TITLE = "title";
46        public static final String URL = "url";
47        public static final String FAVICON = "favicon";
48        public static final String THUMBNAIL = "thumbnail";
49        public static final String DATE_CREATED = "date_created";
50        public static final String VIEWSTATE_PATH = "viewstate_path";
51        public static final String VIEWSTATE_SIZE = "viewstate_size";
52    }
53
54    public static final String AUTHORITY = "com.android.browser.snapshots";
55    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
56
57    static final String TABLE_SNAPSHOTS = "snapshots";
58    static final int SNAPSHOTS = 10;
59    static final int SNAPSHOTS_ID = 11;
60    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
61    // Workaround that we can't remove the "NOT NULL" constraint on VIEWSTATE
62    static final byte[] NULL_BLOB_HACK = new byte[0];
63
64    SnapshotDatabaseHelper mOpenHelper;
65
66    static {
67        URI_MATCHER.addURI(AUTHORITY, "snapshots", SNAPSHOTS);
68        URI_MATCHER.addURI(AUTHORITY, "snapshots/#", SNAPSHOTS_ID);
69    }
70
71    final static class SnapshotDatabaseHelper extends SQLiteOpenHelper {
72
73        static final String DATABASE_NAME = "snapshots.db";
74        static final int DATABASE_VERSION = 3;
75
76        public SnapshotDatabaseHelper(Context context) {
77            super(context, DATABASE_NAME, null, DATABASE_VERSION);
78        }
79
80        @Override
81        public void onCreate(SQLiteDatabase db) {
82            db.execSQL("CREATE TABLE " + TABLE_SNAPSHOTS + "(" +
83                    Snapshots._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
84                    Snapshots.TITLE + " TEXT," +
85                    Snapshots.URL + " TEXT NOT NULL," +
86                    Snapshots.DATE_CREATED + " INTEGER," +
87                    Snapshots.FAVICON + " BLOB," +
88                    Snapshots.THUMBNAIL + " BLOB," +
89                    Snapshots.BACKGROUND + " INTEGER," +
90                    Snapshots.VIEWSTATE + " BLOB NOT NULL," +
91                    Snapshots.VIEWSTATE_PATH + " TEXT," +
92                    Snapshots.VIEWSTATE_SIZE + " INTEGER" +
93                    ");");
94        }
95
96        @Override
97        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
98            if (oldVersion < 2) {
99                db.execSQL("DROP TABLE " + TABLE_SNAPSHOTS);
100                onCreate(db);
101            }
102            if (oldVersion < 3) {
103                db.execSQL("ALTER TABLE " + TABLE_SNAPSHOTS + " ADD COLUMN "
104                        + Snapshots.VIEWSTATE_PATH + " TEXT");
105                db.execSQL("ALTER TABLE " + TABLE_SNAPSHOTS + " ADD COLUMN "
106                        + Snapshots.VIEWSTATE_SIZE + " INTEGER");
107                db.execSQL("UPDATE " + TABLE_SNAPSHOTS + " SET "
108                        + Snapshots.VIEWSTATE_SIZE + " = length("
109                        + Snapshots.VIEWSTATE + ")");
110            }
111        }
112
113    }
114
115    static File getOldDatabasePath(Context context) {
116        File dir = context.getExternalFilesDir(null);
117        return new File(dir, SnapshotDatabaseHelper.DATABASE_NAME);
118    }
119
120    private void migrateToDataFolder() {
121        File dbPath = getContext().getDatabasePath(SnapshotDatabaseHelper.DATABASE_NAME);
122        if (dbPath.exists()) return;
123        File oldPath = getOldDatabasePath(getContext());
124        if (oldPath.exists()) {
125            // Try to move
126            if (!oldPath.renameTo(dbPath)) {
127                // Failed, do a copy
128                FileUtils.copyFile(oldPath, dbPath);
129            }
130            // Cleanup
131            oldPath.delete();
132        }
133    }
134
135    @Override
136    public boolean onCreate() {
137        migrateToDataFolder();
138        mOpenHelper = new SnapshotDatabaseHelper(getContext());
139        return true;
140    }
141
142    SQLiteDatabase getWritableDatabase() {
143        return mOpenHelper.getWritableDatabase();
144    }
145
146    SQLiteDatabase getReadableDatabase() {
147        return mOpenHelper.getReadableDatabase();
148    }
149
150    @Override
151    public Cursor query(Uri uri, String[] projection, String selection,
152            String[] selectionArgs, String sortOrder) {
153        SQLiteDatabase db = getReadableDatabase();
154        if (db == null) {
155            return null;
156        }
157        final int match = URI_MATCHER.match(uri);
158        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
159        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
160        switch (match) {
161        case SNAPSHOTS_ID:
162            selection = DatabaseUtils.concatenateWhere(selection, "_id=?");
163            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
164                    new String[] { Long.toString(ContentUris.parseId(uri)) });
165            // fall through
166        case SNAPSHOTS:
167            qb.setTables(TABLE_SNAPSHOTS);
168            break;
169
170        default:
171            throw new UnsupportedOperationException("Unknown URL " + uri.toString());
172        }
173        Cursor cursor = qb.query(db, projection, selection, selectionArgs,
174                null, null, sortOrder, limit);
175        cursor.setNotificationUri(getContext().getContentResolver(),
176                AUTHORITY_URI);
177        return cursor;
178    }
179
180    @Override
181    public String getType(Uri uri) {
182        return null;
183    }
184
185    @Override
186    public Uri insert(Uri uri, ContentValues values) {
187        SQLiteDatabase db = getWritableDatabase();
188        if (db == null) {
189            return null;
190        }
191        int match = URI_MATCHER.match(uri);
192        long id = -1;
193        switch (match) {
194        case SNAPSHOTS:
195            if (!values.containsKey(Snapshots.VIEWSTATE)) {
196                values.put(Snapshots.VIEWSTATE, NULL_BLOB_HACK);
197            }
198            id = db.insert(TABLE_SNAPSHOTS, Snapshots.TITLE, values);
199            break;
200        default:
201            throw new UnsupportedOperationException("Unknown insert URI " + uri);
202        }
203        if (id < 0) {
204            return null;
205        }
206        Uri inserted = ContentUris.withAppendedId(uri, id);
207        getContext().getContentResolver().notifyChange(inserted, null, false);
208        return inserted;
209    }
210
211    static final String[] DELETE_PROJECTION = new String[] {
212        Snapshots.VIEWSTATE_PATH,
213    };
214    private void deleteDataFiles(SQLiteDatabase db, String selection,
215            String[] selectionArgs) {
216        Cursor c = db.query(TABLE_SNAPSHOTS, DELETE_PROJECTION, selection,
217                selectionArgs, null, null, null);
218        final Context context = getContext();
219        while (c.moveToNext()) {
220            String filename = c.getString(0);
221            if (TextUtils.isEmpty(filename)) {
222                continue;
223            }
224            File f = context.getFileStreamPath(filename);
225            if (f.exists()) {
226                if (!f.delete()) {
227                    f.deleteOnExit();
228                }
229            }
230        }
231        c.close();
232    }
233
234    @Override
235    public int delete(Uri uri, String selection, String[] selectionArgs) {
236        SQLiteDatabase db = getWritableDatabase();
237        if (db == null) {
238            return 0;
239        }
240        int match = URI_MATCHER.match(uri);
241        int deleted = 0;
242        switch (match) {
243        case SNAPSHOTS_ID: {
244            selection = DatabaseUtils.concatenateWhere(selection, TABLE_SNAPSHOTS + "._id=?");
245            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
246                    new String[] { Long.toString(ContentUris.parseId(uri)) });
247            // fall through
248        }
249        case SNAPSHOTS:
250            deleteDataFiles(db, selection, selectionArgs);
251            deleted = db.delete(TABLE_SNAPSHOTS, selection, selectionArgs);
252            break;
253        default:
254            throw new UnsupportedOperationException("Unknown delete URI " + uri);
255        }
256        if (deleted > 0) {
257            getContext().getContentResolver().notifyChange(uri, null, false);
258        }
259        return deleted;
260    }
261
262    @Override
263    public int update(Uri uri, ContentValues values, String selection,
264            String[] selectionArgs) {
265        throw new UnsupportedOperationException("not implemented");
266    }
267
268}
269