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