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