blob: 92ffb93365d20790ed8b1de0b6266f05ae8aae17 [file] [log] [blame]
Jeff Sharkey92d7e692013-08-02 10:33:21 -07001/*
2 * Copyright (C) 2013 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.documentsui;
18
Jeff Sharkey758f97e2013-10-24 10:44:03 -070019import static com.android.documentsui.model.DocumentInfo.getCursorString;
20
Jeff Sharkey92d7e692013-08-02 10:33:21 -070021import android.content.ContentProvider;
Jeff Sharkeydc2963a2013-08-02 15:55:26 -070022import android.content.ContentResolver;
Jeff Sharkey92d7e692013-08-02 10:33:21 -070023import android.content.ContentValues;
24import android.content.Context;
Jeff Sharkey758f97e2013-10-24 10:44:03 -070025import android.content.Intent;
Jeff Sharkey92d7e692013-08-02 10:33:21 -070026import android.content.UriMatcher;
Jeff Sharkey758f97e2013-10-24 10:44:03 -070027import android.content.pm.ResolveInfo;
Jeff Sharkey92d7e692013-08-02 10:33:21 -070028import android.database.Cursor;
29import android.database.sqlite.SQLiteDatabase;
30import android.database.sqlite.SQLiteOpenHelper;
31import android.net.Uri;
Jeff Sharkey758f97e2013-10-24 10:44:03 -070032import android.os.Bundle;
33import android.provider.DocumentsContract;
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070034import android.provider.DocumentsContract.Document;
35import android.provider.DocumentsContract.Root;
Jeff Sharkey92d7e692013-08-02 10:33:21 -070036import android.text.format.DateUtils;
37import android.util.Log;
38
Jeff Sharkey758f97e2013-10-24 10:44:03 -070039import com.android.documentsui.model.DocumentStack;
40import com.android.documentsui.model.DurableUtils;
41import com.android.internal.util.Predicate;
Steve McKay7a3b88c2015-09-23 17:21:40 -070042
Jeff Sharkey758f97e2013-10-24 10:44:03 -070043import com.google.android.collect.Sets;
44
45import libcore.io.IoUtils;
46
47import java.io.IOException;
48import java.util.Set;
49
Jeff Sharkey92d7e692013-08-02 10:33:21 -070050public class RecentsProvider extends ContentProvider {
51 private static final String TAG = "RecentsProvider";
52
Jeff Sharkey758f97e2013-10-24 10:44:03 -070053 private static final long MAX_HISTORY_IN_MILLIS = 45 * DateUtils.DAY_IN_MILLIS;
Jeff Sharkeydc2963a2013-08-02 15:55:26 -070054
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070055 private static final String AUTHORITY = "com.android.documentsui.recents";
Jeff Sharkey92d7e692013-08-02 10:33:21 -070056
57 private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
58
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070059 private static final int URI_RECENT = 1;
60 private static final int URI_STATE = 2;
Jeff Sharkey92d7e692013-08-02 10:33:21 -070061 private static final int URI_RESUME = 3;
62
Jeff Sharkey758f97e2013-10-24 10:44:03 -070063 public static final String METHOD_PURGE = "purge";
64 public static final String METHOD_PURGE_PACKAGE = "purgePackage";
65
Jeff Sharkey92d7e692013-08-02 10:33:21 -070066 static {
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070067 sMatcher.addURI(AUTHORITY, "recent", URI_RECENT);
68 // state/authority/rootId/docId
69 sMatcher.addURI(AUTHORITY, "state/*/*/*", URI_STATE);
70 // resume/packageName
Jeff Sharkey92d7e692013-08-02 10:33:21 -070071 sMatcher.addURI(AUTHORITY, "resume/*", URI_RESUME);
72 }
73
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070074 public static final String TABLE_RECENT = "recent";
75 public static final String TABLE_STATE = "state";
76 public static final String TABLE_RESUME = "resume";
Jeff Sharkey92d7e692013-08-02 10:33:21 -070077
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070078 public static class RecentColumns {
Jeff Sharkey6efba222013-09-27 16:44:11 -070079 public static final String KEY = "key";
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070080 public static final String STACK = "stack";
81 public static final String TIMESTAMP = "timestamp";
Jeff Sharkeydc2963a2013-08-02 15:55:26 -070082 }
83
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070084 public static class StateColumns {
85 public static final String AUTHORITY = "authority";
86 public static final String ROOT_ID = Root.COLUMN_ROOT_ID;
87 public static final String DOCUMENT_ID = Document.COLUMN_DOCUMENT_ID;
Steve McKay3eb2d072016-01-25 19:00:22 -080088
89 @Deprecated // mode is tracked in local preferences now...by root only
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070090 public static final String MODE = "mode";
91 public static final String SORT_ORDER = "sortOrder";
92 }
93
94 public static class ResumeColumns {
95 public static final String PACKAGE_NAME = "package_name";
96 public static final String STACK = "stack";
97 public static final String TIMESTAMP = "timestamp";
Jeff Sharkeydeffade2013-09-24 12:07:12 -070098 public static final String EXTERNAL = "external";
Jeff Sharkeyd182bb62013-09-07 14:45:03 -070099 }
100
101 public static Uri buildRecent() {
Jeff Sharkeydc2963a2013-08-02 15:55:26 -0700102 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700103 .authority(AUTHORITY).appendPath("recent").build();
104 }
105
106 public static Uri buildState(String authority, String rootId, String documentId) {
107 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY)
108 .appendPath("state").appendPath(authority).appendPath(rootId).appendPath(documentId)
109 .build();
Jeff Sharkeydc2963a2013-08-02 15:55:26 -0700110 }
111
112 public static Uri buildResume(String packageName) {
113 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
114 .authority(AUTHORITY).appendPath("resume").appendPath(packageName).build();
115 }
116
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700117 private DatabaseHelper mHelper;
118
119 private static class DatabaseHelper extends SQLiteOpenHelper {
Jeff Sharkeydc2963a2013-08-02 15:55:26 -0700120 private static final String DB_NAME = "recents.db";
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700121
122 private static final int VERSION_INIT = 1;
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700123 private static final int VERSION_AS_BLOB = 3;
Jeff Sharkeydeffade2013-09-24 12:07:12 -0700124 private static final int VERSION_ADD_EXTERNAL = 4;
Jeff Sharkey6efba222013-09-27 16:44:11 -0700125 private static final int VERSION_ADD_RECENT_KEY = 5;
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700126
127 public DatabaseHelper(Context context) {
Jeff Sharkey6efba222013-09-27 16:44:11 -0700128 super(context, DB_NAME, null, VERSION_ADD_RECENT_KEY);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700129 }
130
131 @Override
132 public void onCreate(SQLiteDatabase db) {
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700133
134 db.execSQL("CREATE TABLE " + TABLE_RECENT + " (" +
Jeff Sharkey6efba222013-09-27 16:44:11 -0700135 RecentColumns.KEY + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
136 RecentColumns.STACK + " BLOB DEFAULT NULL," +
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700137 RecentColumns.TIMESTAMP + " INTEGER" +
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700138 ")");
139
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700140 db.execSQL("CREATE TABLE " + TABLE_STATE + " (" +
141 StateColumns.AUTHORITY + " TEXT," +
142 StateColumns.ROOT_ID + " TEXT," +
143 StateColumns.DOCUMENT_ID + " TEXT," +
144 StateColumns.MODE + " INTEGER," +
145 StateColumns.SORT_ORDER + " INTEGER," +
146 "PRIMARY KEY (" + StateColumns.AUTHORITY + ", " + StateColumns.ROOT_ID + ", "
147 + StateColumns.DOCUMENT_ID + ")" +
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700148 ")");
149
150 db.execSQL("CREATE TABLE " + TABLE_RESUME + " (" +
Jeff Sharkeydeffade2013-09-24 12:07:12 -0700151 ResumeColumns.PACKAGE_NAME + " TEXT NOT NULL PRIMARY KEY," +
152 ResumeColumns.STACK + " BLOB DEFAULT NULL," +
153 ResumeColumns.TIMESTAMP + " INTEGER," +
154 ResumeColumns.EXTERNAL + " INTEGER NOT NULL DEFAULT 0" +
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700155 ")");
156 }
157
158 @Override
159 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
160 Log.w(TAG, "Upgrading database; wiping app data");
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700161 db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT);
162 db.execSQL("DROP TABLE IF EXISTS " + TABLE_STATE);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700163 db.execSQL("DROP TABLE IF EXISTS " + TABLE_RESUME);
164 onCreate(db);
165 }
166 }
167
168 @Override
169 public boolean onCreate() {
170 mHelper = new DatabaseHelper(getContext());
171 return true;
172 }
173
174 @Override
175 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
176 String sortOrder) {
177 final SQLiteDatabase db = mHelper.getReadableDatabase();
178 switch (sMatcher.match(uri)) {
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700179 case URI_RECENT:
Jeff Sharkey3f4c2052013-09-09 16:51:06 -0700180 final long cutoff = System.currentTimeMillis() - MAX_HISTORY_IN_MILLIS;
181 return db.query(TABLE_RECENT, projection, RecentColumns.TIMESTAMP + ">" + cutoff,
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700182 null, null, null, sortOrder);
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700183 case URI_STATE:
184 final String authority = uri.getPathSegments().get(1);
185 final String rootId = uri.getPathSegments().get(2);
186 final String documentId = uri.getPathSegments().get(3);
187 return db.query(TABLE_STATE, projection, StateColumns.AUTHORITY + "=? AND "
188 + StateColumns.ROOT_ID + "=? AND " + StateColumns.DOCUMENT_ID + "=?",
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700189 new String[] { authority, rootId, documentId }, null, null, sortOrder);
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700190 case URI_RESUME:
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700191 final String packageName = uri.getPathSegments().get(1);
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700192 return db.query(TABLE_RESUME, projection, ResumeColumns.PACKAGE_NAME + "=?",
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700193 new String[] { packageName }, null, null, sortOrder);
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700194 default:
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700195 throw new UnsupportedOperationException("Unsupported Uri " + uri);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700196 }
197 }
198
199 @Override
200 public String getType(Uri uri) {
201 return null;
202 }
203
204 @Override
205 public Uri insert(Uri uri, ContentValues values) {
206 final SQLiteDatabase db = mHelper.getWritableDatabase();
Jeff Sharkeydeffade2013-09-24 12:07:12 -0700207 final ContentValues key = new ContentValues();
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700208 switch (sMatcher.match(uri)) {
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700209 case URI_RECENT:
210 values.put(RecentColumns.TIMESTAMP, System.currentTimeMillis());
211 db.insert(TABLE_RECENT, null, values);
Jeff Sharkey3f4c2052013-09-09 16:51:06 -0700212 final long cutoff = System.currentTimeMillis() - MAX_HISTORY_IN_MILLIS;
213 db.delete(TABLE_RECENT, RecentColumns.TIMESTAMP + "<" + cutoff, null);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700214 return uri;
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700215 case URI_STATE:
216 final String authority = uri.getPathSegments().get(1);
217 final String rootId = uri.getPathSegments().get(2);
218 final String documentId = uri.getPathSegments().get(3);
219
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700220 key.put(StateColumns.AUTHORITY, authority);
221 key.put(StateColumns.ROOT_ID, rootId);
222 key.put(StateColumns.DOCUMENT_ID, documentId);
223
224 // Ensure that row exists, then update with changed values
225 db.insertWithOnConflict(TABLE_STATE, null, key, SQLiteDatabase.CONFLICT_IGNORE);
226 db.update(TABLE_STATE, values, StateColumns.AUTHORITY + "=? AND "
227 + StateColumns.ROOT_ID + "=? AND " + StateColumns.DOCUMENT_ID + "=?",
228 new String[] { authority, rootId, documentId });
229
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700230 return uri;
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700231 case URI_RESUME:
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700232 values.put(ResumeColumns.TIMESTAMP, System.currentTimeMillis());
Jeff Sharkeydeffade2013-09-24 12:07:12 -0700233
234 final String packageName = uri.getPathSegments().get(1);
235 key.put(ResumeColumns.PACKAGE_NAME, packageName);
236
237 // Ensure that row exists, then update with changed values
238 db.insertWithOnConflict(TABLE_RESUME, null, key, SQLiteDatabase.CONFLICT_IGNORE);
239 db.update(TABLE_RESUME, values, ResumeColumns.PACKAGE_NAME + "=?",
240 new String[] { packageName });
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700241 return uri;
Jeff Sharkeyd182bb62013-09-07 14:45:03 -0700242 default:
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700243 throw new UnsupportedOperationException("Unsupported Uri " + uri);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700244 }
245 }
246
247 @Override
248 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
249 throw new UnsupportedOperationException("Unsupported Uri " + uri);
250 }
251
252 @Override
253 public int delete(Uri uri, String selection, String[] selectionArgs) {
254 throw new UnsupportedOperationException("Unsupported Uri " + uri);
255 }
Jeff Sharkey758f97e2013-10-24 10:44:03 -0700256
257 @Override
258 public Bundle call(String method, String arg, Bundle extras) {
259 if (METHOD_PURGE.equals(method)) {
260 // Purge references to unknown authorities
261 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
262 final Set<String> knownAuth = Sets.newHashSet();
263 for (ResolveInfo info : getContext()
264 .getPackageManager().queryIntentContentProviders(intent, 0)) {
265 knownAuth.add(info.providerInfo.authority);
266 }
267
268 purgeByAuthority(new Predicate<String>() {
269 @Override
270 public boolean apply(String authority) {
271 // Purge unknown authorities
272 return !knownAuth.contains(authority);
273 }
274 });
275
276 return null;
277
278 } else if (METHOD_PURGE_PACKAGE.equals(method)) {
279 // Purge references to authorities in given package
280 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
281 intent.setPackage(arg);
282 final Set<String> packageAuth = Sets.newHashSet();
283 for (ResolveInfo info : getContext()
284 .getPackageManager().queryIntentContentProviders(intent, 0)) {
285 packageAuth.add(info.providerInfo.authority);
286 }
287
288 if (!packageAuth.isEmpty()) {
289 purgeByAuthority(new Predicate<String>() {
290 @Override
291 public boolean apply(String authority) {
292 // Purge authority matches
293 return packageAuth.contains(authority);
294 }
295 });
296 }
297
298 return null;
299
300 } else {
301 return super.call(method, arg, extras);
302 }
303 }
304
305 /**
306 * Purge all internal data whose authority matches the given
307 * {@link Predicate}.
308 */
309 private void purgeByAuthority(Predicate<String> predicate) {
310 final SQLiteDatabase db = mHelper.getWritableDatabase();
311 final DocumentStack stack = new DocumentStack();
312
313 Cursor cursor = db.query(TABLE_RECENT, null, null, null, null, null, null);
314 try {
315 while (cursor.moveToNext()) {
316 try {
317 final byte[] rawStack = cursor.getBlob(
318 cursor.getColumnIndex(RecentColumns.STACK));
319 DurableUtils.readFromArray(rawStack, stack);
320
321 if (stack.root != null && predicate.apply(stack.root.authority)) {
322 final String key = getCursorString(cursor, RecentColumns.KEY);
323 db.delete(TABLE_RECENT, RecentColumns.KEY + "=?", new String[] { key });
324 }
325 } catch (IOException ignored) {
326 }
327 }
328 } finally {
329 IoUtils.closeQuietly(cursor);
330 }
331
332 cursor = db.query(TABLE_STATE, new String[] {
333 StateColumns.AUTHORITY }, null, null, StateColumns.AUTHORITY, null, null);
334 try {
335 while (cursor.moveToNext()) {
336 final String authority = getCursorString(cursor, StateColumns.AUTHORITY);
337 if (predicate.apply(authority)) {
338 db.delete(TABLE_STATE, StateColumns.AUTHORITY + "=?", new String[] {
339 authority });
340 Log.d(TAG, "Purged state for " + authority);
341 }
342 }
343 } finally {
344 IoUtils.closeQuietly(cursor);
345 }
346
347 cursor = db.query(TABLE_RESUME, null, null, null, null, null, null);
348 try {
349 while (cursor.moveToNext()) {
350 try {
351 final byte[] rawStack = cursor.getBlob(
352 cursor.getColumnIndex(ResumeColumns.STACK));
353 DurableUtils.readFromArray(rawStack, stack);
354
355 if (stack.root != null && predicate.apply(stack.root.authority)) {
356 final String packageName = getCursorString(
357 cursor, ResumeColumns.PACKAGE_NAME);
358 db.delete(TABLE_RESUME, ResumeColumns.PACKAGE_NAME + "=?",
359 new String[] { packageName });
360 }
361 } catch (IOException ignored) {
362 }
363 }
364 } finally {
365 IoUtils.closeQuietly(cursor);
366 }
367 }
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700368}