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