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