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
17package com.android.documentsui;
18
19import static com.android.documentsui.model.DocumentInfo.getCursorString;
20
21import android.content.ContentProvider;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.UriMatcher;
27import android.content.pm.ResolveInfo;
28import android.database.Cursor;
29import android.database.sqlite.SQLiteDatabase;
30import android.database.sqlite.SQLiteOpenHelper;
31import android.net.Uri;
32import android.os.Bundle;
33import android.provider.DocumentsContract;
34import android.provider.DocumentsContract.Document;
35import android.provider.DocumentsContract.Root;
36import android.text.format.DateUtils;
37import android.util.Log;
38
39import com.android.documentsui.model.DocumentStack;
40import com.android.documentsui.model.DurableUtils;
41import com.android.internal.util.Predicate;
42import com.google.android.collect.Sets;
43
44import libcore.io.IoUtils;
45
46import java.io.IOException;
47import java.util.Set;
48
49public class RecentsProvider extends ContentProvider {
50    private static final String TAG = "RecentsProvider";
51
52    private static final long MAX_HISTORY_IN_MILLIS = 45 * DateUtils.DAY_IN_MILLIS;
53
54    private static final String AUTHORITY = "com.android.documentsui.recents";
55
56    private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
57
58    private static final int URI_RECENT = 1;
59    private static final int URI_STATE = 2;
60    private static final int URI_RESUME = 3;
61
62    public static final String METHOD_PURGE = "purge";
63    public static final String METHOD_PURGE_PACKAGE = "purgePackage";
64
65    static {
66        sMatcher.addURI(AUTHORITY, "recent", URI_RECENT);
67        // state/authority/rootId/docId
68        sMatcher.addURI(AUTHORITY, "state/*/*/*", URI_STATE);
69        // resume/packageName
70        sMatcher.addURI(AUTHORITY, "resume/*", URI_RESUME);
71    }
72
73    public static final String TABLE_RECENT = "recent";
74    public static final String TABLE_STATE = "state";
75    public static final String TABLE_RESUME = "resume";
76
77    public static class RecentColumns {
78        public static final String KEY = "key";
79        public static final String STACK = "stack";
80        public static final String TIMESTAMP = "timestamp";
81    }
82
83    public static class StateColumns {
84        public static final String AUTHORITY = "authority";
85        public static final String ROOT_ID = Root.COLUMN_ROOT_ID;
86        public static final String DOCUMENT_ID = Document.COLUMN_DOCUMENT_ID;
87        public static final String MODE = "mode";
88        public static final String SORT_ORDER = "sortOrder";
89    }
90
91    public static class ResumeColumns {
92        public static final String PACKAGE_NAME = "package_name";
93        public static final String STACK = "stack";
94        public static final String TIMESTAMP = "timestamp";
95        public static final String EXTERNAL = "external";
96    }
97
98    public static Uri buildRecent() {
99        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
100                .authority(AUTHORITY).appendPath("recent").build();
101    }
102
103    public static Uri buildState(String authority, String rootId, String documentId) {
104        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY)
105                .appendPath("state").appendPath(authority).appendPath(rootId).appendPath(documentId)
106                .build();
107    }
108
109    public static Uri buildResume(String packageName) {
110        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
111                .authority(AUTHORITY).appendPath("resume").appendPath(packageName).build();
112    }
113
114    private DatabaseHelper mHelper;
115
116    private static class DatabaseHelper extends SQLiteOpenHelper {
117        private static final String DB_NAME = "recents.db";
118
119        private static final int VERSION_INIT = 1;
120        private static final int VERSION_AS_BLOB = 3;
121        private static final int VERSION_ADD_EXTERNAL = 4;
122        private static final int VERSION_ADD_RECENT_KEY = 5;
123
124        public DatabaseHelper(Context context) {
125            super(context, DB_NAME, null, VERSION_ADD_RECENT_KEY);
126        }
127
128        @Override
129        public void onCreate(SQLiteDatabase db) {
130
131            db.execSQL("CREATE TABLE " + TABLE_RECENT + " (" +
132                    RecentColumns.KEY + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
133                    RecentColumns.STACK + " BLOB DEFAULT NULL," +
134                    RecentColumns.TIMESTAMP + " INTEGER" +
135                    ")");
136
137            db.execSQL("CREATE TABLE " + TABLE_STATE + " (" +
138                    StateColumns.AUTHORITY + " TEXT," +
139                    StateColumns.ROOT_ID + " TEXT," +
140                    StateColumns.DOCUMENT_ID + " TEXT," +
141                    StateColumns.MODE + " INTEGER," +
142                    StateColumns.SORT_ORDER + " INTEGER," +
143                    "PRIMARY KEY (" + StateColumns.AUTHORITY + ", " + StateColumns.ROOT_ID + ", "
144                    + StateColumns.DOCUMENT_ID + ")" +
145                    ")");
146
147            db.execSQL("CREATE TABLE " + TABLE_RESUME + " (" +
148                    ResumeColumns.PACKAGE_NAME + " TEXT NOT NULL PRIMARY KEY," +
149                    ResumeColumns.STACK + " BLOB DEFAULT NULL," +
150                    ResumeColumns.TIMESTAMP + " INTEGER," +
151                    ResumeColumns.EXTERNAL + " INTEGER NOT NULL DEFAULT 0" +
152                    ")");
153        }
154
155        @Override
156        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
157            Log.w(TAG, "Upgrading database; wiping app data");
158            db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT);
159            db.execSQL("DROP TABLE IF EXISTS " + TABLE_STATE);
160            db.execSQL("DROP TABLE IF EXISTS " + TABLE_RESUME);
161            onCreate(db);
162        }
163    }
164
165    @Override
166    public boolean onCreate() {
167        mHelper = new DatabaseHelper(getContext());
168        return true;
169    }
170
171    @Override
172    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
173            String sortOrder) {
174        final SQLiteDatabase db = mHelper.getReadableDatabase();
175        switch (sMatcher.match(uri)) {
176            case URI_RECENT:
177                final long cutoff = System.currentTimeMillis() - MAX_HISTORY_IN_MILLIS;
178                return db.query(TABLE_RECENT, projection, RecentColumns.TIMESTAMP + ">" + cutoff,
179                        null, null, null, sortOrder);
180            case URI_STATE:
181                final String authority = uri.getPathSegments().get(1);
182                final String rootId = uri.getPathSegments().get(2);
183                final String documentId = uri.getPathSegments().get(3);
184                return db.query(TABLE_STATE, projection, StateColumns.AUTHORITY + "=? AND "
185                        + StateColumns.ROOT_ID + "=? AND " + StateColumns.DOCUMENT_ID + "=?",
186                        new String[] { authority, rootId, documentId }, null, null, sortOrder);
187            case URI_RESUME:
188                final String packageName = uri.getPathSegments().get(1);
189                return db.query(TABLE_RESUME, projection, ResumeColumns.PACKAGE_NAME + "=?",
190                        new String[] { packageName }, null, null, sortOrder);
191            default:
192                throw new UnsupportedOperationException("Unsupported Uri " + uri);
193        }
194    }
195
196    @Override
197    public String getType(Uri uri) {
198        return null;
199    }
200
201    @Override
202    public Uri insert(Uri uri, ContentValues values) {
203        final SQLiteDatabase db = mHelper.getWritableDatabase();
204        final ContentValues key = new ContentValues();
205        switch (sMatcher.match(uri)) {
206            case URI_RECENT:
207                values.put(RecentColumns.TIMESTAMP, System.currentTimeMillis());
208                db.insert(TABLE_RECENT, null, values);
209                final long cutoff = System.currentTimeMillis() - MAX_HISTORY_IN_MILLIS;
210                db.delete(TABLE_RECENT, RecentColumns.TIMESTAMP + "<" + cutoff, null);
211                return uri;
212            case URI_STATE:
213                final String authority = uri.getPathSegments().get(1);
214                final String rootId = uri.getPathSegments().get(2);
215                final String documentId = uri.getPathSegments().get(3);
216
217                key.put(StateColumns.AUTHORITY, authority);
218                key.put(StateColumns.ROOT_ID, rootId);
219                key.put(StateColumns.DOCUMENT_ID, documentId);
220
221                // Ensure that row exists, then update with changed values
222                db.insertWithOnConflict(TABLE_STATE, null, key, SQLiteDatabase.CONFLICT_IGNORE);
223                db.update(TABLE_STATE, values, StateColumns.AUTHORITY + "=? AND "
224                        + StateColumns.ROOT_ID + "=? AND " + StateColumns.DOCUMENT_ID + "=?",
225                        new String[] { authority, rootId, documentId });
226
227                return uri;
228            case URI_RESUME:
229                values.put(ResumeColumns.TIMESTAMP, System.currentTimeMillis());
230
231                final String packageName = uri.getPathSegments().get(1);
232                key.put(ResumeColumns.PACKAGE_NAME, packageName);
233
234                // Ensure that row exists, then update with changed values
235                db.insertWithOnConflict(TABLE_RESUME, null, key, SQLiteDatabase.CONFLICT_IGNORE);
236                db.update(TABLE_RESUME, values, ResumeColumns.PACKAGE_NAME + "=?",
237                        new String[] { packageName });
238                return uri;
239            default:
240                throw new UnsupportedOperationException("Unsupported Uri " + uri);
241        }
242    }
243
244    @Override
245    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
246        throw new UnsupportedOperationException("Unsupported Uri " + uri);
247    }
248
249    @Override
250    public int delete(Uri uri, String selection, String[] selectionArgs) {
251        throw new UnsupportedOperationException("Unsupported Uri " + uri);
252    }
253
254    @Override
255    public Bundle call(String method, String arg, Bundle extras) {
256        if (METHOD_PURGE.equals(method)) {
257            // Purge references to unknown authorities
258            final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
259            final Set<String> knownAuth = Sets.newHashSet();
260            for (ResolveInfo info : getContext()
261                    .getPackageManager().queryIntentContentProviders(intent, 0)) {
262                knownAuth.add(info.providerInfo.authority);
263            }
264
265            purgeByAuthority(new Predicate<String>() {
266                @Override
267                public boolean apply(String authority) {
268                    // Purge unknown authorities
269                    return !knownAuth.contains(authority);
270                }
271            });
272
273            return null;
274
275        } else if (METHOD_PURGE_PACKAGE.equals(method)) {
276            // Purge references to authorities in given package
277            final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
278            intent.setPackage(arg);
279            final Set<String> packageAuth = Sets.newHashSet();
280            for (ResolveInfo info : getContext()
281                    .getPackageManager().queryIntentContentProviders(intent, 0)) {
282                packageAuth.add(info.providerInfo.authority);
283            }
284
285            if (!packageAuth.isEmpty()) {
286                purgeByAuthority(new Predicate<String>() {
287                    @Override
288                    public boolean apply(String authority) {
289                        // Purge authority matches
290                        return packageAuth.contains(authority);
291                    }
292                });
293            }
294
295            return null;
296
297        } else {
298            return super.call(method, arg, extras);
299        }
300    }
301
302    /**
303     * Purge all internal data whose authority matches the given
304     * {@link Predicate}.
305     */
306    private void purgeByAuthority(Predicate<String> predicate) {
307        final SQLiteDatabase db = mHelper.getWritableDatabase();
308        final DocumentStack stack = new DocumentStack();
309
310        Cursor cursor = db.query(TABLE_RECENT, null, null, null, null, null, null);
311        try {
312            while (cursor.moveToNext()) {
313                try {
314                    final byte[] rawStack = cursor.getBlob(
315                            cursor.getColumnIndex(RecentColumns.STACK));
316                    DurableUtils.readFromArray(rawStack, stack);
317
318                    if (stack.root != null && predicate.apply(stack.root.authority)) {
319                        final String key = getCursorString(cursor, RecentColumns.KEY);
320                        db.delete(TABLE_RECENT, RecentColumns.KEY + "=?", new String[] { key });
321                    }
322                } catch (IOException ignored) {
323                }
324            }
325        } finally {
326            IoUtils.closeQuietly(cursor);
327        }
328
329        cursor = db.query(TABLE_STATE, new String[] {
330                StateColumns.AUTHORITY }, null, null, StateColumns.AUTHORITY, null, null);
331        try {
332            while (cursor.moveToNext()) {
333                final String authority = getCursorString(cursor, StateColumns.AUTHORITY);
334                if (predicate.apply(authority)) {
335                    db.delete(TABLE_STATE, StateColumns.AUTHORITY + "=?", new String[] {
336                            authority });
337                    Log.d(TAG, "Purged state for " + authority);
338                }
339            }
340        } finally {
341            IoUtils.closeQuietly(cursor);
342        }
343
344        cursor = db.query(TABLE_RESUME, null, null, null, null, null, null);
345        try {
346            while (cursor.moveToNext()) {
347                try {
348                    final byte[] rawStack = cursor.getBlob(
349                            cursor.getColumnIndex(ResumeColumns.STACK));
350                    DurableUtils.readFromArray(rawStack, stack);
351
352                    if (stack.root != null && predicate.apply(stack.root.authority)) {
353                        final String packageName = getCursorString(
354                                cursor, ResumeColumns.PACKAGE_NAME);
355                        db.delete(TABLE_RESUME, ResumeColumns.PACKAGE_NAME + "=?",
356                                new String[] { packageName });
357                    }
358                } catch (IOException ignored) {
359                }
360            }
361        } finally {
362            IoUtils.closeQuietly(cursor);
363        }
364    }
365}
366