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;
18 
19 import static com.android.documentsui.DocumentsActivity.TAG;
20 import static com.android.documentsui.BaseActivity.State.MODE_UNKNOWN;
21 import static com.android.documentsui.BaseActivity.State.SORT_ORDER_DISPLAY_NAME;
22 import static com.android.documentsui.BaseActivity.State.SORT_ORDER_LAST_MODIFIED;
23 import static com.android.documentsui.BaseActivity.State.SORT_ORDER_SIZE;
24 import static com.android.documentsui.BaseActivity.State.SORT_ORDER_UNKNOWN;
25 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
26 
27 import android.content.AsyncTaskLoader;
28 import android.content.ContentProviderClient;
29 import android.content.ContentResolver;
30 import android.content.Context;
31 import android.database.Cursor;
32 import android.net.Uri;
33 import android.os.CancellationSignal;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.OperationCanceledException;
37 import android.os.RemoteException;
38 import android.provider.DocumentsContract;
39 import android.provider.DocumentsContract.Document;
40 import android.util.Log;
41 
42 import com.android.documentsui.BaseActivity.State;
43 import com.android.documentsui.RecentsProvider.StateColumns;
44 import com.android.documentsui.model.DocumentInfo;
45 import com.android.documentsui.model.RootInfo;
46 
47 import libcore.io.IoUtils;
48 
49 import java.io.FileNotFoundException;
50 
51 class DirectoryResult implements AutoCloseable {
52     ContentProviderClient client;
53     Cursor cursor;
54     Exception exception;
55 
56     int mode = MODE_UNKNOWN;
57     int sortOrder = SORT_ORDER_UNKNOWN;
58 
59     @Override
close()60     public void close() {
61         IoUtils.closeQuietly(cursor);
62         ContentProviderClient.releaseQuietly(client);
63         cursor = null;
64         client = null;
65     }
66 }
67 
68 public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
69 
70     private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
71 
72     private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
73 
74     private final int mType;
75     private final RootInfo mRoot;
76     private DocumentInfo mDoc;
77     private final Uri mUri;
78     private final int mUserSortOrder;
79 
80     private CancellationSignal mSignal;
81     private DirectoryResult mResult;
82 
DirectoryLoader(Context context, int type, RootInfo root, DocumentInfo doc, Uri uri, int userSortOrder)83     public DirectoryLoader(Context context, int type, RootInfo root, DocumentInfo doc, Uri uri,
84             int userSortOrder) {
85         super(context, ProviderExecutor.forAuthority(root.authority));
86         mType = type;
87         mRoot = root;
88         mDoc = doc;
89         mUri = uri;
90         mUserSortOrder = userSortOrder;
91     }
92 
93     @Override
loadInBackground()94     public final DirectoryResult loadInBackground() {
95         synchronized (this) {
96             if (isLoadInBackgroundCanceled()) {
97                 throw new OperationCanceledException();
98             }
99             mSignal = new CancellationSignal();
100         }
101 
102         final ContentResolver resolver = getContext().getContentResolver();
103         final String authority = mUri.getAuthority();
104 
105         final DirectoryResult result = new DirectoryResult();
106 
107         int userMode = State.MODE_UNKNOWN;
108 
109         // Use default document when searching
110         if (mType == DirectoryFragment.TYPE_SEARCH) {
111             final Uri docUri = DocumentsContract.buildDocumentUri(
112                     mRoot.authority, mRoot.documentId);
113             try {
114                 mDoc = DocumentInfo.fromUri(resolver, docUri);
115             } catch (FileNotFoundException e) {
116                 Log.w(TAG, "Failed to query", e);
117                 result.exception = e;
118                 return result;
119             }
120         }
121 
122         // Pick up any custom modes requested by user
123         Cursor cursor = null;
124         try {
125             final Uri stateUri = RecentsProvider.buildState(
126                     mRoot.authority, mRoot.rootId, mDoc.documentId);
127             cursor = resolver.query(stateUri, null, null, null, null);
128             if (cursor.moveToFirst()) {
129                 userMode = getCursorInt(cursor, StateColumns.MODE);
130             }
131         } finally {
132             IoUtils.closeQuietly(cursor);
133         }
134 
135         if (userMode != State.MODE_UNKNOWN) {
136             result.mode = userMode;
137         } else {
138             if ((mDoc.flags & Document.FLAG_DIR_PREFERS_GRID) != 0) {
139                 result.mode = State.MODE_GRID;
140             } else {
141                 result.mode = State.MODE_LIST;
142             }
143         }
144 
145         if (mUserSortOrder != State.SORT_ORDER_UNKNOWN) {
146             result.sortOrder = mUserSortOrder;
147         } else {
148             if ((mDoc.flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0) {
149                 result.sortOrder = State.SORT_ORDER_LAST_MODIFIED;
150             } else {
151                 result.sortOrder = State.SORT_ORDER_DISPLAY_NAME;
152             }
153         }
154 
155         // Search always uses ranking from provider
156         if (mType == DirectoryFragment.TYPE_SEARCH) {
157             result.sortOrder = State.SORT_ORDER_UNKNOWN;
158         }
159 
160         Log.d(TAG, "userMode=" + userMode + ", userSortOrder=" + mUserSortOrder + " --> mode="
161                 + result.mode + ", sortOrder=" + result.sortOrder);
162 
163         ContentProviderClient client = null;
164         try {
165             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
166 
167             cursor = client.query(
168                     mUri, null, null, null, getQuerySortOrder(result.sortOrder), mSignal);
169             if (cursor == null) {
170                 throw new RemoteException("Provider returned null");
171             }
172 
173             cursor.registerContentObserver(mObserver);
174 
175             cursor = new RootCursorWrapper(mUri.getAuthority(), mRoot.rootId, cursor, -1);
176 
177             if (mType == DirectoryFragment.TYPE_SEARCH) {
178                 // Filter directories out of search results, for now
179                 cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES);
180             } else {
181                 // Normal directories should have sorting applied
182                 cursor = new SortingCursorWrapper(cursor, result.sortOrder);
183             }
184 
185             result.client = client;
186             result.cursor = cursor;
187         } catch (Exception e) {
188             Log.w(TAG, "Failed to query", e);
189             result.exception = e;
190             ContentProviderClient.releaseQuietly(client);
191         } finally {
192             synchronized (this) {
193                 mSignal = null;
194             }
195         }
196 
197         return result;
198     }
199 
200     @Override
cancelLoadInBackground()201     public void cancelLoadInBackground() {
202         super.cancelLoadInBackground();
203 
204         synchronized (this) {
205             if (mSignal != null) {
206                 mSignal.cancel();
207             }
208         }
209     }
210 
211     @Override
deliverResult(DirectoryResult result)212     public void deliverResult(DirectoryResult result) {
213         if (isReset()) {
214             IoUtils.closeQuietly(result);
215             return;
216         }
217         DirectoryResult oldResult = mResult;
218         mResult = result;
219 
220         if (isStarted()) {
221             super.deliverResult(result);
222         }
223 
224         if (oldResult != null && oldResult != result) {
225             IoUtils.closeQuietly(oldResult);
226         }
227     }
228 
229     @Override
onStartLoading()230     protected void onStartLoading() {
231         if (mResult != null) {
232             deliverResult(mResult);
233         }
234         if (takeContentChanged() || mResult == null) {
235             forceLoad();
236         }
237     }
238 
239     @Override
onStopLoading()240     protected void onStopLoading() {
241         cancelLoad();
242     }
243 
244     @Override
onCanceled(DirectoryResult result)245     public void onCanceled(DirectoryResult result) {
246         IoUtils.closeQuietly(result);
247     }
248 
249     @Override
onReset()250     protected void onReset() {
251         super.onReset();
252 
253         // Ensure the loader is stopped
254         onStopLoading();
255 
256         IoUtils.closeQuietly(mResult);
257         mResult = null;
258 
259         getContext().getContentResolver().unregisterContentObserver(mObserver);
260     }
261 
getQuerySortOrder(int sortOrder)262     public static String getQuerySortOrder(int sortOrder) {
263         switch (sortOrder) {
264             case SORT_ORDER_DISPLAY_NAME:
265                 return Document.COLUMN_DISPLAY_NAME + " ASC";
266             case SORT_ORDER_LAST_MODIFIED:
267                 return Document.COLUMN_LAST_MODIFIED + " DESC";
268             case SORT_ORDER_SIZE:
269                 return Document.COLUMN_SIZE + " DESC";
270             default:
271                 return null;
272         }
273     }
274 }
275