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