1 /* 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 17 package com.android.documentsui; 18 19 import static com.android.documentsui.model.DocumentInfo.getCursorString; 20 21 import android.content.ContentProvider; 22 import android.content.ContentResolver; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.UriMatcher; 27 import android.content.pm.ResolveInfo; 28 import android.database.Cursor; 29 import android.database.sqlite.SQLiteDatabase; 30 import android.database.sqlite.SQLiteOpenHelper; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.provider.DocumentsContract; 34 import android.provider.DocumentsContract.Document; 35 import android.provider.DocumentsContract.Root; 36 import android.text.format.DateUtils; 37 import android.util.Log; 38 39 import com.android.documentsui.model.DocumentStack; 40 import com.android.documentsui.model.DurableUtils; 41 import com.android.internal.util.Predicate; 42 import com.google.android.collect.Sets; 43 44 import libcore.io.IoUtils; 45 46 import java.io.IOException; 47 import java.util.Set; 48 49 public class RecentsProvider extends ContentProvider { 50 private static final String TAG = "RecentsProvider"; 51 52 private static final long MAX_HISTORY_IN_MILLIS = 45 * DateUtils.DAY_IN_MILLIS; 53 54 private static final String AUTHORITY = "com.android.documentsui.recents"; 55 56 private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); 57 58 private static final int URI_RECENT = 1; 59 private static final int URI_STATE = 2; 60 private static final int URI_RESUME = 3; 61 62 public static final String METHOD_PURGE = "purge"; 63 public static final String METHOD_PURGE_PACKAGE = "purgePackage"; 64 65 static { sMatcher.addURI(AUTHORITY, "recent", URI_RECENT)66 sMatcher.addURI(AUTHORITY, "recent", URI_RECENT); 67 // state/authority/rootId/docId sMatcher.addURI(AUTHORITY, "state/*/*/*", URI_STATE)68 sMatcher.addURI(AUTHORITY, "state/*/*/*", URI_STATE); 69 // resume/packageName sMatcher.addURI(AUTHORITY, "resume/*", URI_RESUME)70 sMatcher.addURI(AUTHORITY, "resume/*", URI_RESUME); 71 } 72 73 public static final String TABLE_RECENT = "recent"; 74 public static final String TABLE_STATE = "state"; 75 public static final String TABLE_RESUME = "resume"; 76 77 public static class RecentColumns { 78 public static final String KEY = "key"; 79 public static final String STACK = "stack"; 80 public static final String TIMESTAMP = "timestamp"; 81 } 82 83 public static class StateColumns { 84 public static final String AUTHORITY = "authority"; 85 public static final String ROOT_ID = Root.COLUMN_ROOT_ID; 86 public static final String DOCUMENT_ID = Document.COLUMN_DOCUMENT_ID; 87 public static final String MODE = "mode"; 88 public static final String SORT_ORDER = "sortOrder"; 89 } 90 91 public static class ResumeColumns { 92 public static final String PACKAGE_NAME = "package_name"; 93 public static final String STACK = "stack"; 94 public static final String TIMESTAMP = "timestamp"; 95 public static final String EXTERNAL = "external"; 96 } 97 buildRecent()98 public static Uri buildRecent() { 99 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 100 .authority(AUTHORITY).appendPath("recent").build(); 101 } 102 buildState(String authority, String rootId, String documentId)103 public static Uri buildState(String authority, String rootId, String documentId) { 104 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY) 105 .appendPath("state").appendPath(authority).appendPath(rootId).appendPath(documentId) 106 .build(); 107 } 108 buildResume(String packageName)109 public static Uri buildResume(String packageName) { 110 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 111 .authority(AUTHORITY).appendPath("resume").appendPath(packageName).build(); 112 } 113 114 private DatabaseHelper mHelper; 115 116 private static class DatabaseHelper extends SQLiteOpenHelper { 117 private static final String DB_NAME = "recents.db"; 118 119 private static final int VERSION_INIT = 1; 120 private static final int VERSION_AS_BLOB = 3; 121 private static final int VERSION_ADD_EXTERNAL = 4; 122 private static final int VERSION_ADD_RECENT_KEY = 5; 123 DatabaseHelper(Context context)124 public DatabaseHelper(Context context) { 125 super(context, DB_NAME, null, VERSION_ADD_RECENT_KEY); 126 } 127 128 @Override onCreate(SQLiteDatabase db)129 public void onCreate(SQLiteDatabase db) { 130 131 db.execSQL("CREATE TABLE " + TABLE_RECENT + " (" + 132 RecentColumns.KEY + " TEXT PRIMARY KEY ON CONFLICT REPLACE," + 133 RecentColumns.STACK + " BLOB DEFAULT NULL," + 134 RecentColumns.TIMESTAMP + " INTEGER" + 135 ")"); 136 137 db.execSQL("CREATE TABLE " + TABLE_STATE + " (" + 138 StateColumns.AUTHORITY + " TEXT," + 139 StateColumns.ROOT_ID + " TEXT," + 140 StateColumns.DOCUMENT_ID + " TEXT," + 141 StateColumns.MODE + " INTEGER," + 142 StateColumns.SORT_ORDER + " INTEGER," + 143 "PRIMARY KEY (" + StateColumns.AUTHORITY + ", " + StateColumns.ROOT_ID + ", " 144 + StateColumns.DOCUMENT_ID + ")" + 145 ")"); 146 147 db.execSQL("CREATE TABLE " + TABLE_RESUME + " (" + 148 ResumeColumns.PACKAGE_NAME + " TEXT NOT NULL PRIMARY KEY," + 149 ResumeColumns.STACK + " BLOB DEFAULT NULL," + 150 ResumeColumns.TIMESTAMP + " INTEGER," + 151 ResumeColumns.EXTERNAL + " INTEGER NOT NULL DEFAULT 0" + 152 ")"); 153 } 154 155 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)156 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 157 Log.w(TAG, "Upgrading database; wiping app data"); 158 db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT); 159 db.execSQL("DROP TABLE IF EXISTS " + TABLE_STATE); 160 db.execSQL("DROP TABLE IF EXISTS " + TABLE_RESUME); 161 onCreate(db); 162 } 163 } 164 165 @Override onCreate()166 public boolean onCreate() { 167 mHelper = new DatabaseHelper(getContext()); 168 return true; 169 } 170 171 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)172 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 173 String sortOrder) { 174 final SQLiteDatabase db = mHelper.getReadableDatabase(); 175 switch (sMatcher.match(uri)) { 176 case URI_RECENT: 177 final long cutoff = System.currentTimeMillis() - MAX_HISTORY_IN_MILLIS; 178 return db.query(TABLE_RECENT, projection, RecentColumns.TIMESTAMP + ">" + cutoff, 179 null, null, null, sortOrder); 180 case URI_STATE: 181 final String authority = uri.getPathSegments().get(1); 182 final String rootId = uri.getPathSegments().get(2); 183 final String documentId = uri.getPathSegments().get(3); 184 return db.query(TABLE_STATE, projection, StateColumns.AUTHORITY + "=? AND " 185 + StateColumns.ROOT_ID + "=? AND " + StateColumns.DOCUMENT_ID + "=?", 186 new String[] { authority, rootId, documentId }, null, null, sortOrder); 187 case URI_RESUME: 188 final String packageName = uri.getPathSegments().get(1); 189 return db.query(TABLE_RESUME, projection, ResumeColumns.PACKAGE_NAME + "=?", 190 new String[] { packageName }, null, null, sortOrder); 191 default: 192 throw new UnsupportedOperationException("Unsupported Uri " + uri); 193 } 194 } 195 196 @Override getType(Uri uri)197 public String getType(Uri uri) { 198 return null; 199 } 200 201 @Override insert(Uri uri, ContentValues values)202 public Uri insert(Uri uri, ContentValues values) { 203 final SQLiteDatabase db = mHelper.getWritableDatabase(); 204 final ContentValues key = new ContentValues(); 205 switch (sMatcher.match(uri)) { 206 case URI_RECENT: 207 values.put(RecentColumns.TIMESTAMP, System.currentTimeMillis()); 208 db.insert(TABLE_RECENT, null, values); 209 final long cutoff = System.currentTimeMillis() - MAX_HISTORY_IN_MILLIS; 210 db.delete(TABLE_RECENT, RecentColumns.TIMESTAMP + "<" + cutoff, null); 211 return uri; 212 case URI_STATE: 213 final String authority = uri.getPathSegments().get(1); 214 final String rootId = uri.getPathSegments().get(2); 215 final String documentId = uri.getPathSegments().get(3); 216 217 key.put(StateColumns.AUTHORITY, authority); 218 key.put(StateColumns.ROOT_ID, rootId); 219 key.put(StateColumns.DOCUMENT_ID, documentId); 220 221 // Ensure that row exists, then update with changed values 222 db.insertWithOnConflict(TABLE_STATE, null, key, SQLiteDatabase.CONFLICT_IGNORE); 223 db.update(TABLE_STATE, values, StateColumns.AUTHORITY + "=? AND " 224 + StateColumns.ROOT_ID + "=? AND " + StateColumns.DOCUMENT_ID + "=?", 225 new String[] { authority, rootId, documentId }); 226 227 return uri; 228 case URI_RESUME: 229 values.put(ResumeColumns.TIMESTAMP, System.currentTimeMillis()); 230 231 final String packageName = uri.getPathSegments().get(1); 232 key.put(ResumeColumns.PACKAGE_NAME, packageName); 233 234 // Ensure that row exists, then update with changed values 235 db.insertWithOnConflict(TABLE_RESUME, null, key, SQLiteDatabase.CONFLICT_IGNORE); 236 db.update(TABLE_RESUME, values, ResumeColumns.PACKAGE_NAME + "=?", 237 new String[] { packageName }); 238 return uri; 239 default: 240 throw new UnsupportedOperationException("Unsupported Uri " + uri); 241 } 242 } 243 244 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)245 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 246 throw new UnsupportedOperationException("Unsupported Uri " + uri); 247 } 248 249 @Override delete(Uri uri, String selection, String[] selectionArgs)250 public int delete(Uri uri, String selection, String[] selectionArgs) { 251 throw new UnsupportedOperationException("Unsupported Uri " + uri); 252 } 253 254 @Override call(String method, String arg, Bundle extras)255 public Bundle call(String method, String arg, Bundle extras) { 256 if (METHOD_PURGE.equals(method)) { 257 // Purge references to unknown authorities 258 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); 259 final Set<String> knownAuth = Sets.newHashSet(); 260 for (ResolveInfo info : getContext() 261 .getPackageManager().queryIntentContentProviders(intent, 0)) { 262 knownAuth.add(info.providerInfo.authority); 263 } 264 265 purgeByAuthority(new Predicate<String>() { 266 @Override 267 public boolean apply(String authority) { 268 // Purge unknown authorities 269 return !knownAuth.contains(authority); 270 } 271 }); 272 273 return null; 274 275 } else if (METHOD_PURGE_PACKAGE.equals(method)) { 276 // Purge references to authorities in given package 277 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); 278 intent.setPackage(arg); 279 final Set<String> packageAuth = Sets.newHashSet(); 280 for (ResolveInfo info : getContext() 281 .getPackageManager().queryIntentContentProviders(intent, 0)) { 282 packageAuth.add(info.providerInfo.authority); 283 } 284 285 if (!packageAuth.isEmpty()) { 286 purgeByAuthority(new Predicate<String>() { 287 @Override 288 public boolean apply(String authority) { 289 // Purge authority matches 290 return packageAuth.contains(authority); 291 } 292 }); 293 } 294 295 return null; 296 297 } else { 298 return super.call(method, arg, extras); 299 } 300 } 301 302 /** 303 * Purge all internal data whose authority matches the given 304 * {@link Predicate}. 305 */ purgeByAuthority(Predicate<String> predicate)306 private void purgeByAuthority(Predicate<String> predicate) { 307 final SQLiteDatabase db = mHelper.getWritableDatabase(); 308 final DocumentStack stack = new DocumentStack(); 309 310 Cursor cursor = db.query(TABLE_RECENT, null, null, null, null, null, null); 311 try { 312 while (cursor.moveToNext()) { 313 try { 314 final byte[] rawStack = cursor.getBlob( 315 cursor.getColumnIndex(RecentColumns.STACK)); 316 DurableUtils.readFromArray(rawStack, stack); 317 318 if (stack.root != null && predicate.apply(stack.root.authority)) { 319 final String key = getCursorString(cursor, RecentColumns.KEY); 320 db.delete(TABLE_RECENT, RecentColumns.KEY + "=?", new String[] { key }); 321 } 322 } catch (IOException ignored) { 323 } 324 } 325 } finally { 326 IoUtils.closeQuietly(cursor); 327 } 328 329 cursor = db.query(TABLE_STATE, new String[] { 330 StateColumns.AUTHORITY }, null, null, StateColumns.AUTHORITY, null, null); 331 try { 332 while (cursor.moveToNext()) { 333 final String authority = getCursorString(cursor, StateColumns.AUTHORITY); 334 if (predicate.apply(authority)) { 335 db.delete(TABLE_STATE, StateColumns.AUTHORITY + "=?", new String[] { 336 authority }); 337 Log.d(TAG, "Purged state for " + authority); 338 } 339 } 340 } finally { 341 IoUtils.closeQuietly(cursor); 342 } 343 344 cursor = db.query(TABLE_RESUME, null, null, null, null, null, null); 345 try { 346 while (cursor.moveToNext()) { 347 try { 348 final byte[] rawStack = cursor.getBlob( 349 cursor.getColumnIndex(ResumeColumns.STACK)); 350 DurableUtils.readFromArray(rawStack, stack); 351 352 if (stack.root != null && predicate.apply(stack.root.authority)) { 353 final String packageName = getCursorString( 354 cursor, ResumeColumns.PACKAGE_NAME); 355 db.delete(TABLE_RESUME, ResumeColumns.PACKAGE_NAME + "=?", 356 new String[] { packageName }); 357 } 358 } catch (IOException ignored) { 359 } 360 } 361 } finally { 362 IoUtils.closeQuietly(cursor); 363 } 364 } 365 } 366