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