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