1 /*
2  * Copyright (C) 2017 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 package com.android.documentsui.inspector;
17 
18 import static androidx.core.util.Preconditions.checkArgument;
19 
20 import android.content.Context;
21 import android.database.ContentObserver;
22 import android.database.Cursor;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.provider.DocumentsContract;
28 
29 import androidx.annotation.Nullable;
30 import androidx.loader.app.LoaderManager;
31 import androidx.loader.app.LoaderManager.LoaderCallbacks;
32 import androidx.loader.content.Loader;
33 
34 import com.android.documentsui.base.DocumentInfo;
35 import com.android.documentsui.base.UserId;
36 import com.android.documentsui.inspector.InspectorController.DataSupplier;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.function.Consumer;
41 
42 /**
43  * Asynchronously loads a document data for the inspector.
44  *
45  * <p>This loader is not a Loader! Its our own funky loader.
46  */
47 public class RuntimeDataSupplier implements DataSupplier {
48 
49     private final Context mContext;
50     private final LoaderManager mLoaderMgr;
51     private final List<Integer> loaderIds = new ArrayList<>();
52     private @Nullable Callbacks mDocCallbacks;
53     private @Nullable Callbacks mDirCallbacks;
54     private @Nullable LoaderCallbacks<Bundle> mMetadataCallbacks;
55 
RuntimeDataSupplier(Context context, LoaderManager loaderMgr)56     public RuntimeDataSupplier(Context context, LoaderManager loaderMgr) {
57         checkArgument(context != null);
58         checkArgument(loaderMgr != null);
59         mContext = context;
60         mLoaderMgr = loaderMgr;
61     }
62 
63     /**
64      * Loads documents metadata.
65      */
66     @Override
loadDocInfo(Uri uri, UserId userId, Consumer<DocumentInfo> updateView)67     public void loadDocInfo(Uri uri, UserId userId, Consumer<DocumentInfo> updateView) {
68         //Check that we have correct Uri type and that the loader is not already created.
69         checkArgument(uri.getScheme().equals("content"));
70 
71         Consumer<Cursor> callback = new Consumer<Cursor>() {
72             @Override
73             public void accept(Cursor cursor) {
74 
75                 if (cursor == null || !cursor.moveToFirst()) {
76                     updateView.accept(null);
77                 } else {
78                     DocumentInfo docInfo = DocumentInfo.fromCursor(cursor, userId,
79                             uri.getAuthority());
80                     updateView.accept(docInfo);
81                 }
82             }
83         };
84 
85         mDocCallbacks = new Callbacks(mContext, uri, userId, callback);
86         mLoaderMgr.restartLoader(getNextLoaderId(), null, mDocCallbacks);
87     }
88 
89     /**
90      * Loads a directories item count.
91      */
92     @Override
loadDirCount(DocumentInfo directory, Consumer<Integer> updateView)93     public void loadDirCount(DocumentInfo directory, Consumer<Integer> updateView) {
94         checkArgument(directory.isDirectory());
95         Uri children = DocumentsContract.buildChildDocumentsUri(
96                 directory.authority, directory.documentId);
97 
98         Consumer<Cursor> callback = new Consumer<Cursor>() {
99             @Override
100             public void accept(Cursor cursor) {
101                 if(cursor != null && cursor.moveToFirst()) {
102                     updateView.accept(cursor.getCount());
103                 }
104             }
105         };
106 
107         mDirCallbacks = new Callbacks(mContext, children, directory.userId, callback);
108         mLoaderMgr.restartLoader(getNextLoaderId(), null, mDirCallbacks);
109     }
110 
111     @Override
getDocumentMetadata(Uri uri, UserId userId, Consumer<Bundle> callback)112     public void getDocumentMetadata(Uri uri, UserId userId, Consumer<Bundle> callback) {
113         mMetadataCallbacks = new LoaderCallbacks<Bundle>() {
114             @Override
115             public Loader<Bundle> onCreateLoader(int id, Bundle unused) {
116                 return new MetadataLoader(mContext, uri, userId.getContentResolver(mContext));
117             }
118 
119             @Override
120             public void onLoadFinished(Loader<Bundle> loader, Bundle data) {
121                 callback.accept(data);
122             }
123 
124             @Override
125             public void onLoaderReset(Loader<Bundle> loader) {
126             }
127         };
128 
129         // TODO: Listen for changes on content URI.
130         mLoaderMgr.restartLoader(getNextLoaderId(), null, mMetadataCallbacks);
131     }
132 
133     @Override
reset()134     public void reset() {
135         for (Integer id : loaderIds) {
136             mLoaderMgr.destroyLoader(id);
137         }
138         loaderIds.clear();
139 
140         if (mDocCallbacks != null && mDocCallbacks.getObserver() != null) {
141             mContext.getContentResolver().unregisterContentObserver(mDocCallbacks.getObserver());
142         }
143 
144         if (mDirCallbacks != null && mDirCallbacks.getObserver() != null) {
145             mContext.getContentResolver().unregisterContentObserver(mDirCallbacks.getObserver());
146         }
147     }
148 
getNextLoaderId()149     private int getNextLoaderId() {
150         int id = 0;
151         while(mLoaderMgr.getLoader(id) != null) {
152             id++;
153             checkArgument(id <= Integer.MAX_VALUE);
154         }
155         loaderIds.add(id);
156         return id;
157     }
158 
159     /**
160      * Implements the callback interface for cursor loader.
161      */
162     static final class Callbacks implements LoaderCallbacks<Cursor> {
163 
164         private final Context mContext;
165         private final Uri mUri;
166         private final UserId mUserId;
167         private final Consumer<Cursor> mCallback;
168         private ContentObserver mObserver;
169 
Callbacks(Context context, Uri uri, UserId userId, Consumer<Cursor> callback)170         Callbacks(Context context, Uri uri, UserId userId, Consumer<Cursor> callback) {
171             checkArgument(context != null);
172             checkArgument(uri != null);
173             checkArgument(userId != null);
174             checkArgument(callback != null);
175             mContext = context;
176             mUri = uri;
177             mUserId = userId;
178             mCallback = callback;
179         }
180 
181         @Override
onCreateLoader(int id, Bundle args)182         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
183             return UserId.createCursorLoader(mContext, mUri, mUserId);
184         }
185 
186         @Override
onLoadFinished(Loader<Cursor> loader, Cursor cursor)187         public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
188 
189             if (cursor != null) {
190                 mObserver = new InspectorContentObserver(loader::onContentChanged);
191                 cursor.registerContentObserver(mObserver);
192             }
193 
194             mCallback.accept(cursor);
195         }
196 
197         @Override
onLoaderReset(Loader<Cursor> loader)198         public void onLoaderReset(Loader<Cursor> loader) {
199             if (mObserver != null) {
200                 mUserId.getContentResolver(mContext).unregisterContentObserver(mObserver);
201             }
202         }
203 
getObserver()204         public ContentObserver getObserver() {
205             return mObserver;
206         }
207     }
208 
209     private static final class InspectorContentObserver extends ContentObserver {
210         private final Runnable mContentChangedCallback;
211 
InspectorContentObserver(Runnable contentChangedCallback)212         public InspectorContentObserver(Runnable contentChangedCallback) {
213             super(new Handler(Looper.getMainLooper()));
214             mContentChangedCallback = contentChangedCallback;
215         }
216 
217         @Override
onChange(boolean selfChange)218         public void onChange(boolean selfChange) {
219             mContentChangedCallback.run();
220         }
221     }
222 }
223