1 /* 2 * Copyright (C) 2015 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 17 package com.android.documentsui.base; 18 19 import static com.android.documentsui.base.SharedMinimal.TAG; 20 21 import android.app.Activity; 22 import android.app.compat.CompatChanges; 23 import android.compat.annotation.ChangeId; 24 import android.compat.annotation.EnabledAfter; 25 import android.content.ComponentName; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageManager; 31 import android.content.pm.PackageManager.NameNotFoundException; 32 import android.content.res.Configuration; 33 import android.net.Uri; 34 import android.os.Looper; 35 import android.os.Process; 36 import android.provider.DocumentsContract; 37 import android.provider.Settings; 38 import android.text.TextUtils; 39 import android.text.format.DateUtils; 40 import android.util.Log; 41 import android.view.View; 42 import android.view.WindowManager; 43 44 import androidx.annotation.NonNull; 45 import androidx.annotation.PluralsRes; 46 import androidx.appcompat.app.AlertDialog; 47 48 import com.android.documentsui.R; 49 import com.android.documentsui.ui.MessageBuilder; 50 import com.android.documentsui.util.VersionUtils; 51 52 import java.text.Collator; 53 import java.time.Instant; 54 import java.time.LocalDateTime; 55 import java.time.ZoneId; 56 import java.util.ArrayList; 57 import java.util.List; 58 59 import javax.annotation.Nullable; 60 61 /** @hide */ 62 public final class Shared { 63 64 /** Intent action name to pick a copy destination. */ 65 public static final String ACTION_PICK_COPY_DESTINATION = 66 "com.android.documentsui.PICK_COPY_DESTINATION"; 67 68 // These values track values declared in MediaDocumentsProvider. 69 public static final String METADATA_KEY_AUDIO = "android.media.metadata.audio"; 70 public static final String METADATA_KEY_VIDEO = "android.media.metadata.video"; 71 public static final String METADATA_VIDEO_LATITUDE = "android.media.metadata.video:latitude"; 72 public static final String METADATA_VIDEO_LONGITUTE = "android.media.metadata.video:longitude"; 73 74 /** 75 * Extra flag used to store the current stack so user opens in right spot. 76 */ 77 public static final String EXTRA_STACK = "com.android.documentsui.STACK"; 78 79 /** 80 * Extra flag used to store query of type String in the bundle. 81 */ 82 public static final String EXTRA_QUERY = "query"; 83 84 /** 85 * Extra flag used to store chip's title of type String array in the bundle. 86 */ 87 public static final String EXTRA_QUERY_CHIPS = "query_chips"; 88 89 /** 90 * Extra flag used to store state of type State in the bundle. 91 */ 92 public static final String EXTRA_STATE = "state"; 93 94 /** 95 * Extra flag used to store root of type RootInfo in the bundle. 96 */ 97 public static final String EXTRA_ROOT = "root"; 98 99 /** 100 * Extra flag used to store document of DocumentInfo type in the bundle. 101 */ 102 public static final String EXTRA_DOC = "document"; 103 104 /** 105 * Extra flag used to store DirectoryFragment's selection of Selection type in the bundle. 106 */ 107 public static final String EXTRA_SELECTION = "selection"; 108 109 /** 110 * Extra flag used to store DirectoryFragment's ignore state of boolean type in the bundle. 111 */ 112 public static final String EXTRA_IGNORE_STATE = "ignoreState"; 113 114 /** 115 * Extra flag used to store pick result state of PickResult type in the bundle. 116 */ 117 public static final String EXTRA_PICK_RESULT = "pickResult"; 118 119 /** 120 * Extra for an Intent for enabling performance benchmark. Used only by tests. 121 */ 122 public static final String EXTRA_BENCHMARK = "com.android.documentsui.benchmark"; 123 124 /** 125 * Extra flag used to signify to inspector that debug section can be shown. 126 */ 127 public static final String EXTRA_SHOW_DEBUG = "com.android.documentsui.SHOW_DEBUG"; 128 129 /** 130 * Maximum number of items in a Binder transaction packet. 131 */ 132 public static final int MAX_DOCS_IN_INTENT = 500; 133 134 /** 135 * Animation duration of checkbox in directory list/grid in millis. 136 */ 137 public static final int CHECK_ANIMATION_DURATION = 100; 138 139 /** 140 * Class name of launcher icon avtivity. 141 */ 142 public static final String LAUNCHER_TARGET_CLASS = "com.android.documentsui.LauncherActivity"; 143 144 private static final Collator sCollator; 145 146 /** 147 * We support restrict Storage Access Framework from {@link android.os.Build.VERSION_CODES#R}. 148 * App Compatibility flag that indicates whether the app should be restricted or not. 149 * This flag is turned on by default for all apps targeting > 150 * {@link android.os.Build.VERSION_CODES#Q}. 151 */ 152 @ChangeId 153 @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.Q) 154 private static final long RESTRICT_STORAGE_ACCESS_FRAMEWORK = 141600225L; 155 156 static { 157 sCollator = Collator.getInstance(); 158 sCollator.setStrength(Collator.SECONDARY); 159 } 160 161 /** 162 * @deprecated use {@link MessageBuilder#getQuantityString} 163 */ 164 @Deprecated getQuantityString(Context context, @PluralsRes int resourceId, int quantity)165 public static String getQuantityString(Context context, @PluralsRes int resourceId, 166 int quantity) { 167 return context.getResources().getQuantityString(resourceId, quantity, quantity); 168 } 169 170 /** 171 * Whether the calling app should be restricted in Storage Access Framework or not. 172 */ shouldRestrictStorageAccessFramework(Activity activity)173 public static boolean shouldRestrictStorageAccessFramework(Activity activity) { 174 if (VersionUtils.isAtLeastS()) { 175 return true; 176 } 177 178 if (!VersionUtils.isAtLeastR()) { 179 return false; 180 } 181 182 final String packageName = getCallingPackageName(activity); 183 final boolean ret = CompatChanges.isChangeEnabled(RESTRICT_STORAGE_ACCESS_FRAMEWORK, 184 packageName, Process.myUserHandle()); 185 186 Log.d(TAG, 187 "shouldRestrictStorageAccessFramework = " + ret + ", packageName = " + packageName); 188 189 return ret; 190 } 191 formatTime(Context context, long when)192 public static String formatTime(Context context, long when) { 193 // TODO: DateUtils should make this easier 194 ZoneId zoneId = ZoneId.systemDefault(); 195 LocalDateTime then = LocalDateTime.ofInstant(Instant.ofEpochMilli(when), zoneId); 196 LocalDateTime now = LocalDateTime.ofInstant(Instant.now(), zoneId); 197 198 int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT 199 | DateUtils.FORMAT_ABBREV_ALL; 200 201 if (then.getYear() != now.getYear()) { 202 flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; 203 } else if (then.getDayOfYear() != now.getDayOfYear()) { 204 flags |= DateUtils.FORMAT_SHOW_DATE; 205 } else { 206 flags |= DateUtils.FORMAT_SHOW_TIME; 207 } 208 209 return DateUtils.formatDateTime(context, when, flags); 210 } 211 212 /** 213 * A convenient way to transform any list into a (parcelable) ArrayList. 214 * Uses cast if possible, else creates a new list with entries from {@code list}. 215 */ asArrayList(List<T> list)216 public static <T> ArrayList<T> asArrayList(List<T> list) { 217 return list instanceof ArrayList 218 ? (ArrayList<T>) list 219 : new ArrayList<>(list); 220 } 221 222 /** 223 * Compare two strings against each other using system default collator in a 224 * case-insensitive mode. Clusters strings prefixed with {@link DIR_PREFIX} 225 * before other items. 226 */ compareToIgnoreCaseNullable(String lhs, String rhs)227 public static int compareToIgnoreCaseNullable(String lhs, String rhs) { 228 final boolean leftEmpty = TextUtils.isEmpty(lhs); 229 final boolean rightEmpty = TextUtils.isEmpty(rhs); 230 231 if (leftEmpty && rightEmpty) return 0; 232 if (leftEmpty) return -1; 233 if (rightEmpty) return 1; 234 235 return sCollator.compare(lhs, rhs); 236 } 237 isSystemApp(ApplicationInfo ai)238 private static boolean isSystemApp(ApplicationInfo ai) { 239 return (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 240 } 241 isUpdatedSystemApp(ApplicationInfo ai)242 private static boolean isUpdatedSystemApp(ApplicationInfo ai) { 243 return (ai.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; 244 } 245 246 /** 247 * Returns the calling package, possibly overridden by EXTRA_PACKAGE_NAME. 248 * @param activity 249 * @return 250 */ getCallingPackageName(Activity activity)251 public static String getCallingPackageName(Activity activity) { 252 String callingPackage = activity.getCallingPackage(); 253 // System apps can set the calling package name using an extra. 254 try { 255 ApplicationInfo info = 256 activity.getPackageManager().getApplicationInfo(callingPackage, 0); 257 if (isSystemApp(info) || isUpdatedSystemApp(info)) { 258 final String extra = activity.getIntent().getStringExtra( 259 Intent.EXTRA_PACKAGE_NAME); 260 if (extra != null && !TextUtils.isEmpty(extra)) { 261 callingPackage = extra; 262 } 263 } 264 } catch (NameNotFoundException e) { 265 // Couldn't lookup calling package info. This isn't really 266 // gonna happen, given that we're getting the name of the 267 // calling package from trusty old Activity.getCallingPackage. 268 // For that reason, we ignore this exception. 269 } 270 return callingPackage; 271 } 272 273 /** 274 * Returns the calling app name. 275 * @param activity 276 * @return the calling app name or general anonymous name if not found 277 */ 278 @NonNull getCallingAppName(Activity activity)279 public static String getCallingAppName(Activity activity) { 280 final String anonymous = activity.getString(R.string.anonymous_application); 281 final String packageName = getCallingPackageName(activity); 282 if (TextUtils.isEmpty(packageName)) { 283 return anonymous; 284 } 285 286 final PackageManager pm = activity.getPackageManager(); 287 ApplicationInfo ai; 288 try { 289 ai = pm.getApplicationInfo(packageName, 0); 290 } catch (final PackageManager.NameNotFoundException e) { 291 return anonymous; 292 } 293 294 CharSequence result = pm.getApplicationLabel(ai); 295 return TextUtils.isEmpty(result) ? anonymous : result.toString(); 296 } 297 298 /** 299 * Returns the default directory to be presented after starting the activity. 300 * Method can be overridden if the change of the behavior of the the child activity is needed. 301 */ getDefaultRootUri(Activity activity)302 public static Uri getDefaultRootUri(Activity activity) { 303 Uri defaultUri = Uri.parse(activity.getResources().getString(R.string.default_root_uri)); 304 305 if (!DocumentsContract.isRootUri(activity, defaultUri)) { 306 Log.e(TAG, "Default Root URI is not a valid root URI, falling back to Downloads."); 307 defaultUri = DocumentsContract.buildRootUri(Providers.AUTHORITY_DOWNLOADS, 308 Providers.ROOT_ID_DOWNLOADS); 309 } 310 311 return defaultUri; 312 } 313 isHardwareKeyboardAvailable(Context context)314 public static boolean isHardwareKeyboardAvailable(Context context) { 315 return context.getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS; 316 } 317 ensureKeyboardPresent(Context context, AlertDialog dialog)318 public static void ensureKeyboardPresent(Context context, AlertDialog dialog) { 319 if (!isHardwareKeyboardAvailable(context)) { 320 dialog.getWindow().setSoftInputMode( 321 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE 322 | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); 323 } 324 } 325 326 /** 327 * Check config whether DocumentsUI is launcher enabled or not. 328 * @return true if launcher icon is shown. 329 */ isLauncherEnabled(Context context)330 public static boolean isLauncherEnabled(Context context) { 331 PackageManager pm = context.getPackageManager(); 332 if (pm != null) { 333 final ComponentName component = new ComponentName( 334 context.getPackageName(), LAUNCHER_TARGET_CLASS); 335 final int value = pm.getComponentEnabledSetting(component); 336 return value == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; 337 } 338 339 return false; 340 } 341 getDeviceName(ContentResolver resolver)342 public static String getDeviceName(ContentResolver resolver) { 343 // We match the value supplied by ExternalStorageProvider for 344 // the internal storage root. 345 return Settings.Global.getString(resolver, Settings.Global.DEVICE_NAME); 346 } 347 checkMainLoop()348 public static void checkMainLoop() { 349 if (Looper.getMainLooper() != Looper.myLooper()) { 350 Log.e(TAG, "Calling from non-UI thread!"); 351 } 352 } 353 354 /** 355 * This method exists solely to smooth over the fact that two different types of 356 * views cannot be bound to the same id in different layouts. "What's this crazy-pants 357 * stuff?", you say? Here's an example: 358 * 359 * The main DocumentsUI view (aka "Files app") when running on a phone has a drop-down 360 * "breadcrumb" (file path representation) in both landscape and portrait orientation. 361 * Larger format devices, like a tablet, use a horizontal "Dir1 > Dir2 > Dir3" format 362 * breadcrumb in landscape layouts, but the regular drop-down breadcrumb in portrait 363 * mode. 364 * 365 * Our initial inclination was to give each of those views the same ID (as they both 366 * implement the same "Breadcrumb" interface). But at runtime, when rotating a device 367 * from one orientation to the other, deeeeeeep within the UI toolkit a exception 368 * would happen, because one view instance (drop-down) was being inflated in place of 369 * another (horizontal). I'm writing this code comment significantly after the face, 370 * so I don't recall all of the details, but it had to do with View type-checking the 371 * Parcelable state in onRestore, or something like that. Either way, this isn't 372 * allowed (my patch to fix this was rejected). 373 * 374 * To work around this we have this cute little method that accepts multiple 375 * resource IDs, and along w/ type inference finds our view, no matter which 376 * id it is wearing, and returns it. 377 */ 378 @SuppressWarnings("TypeParameterUnusedInFormals") findView(Activity activity, int... resources)379 public static @Nullable <T> T findView(Activity activity, int... resources) { 380 for (int id : resources) { 381 @SuppressWarnings("unchecked") 382 View view = activity.findViewById(id); 383 if (view != null) { 384 return (T) view; 385 } 386 } 387 return null; 388 } 389 Shared()390 private Shared() { 391 throw new UnsupportedOperationException("provides static fields only"); 392 } 393 } 394