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.provider.DocumentsContract;
35 import android.util.Log;
36 
37 import com.android.documentsui.base.DocumentStack;
38 import com.android.documentsui.base.DurableUtils;
39 
40 import libcore.io.IoUtils;
41 
42 import com.google.android.collect.Sets;
43 
44 import java.io.IOException;
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         final byte[] rawStack = DurableUtils.writeToArrayOrNull(stack);
127         values.clear();
128         values.put(Columns.STACK, rawStack);
129         values.put(Columns.EXTERNAL, 0);
130         resolver.insert(buildLastAccessed(packageName), values);
131     }
132 
133     @Override
onCreate()134     public boolean onCreate() {
135         mHelper = new DatabaseHelper(getContext());
136         return true;
137     }
138 
139     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)140     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
141             String sortOrder) {
142         if (sMatcher.match(uri) != URI_LAST_ACCESSED) {
143             throw new UnsupportedOperationException("Unsupported Uri " + uri);
144         }
145 
146         final SQLiteDatabase db = mHelper.getReadableDatabase();
147         final String packageName = uri.getPathSegments().get(1);
148         return db.query(TABLE_LAST_ACCESSED, projection, Columns.PACKAGE_NAME + "=?",
149                         new String[] { packageName }, null, null, sortOrder);
150     }
151 
152     @Override
getType(Uri uri)153     public String getType(Uri uri) {
154         return null;
155     }
156 
157     @Override
insert(Uri uri, ContentValues values)158     public Uri insert(Uri uri, ContentValues values) {
159         if (sMatcher.match(uri) != URI_LAST_ACCESSED) {
160             throw new UnsupportedOperationException("Unsupported Uri " + uri);
161         }
162 
163         final SQLiteDatabase db = mHelper.getWritableDatabase();
164         final ContentValues key = new ContentValues();
165 
166         values.put(Columns.TIMESTAMP, System.currentTimeMillis());
167 
168         final String packageName = uri.getPathSegments().get(1);
169         key.put(Columns.PACKAGE_NAME, packageName);
170 
171         // Ensure that row exists, then update with changed values
172         db.insertWithOnConflict(TABLE_LAST_ACCESSED, null, key, SQLiteDatabase.CONFLICT_IGNORE);
173         db.update(TABLE_LAST_ACCESSED, values, Columns.PACKAGE_NAME + "=?",
174                         new String[] { packageName });
175         return uri;
176     }
177 
178     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)179     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
180         throw new UnsupportedOperationException("Unsupported Uri " + uri);
181     }
182 
183     @Override
delete(Uri uri, String selection, String[] selectionArgs)184     public int delete(Uri uri, String selection, String[] selectionArgs) {
185         throw new UnsupportedOperationException("Unsupported Uri " + uri);
186     }
187 
188     @Override
call(String method, String arg, Bundle extras)189     public Bundle call(String method, String arg, Bundle extras) {
190         if (METHOD_PURGE.equals(method)) {
191             // Purge references to unknown authorities
192             final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
193             final Set<String> knownAuth = Sets.newHashSet();
194             for (ResolveInfo info : getContext()
195                     .getPackageManager().queryIntentContentProviders(intent, 0)) {
196                 knownAuth.add(info.providerInfo.authority);
197             }
198 
199             purgeByAuthority(new Predicate<String>() {
200                 @Override
201                 public boolean test(String authority) {
202                     // Purge unknown authorities
203                     return !knownAuth.contains(authority);
204                 }
205             });
206 
207             return null;
208 
209         } else if (METHOD_PURGE_PACKAGE.equals(method)) {
210             // Purge references to authorities in given package
211             final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
212             intent.setPackage(arg);
213             final Set<String> packageAuth = Sets.newHashSet();
214             for (ResolveInfo info : getContext()
215                     .getPackageManager().queryIntentContentProviders(intent, 0)) {
216                 packageAuth.add(info.providerInfo.authority);
217             }
218 
219             if (!packageAuth.isEmpty()) {
220                 purgeByAuthority(new Predicate<String>() {
221                     @Override
222                     public boolean test(String authority) {
223                         // Purge authority matches
224                         return packageAuth.contains(authority);
225                     }
226                 });
227             }
228 
229             return null;
230 
231         } else {
232             return super.call(method, arg, extras);
233         }
234     }
235 
236     /**
237      * Purge all internal data whose authority matches the given
238      * {@link Predicate}.
239      */
purgeByAuthority(Predicate<String> predicate)240     private void purgeByAuthority(Predicate<String> predicate) {
241         final SQLiteDatabase db = mHelper.getWritableDatabase();
242         final DocumentStack stack = new DocumentStack();
243 
244         Cursor cursor = db.query(TABLE_LAST_ACCESSED, null, null, null, null, null, null);
245         try {
246             while (cursor.moveToNext()) {
247                 try {
248                     final byte[] rawStack = cursor.getBlob(
249                             cursor.getColumnIndex(Columns.STACK));
250                     DurableUtils.readFromArray(rawStack, stack);
251 
252                     if (stack.getRoot() != null && predicate.test(stack.getRoot().authority)) {
253                         final String packageName = getCursorString(
254                                 cursor, Columns.PACKAGE_NAME);
255                         db.delete(TABLE_LAST_ACCESSED, Columns.PACKAGE_NAME + "=?",
256                                 new String[] { packageName });
257                     }
258                 } catch (IOException ignored) {
259                 }
260             }
261         } finally {
262             IoUtils.closeQuietly(cursor);
263         }
264     }
265 }
266