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