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