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.picker; 18 19 import static com.android.documentsui.base.DocumentInfo.getCursorString; 20 21 import android.app.Activity; 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.os.FileUtils; 35 import android.provider.DocumentsContract; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import com.android.documentsui.base.DocumentStack; 40 import com.android.documentsui.base.DurableUtils; 41 import com.android.documentsui.base.UserId; 42 43 import java.io.IOException; 44 import java.util.HashSet; 45 import java.util.Set; 46 import java.util.function.Predicate; 47 48 /* 49 * Provider used to keep track of the last known directory navigation trail done by the user 50 */ 51 public class LastAccessedProvider extends ContentProvider { 52 private static final String TAG = "LastAccessedProvider"; 53 54 private static final String AUTHORITY = "com.android.documentsui.lastAccessed"; 55 56 private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); 57 58 private static final int URI_LAST_ACCESSED = 1; 59 60 public static final String METHOD_PURGE = "purge"; 61 public static final String METHOD_PURGE_PACKAGE = "purgePackage"; 62 63 static { sMatcher.addURI(AUTHORITY, "lastAccessed/*", URI_LAST_ACCESSED)64 sMatcher.addURI(AUTHORITY, "lastAccessed/*", URI_LAST_ACCESSED); 65 } 66 67 public static final String TABLE_LAST_ACCESSED = "lastAccessed"; 68 69 public static class Columns { 70 public static final String PACKAGE_NAME = "package_name"; 71 public static final String STACK = "stack"; 72 public static final String TIMESTAMP = "timestamp"; 73 // Indicates handler was an external app, like photos. 74 public static final String EXTERNAL = "external"; 75 } 76 buildLastAccessed(String packageName)77 public static Uri buildLastAccessed(String packageName) { 78 return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 79 .authority(AUTHORITY).appendPath("lastAccessed").appendPath(packageName).build(); 80 } 81 82 private DatabaseHelper mHelper; 83 84 private static class DatabaseHelper extends SQLiteOpenHelper { 85 private static final String DB_NAME = "lastAccess.db"; 86 87 // Used for backwards compatibility 88 private static final int VERSION_INIT = 1; 89 private static final int VERSION_AS_BLOB = 3; 90 private static final int VERSION_ADD_EXTERNAL = 4; 91 private static final int VERSION_ADD_RECENT_KEY = 5; 92 93 private static final int VERSION_LAST_ACCESS_REFACTOR = 6; 94 DatabaseHelper(Context context)95 public DatabaseHelper(Context context) { 96 super(context, DB_NAME, null, VERSION_LAST_ACCESS_REFACTOR); 97 } 98 99 @Override onCreate(SQLiteDatabase db)100 public void onCreate(SQLiteDatabase db) { 101 102 db.execSQL("CREATE TABLE " + TABLE_LAST_ACCESSED + " (" + 103 Columns.PACKAGE_NAME + " TEXT NOT NULL PRIMARY KEY," + 104 Columns.STACK + " BLOB DEFAULT NULL," + 105 Columns.TIMESTAMP + " INTEGER," + 106 Columns.EXTERNAL + " INTEGER NOT NULL DEFAULT 0" + 107 ")"); 108 } 109 110 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)111 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 112 Log.w(TAG, "Upgrading database; wiping app data"); 113 db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_ACCESSED); 114 onCreate(db); 115 } 116 } 117 118 /** 119 * Rather than concretely depending on LastAccessedProvider, consider using 120 * {@link LastAccessedStorage#setLastAccessed(Activity, DocumentStack)}. 121 */ 122 @Deprecated setLastAccessed( ContentResolver resolver, String packageName, DocumentStack stack)123 static void setLastAccessed( 124 ContentResolver resolver, String packageName, DocumentStack stack) { 125 final ContentValues values = new ContentValues(); 126 values.clear(); 127 if (stack.getRoot() != null && !UserId.CURRENT_USER.equals(stack.getRoot().userId)) { 128 // Do not remember and clear the stack if it is not from the current user. Next time 129 // it will launch into default root. 130 values.put(Columns.STACK, (Byte) null); 131 } else { 132 final byte[] rawStack = DurableUtils.writeToArrayOrNull(stack); 133 values.put(Columns.STACK, rawStack); 134 } 135 values.put(Columns.EXTERNAL, 0); 136 resolver.insert(buildLastAccessed(packageName), values); 137 } 138 139 @Override onCreate()140 public boolean onCreate() { 141 mHelper = new DatabaseHelper(getContext()); 142 return true; 143 } 144 145 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)146 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 147 String sortOrder) { 148 if (sMatcher.match(uri) != URI_LAST_ACCESSED) { 149 throw new UnsupportedOperationException("Unsupported Uri " + uri); 150 } 151 152 final SQLiteDatabase db = mHelper.getReadableDatabase(); 153 final String packageName = uri.getPathSegments().get(1); 154 return db.query(TABLE_LAST_ACCESSED, projection, Columns.PACKAGE_NAME + "=?", 155 new String[] { packageName }, null, null, sortOrder); 156 } 157 158 @Override getType(Uri uri)159 public String getType(Uri uri) { 160 return null; 161 } 162 163 @Override insert(Uri uri, ContentValues values)164 public Uri insert(Uri uri, ContentValues values) { 165 if (sMatcher.match(uri) != URI_LAST_ACCESSED) { 166 throw new UnsupportedOperationException("Unsupported Uri " + uri); 167 } 168 169 final SQLiteDatabase db = mHelper.getWritableDatabase(); 170 final ContentValues key = new ContentValues(); 171 172 values.put(Columns.TIMESTAMP, System.currentTimeMillis()); 173 174 final String packageName = uri.getPathSegments().get(1); 175 key.put(Columns.PACKAGE_NAME, packageName); 176 177 // Ensure that row exists, then update with changed values 178 db.insertWithOnConflict(TABLE_LAST_ACCESSED, null, key, SQLiteDatabase.CONFLICT_IGNORE); 179 db.update(TABLE_LAST_ACCESSED, values, Columns.PACKAGE_NAME + "=?", 180 new String[] { packageName }); 181 return uri; 182 } 183 184 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)185 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 186 throw new UnsupportedOperationException("Unsupported Uri " + uri); 187 } 188 189 @Override delete(Uri uri, String selection, String[] selectionArgs)190 public int delete(Uri uri, String selection, String[] selectionArgs) { 191 throw new UnsupportedOperationException("Unsupported Uri " + uri); 192 } 193 194 @Override call(String method, String arg, Bundle extras)195 public Bundle call(String method, String arg, Bundle extras) { 196 if (METHOD_PURGE.equals(method)) { 197 // Purge references to unknown authorities 198 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); 199 final Set<String> knownAuth = new HashSet<>(); 200 for (ResolveInfo info : getContext() 201 .getPackageManager().queryIntentContentProviders(intent, 0)) { 202 if (info != null && !TextUtils.isEmpty(info.providerInfo.authority)) { 203 knownAuth.add(info.providerInfo.authority); 204 } 205 } 206 207 purgeByAuthority(new Predicate<String>() { 208 @Override 209 public boolean test(String authority) { 210 // Purge unknown authorities 211 return !knownAuth.contains(authority); 212 } 213 }); 214 215 return null; 216 217 } else if (METHOD_PURGE_PACKAGE.equals(method)) { 218 // Purge references to authorities in given package 219 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); 220 intent.setPackage(arg); 221 final Set<String> packageAuth = new HashSet<>(); 222 for (ResolveInfo info : getContext() 223 .getPackageManager().queryIntentContentProviders(intent, 0)) { 224 packageAuth.add(info.providerInfo.authority); 225 } 226 227 if (!packageAuth.isEmpty()) { 228 purgeByAuthority(new Predicate<String>() { 229 @Override 230 public boolean test(String authority) { 231 // Purge authority matches 232 return packageAuth.contains(authority); 233 } 234 }); 235 } 236 237 return null; 238 239 } else { 240 return super.call(method, arg, extras); 241 } 242 } 243 244 /** 245 * Purge all internal data whose authority matches the given 246 * {@link Predicate}. 247 */ purgeByAuthority(Predicate<String> predicate)248 private void purgeByAuthority(Predicate<String> predicate) { 249 final SQLiteDatabase db = mHelper.getWritableDatabase(); 250 final DocumentStack stack = new DocumentStack(); 251 252 Cursor cursor = db.query(TABLE_LAST_ACCESSED, null, null, null, null, null, null); 253 try { 254 while (cursor.moveToNext()) { 255 try { 256 final byte[] rawStack = cursor.getBlob( 257 cursor.getColumnIndex(Columns.STACK)); 258 DurableUtils.readFromArray(rawStack, stack); 259 260 if (stack.getRoot() != null && predicate.test(stack.getRoot().authority)) { 261 final String packageName = getCursorString( 262 cursor, Columns.PACKAGE_NAME); 263 db.delete(TABLE_LAST_ACCESSED, Columns.PACKAGE_NAME + "=?", 264 new String[] { packageName }); 265 } 266 } catch (IOException ignored) { 267 } 268 } 269 } finally { 270 FileUtils.closeQuietly(cursor); 271 } 272 } 273 } 274