• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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