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.app.Activity;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.graphics.drawable.Drawable;
25 import android.net.Uri;
26 import android.os.Bundle;
27 import android.provider.DocumentsContract;
28 import android.view.View;
29 import android.view.View.OnClickListener;
30 
31 import androidx.annotation.Nullable;
32 import androidx.annotation.StringRes;
33 import androidx.annotation.VisibleForTesting;
34 
35 import com.android.documentsui.DocumentsApplication;
36 import com.android.documentsui.ProviderExecutor;
37 import com.android.documentsui.R;
38 import com.android.documentsui.base.DocumentInfo;
39 import com.android.documentsui.base.Shared;
40 import com.android.documentsui.base.UserId;
41 import com.android.documentsui.inspector.actions.Action;
42 import com.android.documentsui.inspector.actions.ClearDefaultAppAction;
43 import com.android.documentsui.inspector.actions.ShowInProviderAction;
44 import com.android.documentsui.roots.ProvidersAccess;
45 import com.android.documentsui.ui.Snackbars;
46 
47 import java.util.function.Consumer;
48 /**
49  * A controller that coordinates retrieving document information and sending it to the view.
50  */
51 public final class InspectorController {
52 
53     private final DataSupplier mLoader;
54     private final HeaderDisplay mHeader;
55     private final DetailsDisplay mDetails;
56     private final MediaDisplay mMedia;
57     private final ActionDisplay mShowProvider;
58     private final ActionDisplay mAppDefaults;
59     private final DebugDisplay mDebugView;
60     private final Context mContext;
61     private final PackageManager mPackageManager;
62     private final ProvidersAccess mProviders;
63     private final Runnable mErrorSnackbar;
64     private final String mTitle;
65     private final boolean mShowDebug;
66 
67     /**
68      * InspectorControllerTest relies on this controller.
69      */
70     @VisibleForTesting
InspectorController( Context context, DataSupplier loader, PackageManager pm, ProvidersAccess providers, HeaderDisplay header, DetailsDisplay details, MediaDisplay media, ActionDisplay showProvider, ActionDisplay appDefaults, DebugDisplay debugView, String title, boolean showDebug, Runnable errorRunnable)71     public InspectorController(
72             Context context,
73             DataSupplier loader,
74             PackageManager pm,
75             ProvidersAccess providers,
76             HeaderDisplay header,
77             DetailsDisplay details,
78             MediaDisplay media,
79             ActionDisplay showProvider,
80             ActionDisplay appDefaults,
81             DebugDisplay debugView,
82             String title,
83             boolean showDebug,
84             Runnable errorRunnable) {
85 
86         checkArgument(context != null);
87         checkArgument(loader != null);
88         checkArgument(pm != null);
89         checkArgument(providers != null);
90         checkArgument(header != null);
91         checkArgument(details != null);
92         checkArgument(media != null);
93         checkArgument(showProvider != null);
94         checkArgument(appDefaults != null);
95         checkArgument(debugView != null);
96         checkArgument(errorRunnable != null);
97 
98         mContext = context;
99         mLoader = loader;
100         mPackageManager = pm;
101         mProviders = providers;
102         mHeader = header;
103         mDetails = details;
104         mMedia = media;
105         mShowProvider = showProvider;
106         mAppDefaults = appDefaults;
107         mTitle = title;
108         mShowDebug = showDebug;
109         mDebugView = debugView;
110 
111         mErrorSnackbar = errorRunnable;
112     }
113 
114     /**
115      * @param activity
116      * @param loader
117      * @param layout
118      * @param args Bundle of arguments passed to our host {@link InspectorActivity}. These
119      *     can include extras that enable debug mode ({@link Shared#EXTRA_SHOW_DEBUG}
120      *     and override the file title (@link {@link Intent#EXTRA_TITLE}).
121      */
InspectorController(Activity activity, DataSupplier loader, View layout, String title, boolean showDebug)122     public InspectorController(Activity activity, DataSupplier loader, View layout,
123             String title, boolean showDebug) {
124         this(activity,
125             loader,
126             activity.getPackageManager(),
127             DocumentsApplication.getProvidersCache (activity),
128             (HeaderView) layout.findViewById(R.id.inspector_header_view),
129             (DetailsView) layout.findViewById(R.id.inspector_details_view),
130             (MediaView) layout.findViewById(R.id.inspector_media_view),
131             (ActionDisplay) layout.findViewById(R.id.inspector_show_in_provider_view),
132             (ActionDisplay) layout.findViewById(R.id.inspector_app_defaults_view),
133             (DebugView) layout.findViewById(R.id.inspector_debug_view),
134             title,
135             showDebug,
136             () -> {
137                 // using a runnable to support unit testing this feature.
138                 Snackbars.showInspectorError(activity);
139             }
140         );
141 
142         if (showDebug) {
143             DebugView view = (DebugView) layout.findViewById(R.id.inspector_debug_view);
144             view.init(ProviderExecutor::forAuthority);
145         }
146     }
147 
reset()148     public void reset() {
149         mLoader.reset();
150     }
151 
loadInfo(Uri uri, UserId userId)152     public void loadInfo(Uri uri, UserId userId) {
153         mLoader.loadDocInfo(uri, userId, this::updateView);
154     }
155 
156     /**
157      * Updates the view with documentInfo.
158      */
updateView(@ullable DocumentInfo docInfo)159     private void updateView(@Nullable DocumentInfo docInfo) {
160         if (docInfo == null) {
161             mErrorSnackbar.run();
162         } else {
163             mHeader.accept(docInfo);
164             mDetails.accept(docInfo, mTitle != null ? mTitle : docInfo.displayName);
165 
166             if (docInfo.isDirectory()) {
167                 mLoader.loadDirCount(docInfo, this::displayChildCount);
168             } else {
169 
170                 mShowProvider.setVisible(docInfo.isSettingsSupported());
171                 if (docInfo.isSettingsSupported()) {
172                     Action showProviderAction =
173                         new ShowInProviderAction(mContext, mPackageManager, docInfo, mProviders);
174                     mShowProvider.init(
175                         showProviderAction,
176                         (view) -> {
177                             showInProvider(docInfo.derivedUri, UserId.DEFAULT_USER);
178                         });
179                 }
180 
181                 Action defaultAction =
182                     new ClearDefaultAppAction(mContext, mPackageManager, docInfo);
183 
184                 mAppDefaults.setVisible(defaultAction.canPerformAction());
185             }
186 
187             if (docInfo.isMetadataSupported()) {
188                 mLoader.getDocumentMetadata(
189                         docInfo.derivedUri,
190                         docInfo.userId,
191                         (Bundle bundle) -> {
192                             onDocumentMetadataLoaded(docInfo, bundle);
193                         });
194             }
195             mMedia.setVisible(!mMedia.isEmpty());
196 
197             if (mShowDebug) {
198                 mDebugView.accept(docInfo);
199             }
200             mDebugView.setVisible(mShowDebug && !mDebugView.isEmpty());
201         }
202     }
203 
onDocumentMetadataLoaded(DocumentInfo doc, @Nullable Bundle metadata)204     private void onDocumentMetadataLoaded(DocumentInfo doc, @Nullable Bundle metadata) {
205         if (metadata == null) {
206             return;
207         }
208 
209         Runnable geoClickListener = null;
210         if (MetadataUtils.hasGeoCoordinates(metadata)) {
211             float[] coords = MetadataUtils.getGeoCoordinates(metadata);
212             final Intent intent = createGeoIntent(coords[0], coords[1], doc.displayName);
213             if (hasHandler(intent)) {
214                 geoClickListener = () -> {
215                     startActivity(intent);
216                 };
217             }
218         }
219 
220         mMedia.accept(doc, metadata, geoClickListener);
221 
222         if (mShowDebug) {
223             mDebugView.accept(metadata);
224         }
225     }
226 
227     /**
228      * Displays a directory's information to the view.
229      *
230      * @param count - number of items in the directory.
231      */
displayChildCount(Integer count)232     private void displayChildCount(Integer count) {
233         mDetails.setChildrenCount(count);
234     }
235 
startActivity(Intent intent)236     private void startActivity(Intent intent) {
237         assert hasHandler(intent);
238         mContext.startActivity(intent);
239     }
240 
241     /**
242      * checks that we can handle a geo-intent.
243      */
hasHandler(Intent intent)244     private boolean hasHandler(Intent intent) {
245         return mPackageManager.resolveActivity(intent, 0) != null;
246     }
247 
248     /**
249      * Creates a geo-intent for opening a location in maps.
250      *
251      * @see https://developer.android.com/guide/components/intents-common.html#Maps
252      */
createGeoIntent( float latitude, float longitude, @Nullable String label)253     private static Intent createGeoIntent(
254             float latitude, float longitude, @Nullable String label) {
255         label = Uri.encode(label == null ? "" : label);
256         String data = "geo:0,0?q=" + latitude + " " + longitude + "(" + label + ")";
257         Uri uri = Uri.parse(data);
258         return new Intent(Intent.ACTION_VIEW, uri);
259     }
260 
261     /**
262      * Shows the selected document in it's content provider.
263      *
264      * @param DocumentInfo whose flag FLAG_SUPPORTS_SETTINGS is set.
265      */
showInProvider(Uri uri, UserId userId)266     public void showInProvider(Uri uri, UserId userId) {
267 
268         Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_SETTINGS);
269         intent.setPackage(mProviders.getPackageName(userId, uri.getAuthority()));
270         intent.addCategory(Intent.CATEGORY_DEFAULT);
271         intent.setData(uri);
272         userId.startActivityAsUser(mContext, intent);
273     }
274 
275     /**
276      * Interface for loading all the various forms of document data. This primarily
277      * allows us to easily supply test data in tests.
278      */
279     public interface DataSupplier {
280 
281         /**
282          * Starts the Asynchronous process of loading file data.
283          *
284          * @param uri - A content uri to query metadata from.
285          * @param userId - A user to load the uri from.
286          * @param callback - Function to be called when the loader has finished loading metadata. A
287          * DocumentInfo will be sent to this method. DocumentInfo may be null.
288          */
loadDocInfo(Uri uri, UserId userId, Consumer<DocumentInfo> callback)289         void loadDocInfo(Uri uri, UserId userId, Consumer<DocumentInfo> callback);
290 
291         /**
292          * Loads a folders item count.
293          * @param directory - a documentInfo thats a directory.
294          * @param callback - Function to be called when the loader has finished loading the number
295          * of children.
296          */
loadDirCount(DocumentInfo directory, Consumer<Integer> callback)297         void loadDirCount(DocumentInfo directory, Consumer<Integer> callback);
298 
299         /**
300          * Deletes all loader id's when android lifecycle ends.
301          */
reset()302         void reset();
303 
304         /**
305          * @param uri
306          * @param callback
307          */
getDocumentMetadata(Uri uri, UserId userId, Consumer<Bundle> callback)308         void getDocumentMetadata(Uri uri, UserId userId, Consumer<Bundle> callback);
309     }
310 
311     /**
312      * This interface is for unit testing.
313      */
314     public interface Display {
315         /**
316          * Makes the action visible.
317          */
setVisible(boolean visible)318         void setVisible(boolean visible);
319     }
320 
321     /**
322      * This interface is for unit testing.
323      */
324     public interface ActionDisplay extends Display {
325 
326         /**
327          * Initializes the view based on the action.
328          * @param action - ClearDefaultAppAction or ShowInProviderAction
329          * @param listener - listener for when the action is pressed.
330          */
init(Action action, OnClickListener listener)331         void init(Action action, OnClickListener listener);
332 
setActionHeader(String header)333         void setActionHeader(String header);
334 
setAppIcon(Drawable icon)335         void setAppIcon(Drawable icon);
336 
setAppName(String name)337         void setAppName(String name);
338 
showAction(boolean visible)339         void showAction(boolean visible);
340     }
341 
342     /**
343      * Provides details about a file.
344      */
345     public interface HeaderDisplay {
accept(DocumentInfo info)346         void accept(DocumentInfo info);
347     }
348 
349     /**
350      * Provides basic details about a file.
351      */
352     public interface DetailsDisplay {
353 
accept(DocumentInfo info, String displayName)354         void accept(DocumentInfo info, String displayName);
355 
setChildrenCount(int count)356         void setChildrenCount(int count);
357     }
358 
359     /**
360      * Provides details about a media file.
361      */
362     public interface MediaDisplay extends Display {
accept(DocumentInfo info, Bundle metadata, @Nullable Runnable geoClickListener)363         void accept(DocumentInfo info, Bundle metadata, @Nullable Runnable geoClickListener);
364 
365         /**
366          * Returns true if there are now rows in the display. Does not consider the title.
367          */
isEmpty()368         boolean isEmpty();
369     }
370 
371     /**
372      * Provides details about a media file.
373      */
374     public interface DebugDisplay extends Display {
accept(DocumentInfo info)375         void accept(DocumentInfo info);
accept(Bundle metadata)376         void accept(Bundle metadata);
377 
378         /**
379          * Returns true if there are now rows in the display. Does not consider the title.
380          */
isEmpty()381         boolean isEmpty();
382     }
383 
384     /**
385      * Displays a table of image metadata.
386      */
387     public interface TableDisplay extends Display {
388 
389         /**
390          * Adds a row in the table.
391          */
put(@tringRes int keyId, CharSequence value)392         void put(@StringRes int keyId, CharSequence value);
393 
394         /**
395          * Adds a row in the table and makes it clickable.
396          */
put(@tringRes int keyId, CharSequence value, OnClickListener callback)397         void put(@StringRes int keyId, CharSequence value, OnClickListener callback);
398 
399         /**
400          * Returns true if there are now rows in the display. Does not consider the title.
401          */
isEmpty()402         boolean isEmpty();
403     }
404 }