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