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.isAtLeastR()) {
175             return false;
176         }
177 
178         final String packageName = getCallingPackageName(activity);
179         final boolean ret = CompatChanges.isChangeEnabled(RESTRICT_STORAGE_ACCESS_FRAMEWORK,
180                 packageName, Process.myUserHandle());
181 
182         Log.d(TAG,
183                 "shouldRestrictStorageAccessFramework = " + ret + ", packageName = " + packageName);
184 
185         return ret;
186     }
187 
formatTime(Context context, long when)188     public static String formatTime(Context context, long when) {
189         // TODO: DateUtils should make this easier
190         ZoneId zoneId = ZoneId.systemDefault();
191         LocalDateTime then = LocalDateTime.ofInstant(Instant.ofEpochMilli(when), zoneId);
192         LocalDateTime now = LocalDateTime.ofInstant(Instant.now(), zoneId);
193 
194         int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
195                 | DateUtils.FORMAT_ABBREV_ALL;
196 
197         if (then.getYear() != now.getYear()) {
198             flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
199         } else if (then.getDayOfYear() != now.getDayOfYear()) {
200             flags |= DateUtils.FORMAT_SHOW_DATE;
201         } else {
202             flags |= DateUtils.FORMAT_SHOW_TIME;
203         }
204 
205         return DateUtils.formatDateTime(context, when, flags);
206     }
207 
208     /**
209      * A convenient way to transform any list into a (parcelable) ArrayList.
210      * Uses cast if possible, else creates a new list with entries from {@code list}.
211      */
asArrayList(List<T> list)212     public static <T> ArrayList<T> asArrayList(List<T> list) {
213         return list instanceof ArrayList
214             ? (ArrayList<T>) list
215             : new ArrayList<>(list);
216     }
217 
218     /**
219      * Compare two strings against each other using system default collator in a
220      * case-insensitive mode. Clusters strings prefixed with {@link DIR_PREFIX}
221      * before other items.
222      */
compareToIgnoreCaseNullable(String lhs, String rhs)223     public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
224         final boolean leftEmpty = TextUtils.isEmpty(lhs);
225         final boolean rightEmpty = TextUtils.isEmpty(rhs);
226 
227         if (leftEmpty && rightEmpty) return 0;
228         if (leftEmpty) return -1;
229         if (rightEmpty) return 1;
230 
231         return sCollator.compare(lhs, rhs);
232     }
233 
isSystemApp(ApplicationInfo ai)234     private static boolean isSystemApp(ApplicationInfo ai) {
235         return (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
236     }
237 
isUpdatedSystemApp(ApplicationInfo ai)238     private static boolean isUpdatedSystemApp(ApplicationInfo ai) {
239         return (ai.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
240     }
241 
242     /**
243      * Returns the calling package, possibly overridden by EXTRA_PACKAGE_NAME.
244      * @param activity
245      * @return
246      */
getCallingPackageName(Activity activity)247     public static String getCallingPackageName(Activity activity) {
248         String callingPackage = activity.getCallingPackage();
249         // System apps can set the calling package name using an extra.
250         try {
251             ApplicationInfo info =
252                     activity.getPackageManager().getApplicationInfo(callingPackage, 0);
253             if (isSystemApp(info) || isUpdatedSystemApp(info)) {
254                 final String extra = activity.getIntent().getStringExtra(
255                         Intent.EXTRA_PACKAGE_NAME);
256                 if (extra != null && !TextUtils.isEmpty(extra)) {
257                     callingPackage = extra;
258                 }
259             }
260         } catch (NameNotFoundException e) {
261             // Couldn't lookup calling package info. This isn't really
262             // gonna happen, given that we're getting the name of the
263             // calling package from trusty old Activity.getCallingPackage.
264             // For that reason, we ignore this exception.
265         }
266         return callingPackage;
267     }
268 
269     /**
270      * Returns the calling app name.
271      * @param activity
272      * @return the calling app name or general anonymous name if not found
273      */
274     @NonNull
getCallingAppName(Activity activity)275     public static String getCallingAppName(Activity activity) {
276         final String anonymous = activity.getString(R.string.anonymous_application);
277         final String packageName = getCallingPackageName(activity);
278         if (TextUtils.isEmpty(packageName)) {
279             return anonymous;
280         }
281 
282         final PackageManager pm = activity.getPackageManager();
283         ApplicationInfo ai;
284         try {
285             ai = pm.getApplicationInfo(packageName, 0);
286         } catch (final PackageManager.NameNotFoundException e) {
287             return anonymous;
288         }
289 
290         CharSequence result = pm.getApplicationLabel(ai);
291         return TextUtils.isEmpty(result) ? anonymous : result.toString();
292     }
293 
294     /**
295      * Returns the default directory to be presented after starting the activity.
296      * Method can be overridden if the change of the behavior of the the child activity is needed.
297      */
getDefaultRootUri(Activity activity)298     public static Uri getDefaultRootUri(Activity activity) {
299         Uri defaultUri = Uri.parse(activity.getResources().getString(R.string.default_root_uri));
300 
301         if (!DocumentsContract.isRootUri(activity, defaultUri)) {
302             Log.e(TAG, "Default Root URI is not a valid root URI, falling back to Downloads.");
303             defaultUri = DocumentsContract.buildRootUri(Providers.AUTHORITY_DOWNLOADS,
304                     Providers.ROOT_ID_DOWNLOADS);
305         }
306 
307         return defaultUri;
308     }
309 
isHardwareKeyboardAvailable(Context context)310     public static boolean isHardwareKeyboardAvailable(Context context) {
311         return context.getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS;
312     }
313 
ensureKeyboardPresent(Context context, AlertDialog dialog)314     public static void ensureKeyboardPresent(Context context, AlertDialog dialog) {
315         if (!isHardwareKeyboardAvailable(context)) {
316             dialog.getWindow().setSoftInputMode(
317                     WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
318         }
319     }
320 
321     /**
322      * Check config whether DocumentsUI is launcher enabled or not.
323      * @return true if launcher icon is shown.
324      */
isLauncherEnabled(Context context)325     public static boolean isLauncherEnabled(Context context) {
326         PackageManager pm = context.getPackageManager();
327         if (pm != null) {
328             final ComponentName component = new ComponentName(
329                     context.getPackageName(), LAUNCHER_TARGET_CLASS);
330             final int value = pm.getComponentEnabledSetting(component);
331             return value == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
332         }
333 
334         return false;
335     }
336 
getDeviceName(ContentResolver resolver)337     public static String getDeviceName(ContentResolver resolver) {
338         // We match the value supplied by ExternalStorageProvider for
339         // the internal storage root.
340         return Settings.Global.getString(resolver, Settings.Global.DEVICE_NAME);
341     }
342 
checkMainLoop()343     public static void checkMainLoop() {
344         if (Looper.getMainLooper() != Looper.myLooper()) {
345             Log.e(TAG, "Calling from non-UI thread!");
346         }
347     }
348 
349     /**
350      * This method exists solely to smooth over the fact that two different types of
351      * views cannot be bound to the same id in different layouts. "What's this crazy-pants
352      * stuff?", you say? Here's an example:
353      *
354      * The main DocumentsUI view (aka "Files app") when running on a phone has a drop-down
355      * "breadcrumb" (file path representation) in both landscape and portrait orientation.
356      * Larger format devices, like a tablet, use a horizontal "Dir1 > Dir2 > Dir3" format
357      * breadcrumb in landscape layouts, but the regular drop-down breadcrumb in portrait
358      * mode.
359      *
360      * Our initial inclination was to give each of those views the same ID (as they both
361      * implement the same "Breadcrumb" interface). But at runtime, when rotating a device
362      * from one orientation to the other, deeeeeeep within the UI toolkit a exception
363      * would happen, because one view instance (drop-down) was being inflated in place of
364      * another (horizontal). I'm writing this code comment significantly after the face,
365      * so I don't recall all of the details, but it had to do with View type-checking the
366      * Parcelable state in onRestore, or something like that. Either way, this isn't
367      * allowed (my patch to fix this was rejected).
368      *
369      * To work around this we have this cute little method that accepts multiple
370      * resource IDs, and along w/ type inference finds our view, no matter which
371      * id it is wearing, and returns it.
372      */
373     @SuppressWarnings("TypeParameterUnusedInFormals")
findView(Activity activity, int... resources)374     public static @Nullable <T> T findView(Activity activity, int... resources) {
375         for (int id : resources) {
376             @SuppressWarnings("unchecked")
377             View view = activity.findViewById(id);
378             if (view != null) {
379                 return (T) view;
380             }
381         }
382         return null;
383     }
384 
Shared()385     private Shared() {
386         throw new UnsupportedOperationException("provides static fields only");
387     }
388 }
389