• 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 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