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