blob: ec13a5061e8e319bf0e3001d2695093a8df8f9e7 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui.base;
import static com.android.documentsui.base.SharedMinimal.TAG;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Looper;
import android.provider.DocumentsContract;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import com.android.documentsui.R;
import com.android.documentsui.ui.MessageBuilder;
import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import androidx.annotation.PluralsRes;
import androidx.appcompat.app.AlertDialog;
/** @hide */
public final class Shared {
/** Intent action name to pick a copy destination. */
public static final String ACTION_PICK_COPY_DESTINATION =
"com.android.documentsui.PICK_COPY_DESTINATION";
// These values track values declared in MediaDocumentsProvider.
public static final String METADATA_KEY_AUDIO = "android.media.metadata.audio";
public static final String METADATA_KEY_VIDEO = "android.media.metadata.video";
public static final String METADATA_VIDEO_LATITUDE = "android.media.metadata.video:latitude";
public static final String METADATA_VIDEO_LONGITUTE = "android.media.metadata.video:longitude";
/**
* Extra boolean flag for {@link #ACTION_PICK_COPY_DESTINATION}, which
* specifies if the destination directory needs to create new directory or not.
*/
public static final String EXTRA_DIRECTORY_COPY = "com.android.documentsui.DIRECTORY_COPY";
/**
* Extra flag used to store the current stack so user opens in right spot.
*/
public static final String EXTRA_STACK = "com.android.documentsui.STACK";
/**
* Extra flag used to store query of type String in the bundle.
*/
public static final String EXTRA_QUERY = "query";
/**
* Extra flag used to store chip's title of type String array in the bundle.
*/
public static final String EXTRA_QUERY_CHIPS = "query_chips";
/**
* Extra flag used to store state of type State in the bundle.
*/
public static final String EXTRA_STATE = "state";
/**
* Extra flag used to store root of type RootInfo in the bundle.
*/
public static final String EXTRA_ROOT = "root";
/**
* Extra flag used to store document of DocumentInfo type in the bundle.
*/
public static final String EXTRA_DOC = "document";
/**
* Extra flag used to store DirectoryFragment's selection of Selection type in the bundle.
*/
public static final String EXTRA_SELECTION = "selection";
/**
* Extra flag used to store DirectoryFragment's ignore state of boolean type in the bundle.
*/
public static final String EXTRA_IGNORE_STATE = "ignoreState";
/**
* Extra flag used to store pick result state of PickResult type in the bundle.
*/
public static final String EXTRA_PICK_RESULT = "pickResult";
/**
* Extra for an Intent for enabling performance benchmark. Used only by tests.
*/
public static final String EXTRA_BENCHMARK = "com.android.documentsui.benchmark";
/**
* Extra flag used to signify to inspector that debug section can be shown.
*/
public static final String EXTRA_SHOW_DEBUG = "com.android.documentsui.SHOW_DEBUG";
/**
* Maximum number of items in a Binder transaction packet.
*/
public static final int MAX_DOCS_IN_INTENT = 500;
/**
* Animation duration of checkbox in directory list/grid in millis.
*/
public static final int CHECK_ANIMATION_DURATION = 100;
private static final Collator sCollator;
static {
sCollator = Collator.getInstance();
sCollator.setStrength(Collator.SECONDARY);
}
/**
* @deprecated use {@link MessageBuilder#getQuantityString}
*/
@Deprecated
public static final String getQuantityString(Context context, @PluralsRes int resourceId, int quantity) {
return context.getResources().getQuantityString(resourceId, quantity, quantity);
}
public static String formatTime(Context context, long when) {
// TODO: DateUtils should make this easier
Time then = new Time();
then.set(when);
Time now = new Time();
now.setToNow();
int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
| DateUtils.FORMAT_ABBREV_ALL;
if (then.year != now.year) {
flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
} else if (then.yearDay != now.yearDay) {
flags |= DateUtils.FORMAT_SHOW_DATE;
} else {
flags |= DateUtils.FORMAT_SHOW_TIME;
}
return DateUtils.formatDateTime(context, when, flags);
}
/**
* A convenient way to transform any list into a (parcelable) ArrayList.
* Uses cast if possible, else creates a new list with entries from {@code list}.
*/
public static <T> ArrayList<T> asArrayList(List<T> list) {
return list instanceof ArrayList
? (ArrayList<T>) list
: new ArrayList<>(list);
}
/**
* Compare two strings against each other using system default collator in a
* case-insensitive mode. Clusters strings prefixed with {@link DIR_PREFIX}
* before other items.
*/
public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
final boolean leftEmpty = TextUtils.isEmpty(lhs);
final boolean rightEmpty = TextUtils.isEmpty(rhs);
if (leftEmpty && rightEmpty) return 0;
if (leftEmpty) return -1;
if (rightEmpty) return 1;
return sCollator.compare(lhs, rhs);
}
private static boolean isSystemApp(ApplicationInfo ai) {
return (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
}
private static boolean isUpdatedSystemApp(ApplicationInfo ai) {
return (ai.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
}
/**
* Returns the calling package, possibly overridden by EXTRA_PACKAGE_NAME.
* @param activity
* @return
*/
public static String getCallingPackageName(Activity activity) {
String callingPackage = activity.getCallingPackage();
// System apps can set the calling package name using an extra.
try {
ApplicationInfo info =
activity.getPackageManager().getApplicationInfo(callingPackage, 0);
if (isSystemApp(info) || isUpdatedSystemApp(info)) {
final String extra = activity.getIntent().getStringExtra(
Intent.EXTRA_PACKAGE_NAME);
if (extra != null && !TextUtils.isEmpty(extra)) {
callingPackage = extra;
}
}
} catch (NameNotFoundException e) {
// Couldn't lookup calling package info. This isn't really
// gonna happen, given that we're getting the name of the
// calling package from trusty old Activity.getCallingPackage.
// For that reason, we ignore this exception.
}
return callingPackage;
}
/**
* Returns the default directory to be presented after starting the activity.
* Method can be overridden if the change of the behavior of the the child activity is needed.
*/
public static Uri getDefaultRootUri(Activity activity) {
Uri defaultUri = Uri.parse(activity.getResources().getString(R.string.default_root_uri));
if (!DocumentsContract.isRootUri(activity, defaultUri)) {
throw new RuntimeException("Default Root URI is not a valid root URI.");
}
return defaultUri;
}
public static boolean isHardwareKeyboardAvailable(Context context) {
return context.getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS;
}
public static void ensureKeyboardPresent(Context context, AlertDialog dialog) {
if (!isHardwareKeyboardAvailable(context)) {
dialog.getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
}
/**
* Returns true if "Documents" root should be shown.
*/
public static boolean shouldShowDocumentsRoot(Context context) {
return context.getResources().getBoolean(R.bool.show_documents_root);
}
/**
* Check config whether DocumentsUI is launcher enabled or not.
* @return true if "is_launcher_enabled" is true.
*/
public static boolean isLauncherEnabled(Context context) {
return context.getResources().getBoolean(R.bool.is_launcher_enabled);
}
/**
* Check config has quick viewer package value or not.
* @return true if "trusted_quick_viewer_package" has value.
*/
public static boolean hasQuickViewer(Context context) {
return !TextUtils.isEmpty(context.getString(R.string.trusted_quick_viewer_package));
}
/*
* Returns true if the local/device storage root must be visible (this also hides
* the option to toggle visibility in the menu.)
*/
public static boolean mustShowDeviceRoot(Intent intent) {
return intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
}
public static String getDeviceName(ContentResolver resolver) {
// We match the value supplied by ExternalStorageProvider for
// the internal storage root.
return Settings.Global.getString(resolver, Settings.Global.DEVICE_NAME);
}
public static void checkMainLoop() {
if (Looper.getMainLooper() != Looper.myLooper()) {
Log.e(TAG, "Calling from non-UI thread!");
}
}
/**
* This method exists solely to smooth over the fact that two different types of
* views cannot be bound to the same id in different layouts. "What's this crazy-pants
* stuff?", you say? Here's an example:
*
* The main DocumentsUI view (aka "Files app") when running on a phone has a drop-down
* "breadcrumb" (file path representation) in both landscape and portrait orientation.
* Larger format devices, like a tablet, use a horizontal "Dir1 > Dir2 > Dir3" format
* breadcrumb in landscape layouts, but the regular drop-down breadcrumb in portrait
* mode.
*
* Our initial inclination was to give each of those views the same ID (as they both
* implement the same "Breadcrumb" interface). But at runtime, when rotating a device
* from one orientation to the other, deeeeeeep within the UI toolkit a exception
* would happen, because one view instance (drop-down) was being inflated in place of
* another (horizontal). I'm writing this code comment significantly after the face,
* so I don't recall all of the details, but it had to do with View type-checking the
* Parcelable state in onRestore, or something like that. Either way, this isn't
* allowed (my patch to fix this was rejected).
*
* To work around this we have this cute little method that accepts multiple
* resource IDs, and along w/ type inference finds our view, no matter which
* id it is wearing, and returns it.
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
public static @Nullable <T> T findView(Activity activity, int... resources) {
for (int id : resources) {
@SuppressWarnings("unchecked")
View view = activity.findViewById(id);
if (view != null) {
return (T) view;
}
}
return null;
}
private Shared() {
throw new UnsupportedOperationException("provides static fields only");
}
}