/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.inspector; import static androidx.core.util.Preconditions.checkArgument; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.provider.DocumentsContract; import android.view.View; import android.view.View.OnClickListener; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import com.android.documentsui.DocumentsApplication; import com.android.documentsui.ProviderExecutor; import com.android.documentsui.R; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.Shared; import com.android.documentsui.base.UserId; import com.android.documentsui.inspector.actions.Action; import com.android.documentsui.inspector.actions.ClearDefaultAppAction; import com.android.documentsui.inspector.actions.ShowInProviderAction; import com.android.documentsui.roots.ProvidersAccess; import com.android.documentsui.ui.Snackbars; import java.util.function.Consumer; /** * A controller that coordinates retrieving document information and sending it to the view. */ public final class InspectorController { private final DataSupplier mLoader; private final HeaderDisplay mHeader; private final DetailsDisplay mDetails; private final MediaDisplay mMedia; private final ActionDisplay mShowProvider; private final ActionDisplay mAppDefaults; private final DebugDisplay mDebugView; private final Context mContext; private final PackageManager mPackageManager; private final ProvidersAccess mProviders; private final Runnable mErrorSnackbar; private final String mTitle; private final boolean mShowDebug; /** * InspectorControllerTest relies on this controller. */ @VisibleForTesting public 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) { checkArgument(context != null); checkArgument(loader != null); checkArgument(pm != null); checkArgument(providers != null); checkArgument(header != null); checkArgument(details != null); checkArgument(media != null); checkArgument(showProvider != null); checkArgument(appDefaults != null); checkArgument(debugView != null); checkArgument(errorRunnable != null); mContext = context; mLoader = loader; mPackageManager = pm; mProviders = providers; mHeader = header; mDetails = details; mMedia = media; mShowProvider = showProvider; mAppDefaults = appDefaults; mTitle = title; mShowDebug = showDebug; mDebugView = debugView; mErrorSnackbar = errorRunnable; } /** * @param activity * @param loader * @param layout * @param args Bundle of arguments passed to our host {@link InspectorActivity}. These * can include extras that enable debug mode ({@link Shared#EXTRA_SHOW_DEBUG} * and override the file title (@link {@link Intent#EXTRA_TITLE}). */ public InspectorController(Activity activity, DataSupplier loader, View layout, String title, boolean showDebug) { this(activity, loader, activity.getPackageManager(), DocumentsApplication.getProvidersCache (activity), (HeaderView) layout.findViewById(R.id.inspector_header_view), (DetailsView) layout.findViewById(R.id.inspector_details_view), (MediaView) layout.findViewById(R.id.inspector_media_view), (ActionDisplay) layout.findViewById(R.id.inspector_show_in_provider_view), (ActionDisplay) layout.findViewById(R.id.inspector_app_defaults_view), (DebugView) layout.findViewById(R.id.inspector_debug_view), title, showDebug, () -> { // using a runnable to support unit testing this feature. Snackbars.showInspectorError(activity); } ); if (showDebug) { DebugView view = (DebugView) layout.findViewById(R.id.inspector_debug_view); view.init(ProviderExecutor::forAuthority); } } public void reset() { mLoader.reset(); } public void loadInfo(Uri uri, UserId userId) { mLoader.loadDocInfo(uri, userId, this::updateView); } /** * Updates the view with documentInfo. */ private void updateView(@Nullable DocumentInfo docInfo) { if (docInfo == null) { mErrorSnackbar.run(); } else { mHeader.accept(docInfo); mDetails.accept(docInfo, mTitle != null ? mTitle : docInfo.displayName); if (docInfo.isDirectory()) { mLoader.loadDirCount(docInfo, this::displayChildCount); } else { mShowProvider.setVisible(docInfo.isSettingsSupported()); if (docInfo.isSettingsSupported()) { Action showProviderAction = new ShowInProviderAction(mContext, mPackageManager, docInfo, mProviders); mShowProvider.init( showProviderAction, (view) -> { showInProvider(docInfo.derivedUri, UserId.DEFAULT_USER); }); } Action defaultAction = new ClearDefaultAppAction(mContext, mPackageManager, docInfo); mAppDefaults.setVisible(defaultAction.canPerformAction()); } if (docInfo.isMetadataSupported()) { mLoader.getDocumentMetadata( docInfo.derivedUri, docInfo.userId, (Bundle bundle) -> { onDocumentMetadataLoaded(docInfo, bundle); }); } mMedia.setVisible(!mMedia.isEmpty()); if (mShowDebug) { mDebugView.accept(docInfo); } mDebugView.setVisible(mShowDebug && !mDebugView.isEmpty()); } } private void onDocumentMetadataLoaded(DocumentInfo doc, @Nullable Bundle metadata) { if (metadata == null) { return; } Runnable geoClickListener = null; if (MetadataUtils.hasGeoCoordinates(metadata)) { float[] coords = MetadataUtils.getGeoCoordinates(metadata); final Intent intent = createGeoIntent(coords[0], coords[1], doc.displayName); if (hasHandler(intent)) { geoClickListener = () -> { startActivity(intent); }; } } mMedia.accept(doc, metadata, geoClickListener); if (mShowDebug) { mDebugView.accept(metadata); } } /** * Displays a directory's information to the view. * * @param count - number of items in the directory. */ private void displayChildCount(Integer count) { mDetails.setChildrenCount(count); } private void startActivity(Intent intent) { assert hasHandler(intent); mContext.startActivity(intent); } /** * checks that we can handle a geo-intent. */ private boolean hasHandler(Intent intent) { return mPackageManager.resolveActivity(intent, 0) != null; } /** * Creates a geo-intent for opening a location in maps. * * @see https://developer.android.com/guide/components/intents-common.html#Maps */ private static Intent createGeoIntent( float latitude, float longitude, @Nullable String label) { label = Uri.encode(label == null ? "" : label); String data = "geo:0,0?q=" + latitude + " " + longitude + "(" + label + ")"; Uri uri = Uri.parse(data); return new Intent(Intent.ACTION_VIEW, uri); } /** * Shows the selected document in it's content provider. * * @param DocumentInfo whose flag FLAG_SUPPORTS_SETTINGS is set. */ public void showInProvider(Uri uri, UserId userId) { Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_SETTINGS); intent.setPackage(mProviders.getPackageName(userId, uri.getAuthority())); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setData(uri); userId.startActivityAsUser(mContext, intent); } /** * Interface for loading all the various forms of document data. This primarily * allows us to easily supply test data in tests. */ public interface DataSupplier { /** * Starts the Asynchronous process of loading file data. * * @param uri - A content uri to query metadata from. * @param userId - A user to load the uri from. * @param callback - Function to be called when the loader has finished loading metadata. A * DocumentInfo will be sent to this method. DocumentInfo may be null. */ void loadDocInfo(Uri uri, UserId userId, Consumer callback); /** * Loads a folders item count. * @param directory - a documentInfo thats a directory. * @param callback - Function to be called when the loader has finished loading the number * of children. */ void loadDirCount(DocumentInfo directory, Consumer callback); /** * Deletes all loader id's when android lifecycle ends. */ void reset(); /** * @param uri * @param callback */ void getDocumentMetadata(Uri uri, UserId userId, Consumer callback); } /** * This interface is for unit testing. */ public interface Display { /** * Makes the action visible. */ void setVisible(boolean visible); } /** * This interface is for unit testing. */ public interface ActionDisplay extends Display { /** * Initializes the view based on the action. * @param action - ClearDefaultAppAction or ShowInProviderAction * @param listener - listener for when the action is pressed. */ void init(Action action, OnClickListener listener); void setActionHeader(String header); void setAppIcon(Drawable icon); void setAppName(String name); void showAction(boolean visible); } /** * Provides details about a file. */ public interface HeaderDisplay { void accept(DocumentInfo info); } /** * Provides basic details about a file. */ public interface DetailsDisplay { void accept(DocumentInfo info, String displayName); void setChildrenCount(int count); } /** * Provides details about a media file. */ public interface MediaDisplay extends Display { void accept(DocumentInfo info, Bundle metadata, @Nullable Runnable geoClickListener); /** * Returns true if there are now rows in the display. Does not consider the title. */ boolean isEmpty(); } /** * Provides details about a media file. */ public interface DebugDisplay extends Display { void accept(DocumentInfo info); void accept(Bundle metadata); /** * Returns true if there are now rows in the display. Does not consider the title. */ boolean isEmpty(); } /** * Displays a table of image metadata. */ public interface TableDisplay extends Display { /** * Adds a row in the table. */ void put(@StringRes int keyId, CharSequence value); /** * Adds a row in the table and makes it clickable. */ void put(@StringRes int keyId, CharSequence value, OnClickListener callback); /** * Returns true if there are now rows in the display. Does not consider the title. */ boolean isEmpty(); } }