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 }