Home | History | Annotate | Download | only in inspector
      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
     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      */
    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 
    142     public void reset() {
    143         mLoader.reset();
    144     }
    145 
    146     public void loadInfo(Uri uri) {
    147         mLoader.loadDocInfo(uri, this::updateView);
    148     }
    149 
    150     /**
    151      * Updates the view with documentInfo.
    152      */
    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 
    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      */
    233     private void displayChildCount(Integer count) {
    234         mDetails.setChildrenCount(count);
    235     }
    236 
    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      */
    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      */
    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      */
    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      */
    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          */
    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          */
    311         void loadDirCount(DocumentInfo directory, Consumer<Integer> callback);
    312 
    313         /**
    314          * Deletes all loader id's when android lifecycle ends.
    315          */
    316         void reset();
    317 
    318         /**
    319          * @param uri
    320          * @param callback
    321          */
    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          */
    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          */
    345         void init(Action action, OnClickListener listener);
    346 
    347         void setActionHeader(String header);
    348 
    349         void setAppIcon(Drawable icon);
    350 
    351         void setAppName(String name);
    352 
    353         void showAction(boolean visible);
    354     }
    355 
    356     /**
    357      * Provides details about a file.
    358      */
    359     public interface HeaderDisplay {
    360         void accept(DocumentInfo info, String displayName);
    361     }
    362 
    363     /**
    364      * Provides basic details about a file.
    365      */
    366     public interface DetailsDisplay {
    367 
    368         void accept(DocumentInfo info);
    369 
    370         void setChildrenCount(int count);
    371     }
    372 
    373     /**
    374      * Provides details about a media file.
    375      */
    376     public interface MediaDisplay extends Display {
    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          */
    382         boolean isEmpty();
    383     }
    384 
    385     /**
    386      * Provides details about a media file.
    387      */
    388     public interface DebugDisplay extends Display {
    389         void accept(DocumentInfo info);
    390         void accept(Bundle metadata);
    391 
    392         /**
    393          * Returns true if there are now rows in the display. Does not consider the title.
    394          */
    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          */
    406         void put(@StringRes int keyId, CharSequence value);
    407 
    408         /**
    409          * Adds a row in the table and makes it clickable.
    410          */
    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          */
    416         boolean isEmpty();
    417     }
    418 }