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.BroadcastReceiver;
19import android.content.ContentProvider;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.UriMatcher;
26import android.database.Cursor;
27import android.database.DatabaseUtils;
28import android.database.sqlite.SQLiteDatabase;
29import android.database.sqlite.SQLiteOpenHelper;
30import android.database.sqlite.SQLiteQueryBuilder;
31import android.net.Uri;
32import android.os.Environment;
33import android.provider.BrowserContract;
34
35import java.io.File;
36
37/**
38 * This provider is expected to be potentially flaky. It uses a database
39 * stored on external storage, which could be yanked unexpectedly.
40 */
41public class SnapshotProvider extends ContentProvider {
42
43    public static interface Snapshots {
44
45        public static final Uri CONTENT_URI = Uri.withAppendedPath(
46                SnapshotProvider.AUTHORITY_URI, "snapshots");
47        public static final String _ID = "_id";
48        public static final String VIEWSTATE = "view_state";
49        public static final String BACKGROUND = "background";
50        public static final String TITLE = "title";
51        public static final String URL = "url";
52        public static final String FAVICON = "favicon";
53        public static final String THUMBNAIL = "thumbnail";
54        public static final String DATE_CREATED = "date_created";
55    }
56
57    public static final String AUTHORITY = "com.android.browser.snapshots";
58    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
59
60    static final String TABLE_SNAPSHOTS = "snapshots";
61    static final int SNAPSHOTS = 10;
62    static final int SNAPSHOTS_ID = 11;
63    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
64
65    SnapshotDatabaseHelper mOpenHelper;
66
67    static {
68        URI_MATCHER.addURI(AUTHORITY, "snapshots", SNAPSHOTS);
69        URI_MATCHER.addURI(AUTHORITY, "snapshots/#", SNAPSHOTS_ID);
70    }
71
72    final static class SnapshotDatabaseHelper extends SQLiteOpenHelper {
73
74        static final String DATABASE_NAME = "snapshots.db";
75        static final int DATABASE_VERSION = 2;
76
77        public SnapshotDatabaseHelper(Context context) {
78            super(context, getFullDatabaseName(context), null, DATABASE_VERSION);
79        }
80
81        static String getFullDatabaseName(Context context) {
82            File dir = context.getExternalFilesDir(null);
83            return new File(dir, DATABASE_NAME).getAbsolutePath();
84        }
85
86        @Override
87        public void onCreate(SQLiteDatabase db) {
88            db.execSQL("CREATE TABLE " + TABLE_SNAPSHOTS + "(" +
89                    Snapshots._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
90                    Snapshots.TITLE + " TEXT," +
91                    Snapshots.URL + " TEXT NOT NULL," +
92                    Snapshots.DATE_CREATED + " INTEGER," +
93                    Snapshots.FAVICON + " BLOB," +
94                    Snapshots.THUMBNAIL + " BLOB," +
95                    Snapshots.BACKGROUND + " INTEGER," +
96                    Snapshots.VIEWSTATE + " BLOB NOT NULL" +
97                    ");");
98        }
99
100        @Override
101        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
102            if (oldVersion < 2) {
103                db.execSQL("DROP TABLE " + TABLE_SNAPSHOTS);
104                onCreate(db);
105            }
106        }
107
108    }
109
110    @Override
111    public boolean onCreate() {
112        IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
113        filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
114        getContext().registerReceiver(mExternalStorageReceiver, filter);
115        return true;
116    }
117
118    final BroadcastReceiver mExternalStorageReceiver = new BroadcastReceiver() {
119
120        @Override
121        public void onReceive(Context context, Intent intent) {
122            if (mOpenHelper != null) {
123                try {
124                    mOpenHelper.close();
125                } catch (Throwable t) {
126                    // We failed to close the open helper, which most likely means
127                    // another thread is busy attempting to open the database
128                    // or use the database. Let that thread try to gracefully
129                    // deal with the error
130                }
131            }
132        }
133    };
134
135    SQLiteDatabase getWritableDatabase() {
136        String state = Environment.getExternalStorageState();
137        if (Environment.MEDIA_MOUNTED.equals(state)) {
138            try {
139                if (mOpenHelper == null) {
140                    mOpenHelper = new SnapshotDatabaseHelper(getContext());
141                }
142                return mOpenHelper.getWritableDatabase();
143            } catch (Throwable t) {
144                return null;
145            }
146        }
147        return null;
148    }
149
150    SQLiteDatabase getReadableDatabase() {
151        String state = Environment.getExternalStorageState();
152        if (Environment.MEDIA_MOUNTED.equals(state)
153                || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
154            try {
155                if (mOpenHelper == null) {
156                    mOpenHelper = new SnapshotDatabaseHelper(getContext());
157                }
158                return mOpenHelper.getReadableDatabase();
159            } catch (Throwable t) {
160                return null;
161            }
162        }
163        return null;
164    }
165
166    @Override
167    public Cursor query(Uri uri, String[] projection, String selection,
168            String[] selectionArgs, String sortOrder) {
169        SQLiteDatabase db = getReadableDatabase();
170        if (db == null) {
171            return null;
172        }
173        final int match = URI_MATCHER.match(uri);
174        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
175        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
176        switch (match) {
177        case SNAPSHOTS_ID:
178            selection = DatabaseUtils.concatenateWhere(selection, "_id=?");
179            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
180                    new String[] { Long.toString(ContentUris.parseId(uri)) });
181            // fall through
182        case SNAPSHOTS:
183            qb.setTables(TABLE_SNAPSHOTS);
184            break;
185
186        default:
187            throw new UnsupportedOperationException("Unknown URL " + uri.toString());
188        }
189        try {
190            Cursor cursor = qb.query(db, projection, selection, selectionArgs,
191                    null, null, sortOrder, limit);
192            cursor.setNotificationUri(getContext().getContentResolver(),
193                    AUTHORITY_URI);
194            return cursor;
195        } catch (Throwable t) {
196            return null;
197        }
198    }
199
200    @Override
201    public String getType(Uri uri) {
202        return null;
203    }
204
205    @Override
206    public Uri insert(Uri uri, ContentValues values) {
207        SQLiteDatabase db = getWritableDatabase();
208        if (db == null) {
209            return null;
210        }
211        int match = URI_MATCHER.match(uri);
212        long id = -1;
213        switch (match) {
214        case SNAPSHOTS:
215            try {
216                id = db.insert(TABLE_SNAPSHOTS, Snapshots.TITLE, values);
217            } catch (Throwable t) {
218                id = -1;
219            }
220            break;
221        default:
222            throw new UnsupportedOperationException("Unknown insert URI " + uri);
223        }
224        if (id < 0) {
225            return null;
226        }
227        Uri inserted = ContentUris.withAppendedId(uri, id);
228        getContext().getContentResolver().notifyChange(inserted, null, false);
229        return inserted;
230    }
231
232    @Override
233    public int delete(Uri uri, String selection, String[] selectionArgs) {
234        SQLiteDatabase db = getWritableDatabase();
235        if (db == null) {
236            return 0;
237        }
238        int match = URI_MATCHER.match(uri);
239        int deleted = 0;
240        switch (match) {
241        case SNAPSHOTS_ID: {
242            selection = DatabaseUtils.concatenateWhere(selection, TABLE_SNAPSHOTS + "._id=?");
243            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
244                    new String[] { Long.toString(ContentUris.parseId(uri)) });
245            // fall through
246        }
247        case SNAPSHOTS:
248            try {
249                deleted = db.delete(TABLE_SNAPSHOTS, selection, selectionArgs);
250            } catch (Throwable t) {
251            }
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