blob: 291936adda02a3f391c6d88ed7c7026f7f7db51b [file] [log] [blame]
/*
* 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 androidx.annotation.StringRes;
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 androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.view.View;
import android.view.View.OnClickListener;
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.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) {
mLoader.loadDocInfo(uri, 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);
});
}
Action defaultAction =
new ClearDefaultAppAction(mContext, mPackageManager, docInfo);
mAppDefaults.setVisible(defaultAction.canPerformAction());
}
if (docInfo.isMetadataSupported()) {
mLoader.getDocumentMetadata(
docInfo.derivedUri,
(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) {
Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_SETTINGS);
intent.setPackage(mProviders.getPackageName(uri.getAuthority()));
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setData(uri);
mContext.startActivity(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 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, Consumer<DocumentInfo> 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<Integer> callback);
/**
* Deletes all loader id's when android lifecycle ends.
*/
void reset();
/**
* @param uri
* @param callback
*/
void getDocumentMetadata(Uri uri, Consumer<Bundle> 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();
}
}