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