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