diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-11-17 16:38:15 -0500 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-11-17 16:38:15 -0500 |
commit | 6a65f2da209bff03cb0eb6da309710ac6ee5026d (patch) | |
tree | 48e2090e716d4178378cb0599fc5d9cffbcf3f63 /android | |
parent | 46c77c203439b3b37c99d09e326df4b1fe08c10b (diff) | |
download | android-28-6a65f2da209bff03cb0eb6da309710ac6ee5026d.tar.gz |
Import Android SDK Platform P [4456821]
/google/data/ro/projects/android/fetch_artifact \
--bid 4456821 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4456821.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I2d206b200d7952f899a5d1647ab532638cc8dd43
Diffstat (limited to 'android')
379 files changed, 12364 insertions, 5717 deletions
diff --git a/android/animation/ValueAnimator.java b/android/animation/ValueAnimator.java index ee89ca8d..cc95eb6f 100644 --- a/android/animation/ValueAnimator.java +++ b/android/animation/ValueAnimator.java @@ -254,6 +254,11 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio HashMap<String, PropertyValuesHolder> mValuesMap; /** + * If set to non-negative value, this will override {@link #sDurationScale}. + */ + private float mDurationScale = -1f; + + /** * Public constants */ @@ -579,8 +584,23 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio return this; } + /** + * Overrides the global duration scale by a custom value. + * + * @param durationScale The duration scale to set; or {@code -1f} to use the global duration + * scale. + * @hide + */ + public void overrideDurationScale(float durationScale) { + mDurationScale = durationScale; + } + + private float resolveDurationScale() { + return mDurationScale >= 0f ? mDurationScale : sDurationScale; + } + private long getScaledDuration() { - return (long)(mDuration * sDurationScale); + return (long)(mDuration * resolveDurationScale()); } /** @@ -735,7 +755,10 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio if (mSeekFraction >= 0) { return (long) (mDuration * mSeekFraction); } - float durationScale = sDurationScale == 0 ? 1 : sDurationScale; + float durationScale = resolveDurationScale(); + if (durationScale == 0f) { + durationScale = 1f; + } return (long) ((AnimationUtils.currentAnimationTimeMillis() - mStartTime) / durationScale); } @@ -1397,7 +1420,9 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio if (mStartTime < 0) { // First frame. If there is start delay, start delay count down will happen *after* this // frame. - mStartTime = mReversing ? frameTime : frameTime + (long) (mStartDelay * sDurationScale); + mStartTime = mReversing + ? frameTime + : frameTime + (long) (mStartDelay * resolveDurationScale()); } // Handle pause/resume diff --git a/android/app/Activity.java b/android/app/Activity.java index 9d331a02..99f3dee7 100644 --- a/android/app/Activity.java +++ b/android/app/Activity.java @@ -16,8 +16,6 @@ package android.app; -import static android.os.Build.VERSION_CODES.O_MR1; - import static java.lang.Character.MIN_VALUE; import android.annotation.CallSuper; @@ -136,6 +134,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; + /** * An activity is a single, focused thing that the user can do. Almost all * activities interact with the user, so the Activity class takes care of @@ -194,10 +193,13 @@ import java.util.List; * <a name="Fragments"></a> * <h3>Fragments</h3> * - * <p>Starting with {@link android.os.Build.VERSION_CODES#HONEYCOMB}, Activity - * implementations can make use of the {@link Fragment} class to better + * <p>The {@link android.support.v4.app.FragmentActivity} subclass + * can make use of the {@link android.support.v4.app.Fragment} class to better * modularize their code, build more sophisticated user interfaces for larger - * screens, and help scale their application between small and large screens. + * screens, and help scale their application between small and large screens.</p> + * + * <p>For more information about using fragments, read the + * <a href="{@docRoot}guide/components/fragments.html">Fragments</a> developer guide.</p> * * <a name="ActivityLifecycle"></a> * <h3>Activity Lifecycle</h3> @@ -916,7 +918,10 @@ public class Activity extends ContextThemeWrapper /** * Return the LoaderManager for this activity, creating it if needed. + * + * @deprecated Use {@link android.support.v4.app.FragmentActivity#getSupportLoaderManager()} */ + @Deprecated public LoaderManager getLoaderManager() { return mFragments.getLoaderManager(); } @@ -991,17 +996,6 @@ public class Activity extends ContextThemeWrapper protected void onCreate(@Nullable Bundle savedInstanceState) { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState); - if (getApplicationInfo().targetSdkVersion >= O_MR1 && mActivityInfo.isFixedOrientation()) { - final TypedArray ta = obtainStyledAttributes(com.android.internal.R.styleable.Window); - final boolean isTranslucentOrFloating = ActivityInfo.isTranslucentOrFloating(ta); - ta.recycle(); - - if (isTranslucentOrFloating) { - throw new IllegalStateException( - "Only fullscreen opaque activities can request orientation"); - } - } - if (mLastNonConfigurationInstances != null) { mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders); } @@ -2407,7 +2401,10 @@ public class Activity extends ContextThemeWrapper /** * Return the FragmentManager for interacting with fragments associated * with this activity. + * + * @deprecated Use {@link android.support.v4.app.FragmentActivity#getSupportFragmentManager()} */ + @Deprecated public FragmentManager getFragmentManager() { return mFragments.getFragmentManager(); } @@ -2416,7 +2413,11 @@ public class Activity extends ContextThemeWrapper * Called when a Fragment is being attached to this activity, immediately * after the call to its {@link Fragment#onAttach Fragment.onAttach()} * method and before {@link Fragment#onCreate Fragment.onCreate()}. + * + * @deprecated Use {@link + * android.support.v4.app.FragmentActivity#onAttachFragment(android.support.v4.app.Fragment)} */ + @Deprecated public void onAttachFragment(Fragment fragment) { } @@ -5118,7 +5119,11 @@ public class Activity extends ContextThemeWrapper * * @see Fragment#startActivity * @see Fragment#startActivityForResult + * + * @deprecated Use {@link android.support.v4.app.FragmentActivity#startActivityFromFragment( + * android.support.v4.app.Fragment,Intent,int)} */ + @Deprecated public void startActivityFromFragment(@NonNull Fragment fragment, @RequiresPermission Intent intent, int requestCode) { startActivityFromFragment(fragment, intent, requestCode, null); @@ -5143,7 +5148,11 @@ public class Activity extends ContextThemeWrapper * * @see Fragment#startActivity * @see Fragment#startActivityForResult + * + * @deprecated Use {@link android.support.v4.app.FragmentActivity#startActivityFromFragment( + * android.support.v4.app.Fragment,Intent,int,Bundle)} */ + @Deprecated public void startActivityFromFragment(@NonNull Fragment fragment, @RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) { startActivityForResult(fragment.mWho, intent, requestCode, options); @@ -7304,24 +7313,25 @@ public class Activity extends ContextThemeWrapper } /** - * Request to put this Activity in a mode where the user is locked to the - * current task. + * Request to put this activity in a mode where the user is locked to a restricted set of + * applications. * - * This will prevent the user from launching other apps, going to settings, or reaching the - * home screen. This does not include those apps whose {@link android.R.attr#lockTaskMode} - * values permit launching while locked. + * <p>If {@link DevicePolicyManager#isLockTaskPermitted(String)} returns {@code true} + * for this component, the current task will be launched directly into LockTask mode. Only apps + * whitelisted by {@link DevicePolicyManager#setLockTaskPackages(ComponentName, String[])} can + * be launched while LockTask mode is active. The user will not be able to leave this mode + * until this activity calls {@link #stopLockTask()}. Calling this method while the device is + * already in LockTask mode has no effect. * - * If {@link DevicePolicyManager#isLockTaskPermitted(String)} returns true or - * lockTaskMode=lockTaskModeAlways for this component then the app will go directly into - * Lock Task mode. The user will not be able to exit this mode until - * {@link Activity#stopLockTask()} is called. + * <p>Otherwise, the current task will be launched into screen pinning mode. In this case, the + * system will prompt the user with a dialog requesting permission to use this mode. + * The user can exit at any time through instructions shown on the request dialog. Calling + * {@link #stopLockTask()} will also terminate this mode. * - * If {@link DevicePolicyManager#isLockTaskPermitted(String)} returns false - * then the system will prompt the user with a dialog requesting permission to enter - * this mode. When entered through this method the user can exit at any time through - * an action described by the request dialog. Calling stopLockTask will also exit the - * mode. + * <p><strong>Note:</strong> this method can only be called when the activity is foreground. + * That is, between {@link #onResume()} and {@link #onPause()}. * + * @see #stopLockTask() * @see android.R.attr#lockTaskMode */ public void startLockTask() { @@ -7332,25 +7342,24 @@ public class Activity extends ContextThemeWrapper } /** - * Allow the user to switch away from the current task. + * Stop the current task from being locked. * - * Called to end the mode started by {@link Activity#startLockTask}. This - * can only be called by activities that have successfully called - * startLockTask previously. + * <p>Called to end the LockTask or screen pinning mode started by {@link #startLockTask()}. + * This can only be called by activities that have called {@link #startLockTask()} previously. * - * This will allow the user to exit this app and move onto other activities. - * <p>Note: This method should only be called when the activity is user-facing. That is, - * between onResume() and onPause(). - * <p>Note: If there are other tasks below this one that are also locked then calling this - * method will immediately finish this task and resume the previous locked one, remaining in - * lockTask mode. + * <p><strong>Note:</strong> If the device is in LockTask mode that is not initially started + * by this activity, then calling this method will not terminate the LockTask mode, but only + * finish its own task. The device will remain in LockTask mode, until the activity which + * started the LockTask mode calls this method, or until its whitelist authorization is revoked + * by {@link DevicePolicyManager#setLockTaskPackages(ComponentName, String[])}. * + * @see #startLockTask() * @see android.R.attr#lockTaskMode * @see ActivityManager#getLockTaskModeState() */ public void stopLockTask() { try { - ActivityManager.getService().stopLockTaskMode(); + ActivityManager.getService().stopLockTaskModeByToken(mToken); } catch (RemoteException e) { } } diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java index 8d9dc1fa..064e9782 100644 --- a/android/app/ActivityManager.java +++ b/android/app/ActivityManager.java @@ -682,20 +682,23 @@ public class ActivityManager { } /** - * Input parameter to {@link android.app.IActivityManager#moveTaskToDockedStack} which - * specifies the position of the created docked stack at the top half of the screen if + * Parameter to {@link android.app.IActivityManager#setTaskWindowingModeSplitScreenPrimary} + * which specifies the position of the created docked stack at the top half of the screen if * in portrait mode or at the left half of the screen if in landscape mode. * @hide */ - public static final int DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT = 0; + @TestApi + public static final int SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT = 0; /** - * Input parameter to {@link android.app.IActivityManager#moveTaskToDockedStack} which + * Parameter to {@link android.app.IActivityManager#setTaskWindowingModeSplitScreenPrimary} + * which * specifies the position of the created docked stack at the bottom half of the screen if * in portrait mode or at the right half of the screen if in landscape mode. * @hide */ - public static final int DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT = 1; + @TestApi + public static final int SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT = 1; /** * Input parameter to {@link android.app.IActivityManager#resizeTask} which indicates @@ -1925,6 +1928,33 @@ public class ActivityManager { } /** + * Moves the input task to the primary-split-screen stack. + * @param taskId Id of task to move. + * @param createMode The mode the primary split screen stack should be created in if it doesn't + * exist already. See + * {@link android.app.ActivityManager#SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT} + * and + * {@link android.app.ActivityManager + * #SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT} + * @param toTop If the task and stack should be moved to the top. + * @param animate Whether we should play an animation for the moving the task + * @param initialBounds If the primary stack gets created, it will use these bounds for the + * docked stack. Pass {@code null} to use default bounds. + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void setTaskWindowingModeSplitScreenPrimary(int taskId, int createMode, boolean toTop, + boolean animate, Rect initialBounds) throws SecurityException { + try { + getService().setTaskWindowingModeSplitScreenPrimary(taskId, createMode, toTop, animate, + initialBounds); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Resizes the input stack id to the given bounds. * @param stackId Id of the stack to resize. * @param bounds Bounds to resize the stack to or {@code null} for fullscreen. diff --git a/android/app/ActivityManagerInternal.java b/android/app/ActivityManagerInternal.java index 9d14f616..a46b3c72 100644 --- a/android/app/ActivityManagerInternal.java +++ b/android/app/ActivityManagerInternal.java @@ -64,6 +64,27 @@ public abstract class ActivityManagerInternal { public static final int APP_TRANSITION_SNAPSHOT = 4; /** + * The bundle key to extract the assist data. + */ + public static final String ASSIST_KEY_DATA = "data"; + + /** + * The bundle key to extract the assist structure. + */ + public static final String ASSIST_KEY_STRUCTURE = "structure"; + + /** + * The bundle key to extract the assist content. + */ + public static final String ASSIST_KEY_CONTENT = "content"; + + /** + * The bundle key to extract the assist receiver extras. + */ + public static final String ASSIST_KEY_RECEIVER_EXTRAS = "receiverExtras"; + + + /** * Grant Uri permissions from one app to another. This method only extends * permission grants if {@code callingUid} has permission to them. */ diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java index b62e4c2d..4a21f5c4 100644 --- a/android/app/ActivityOptions.java +++ b/android/app/ActivityOptions.java @@ -16,7 +16,7 @@ package android.app; -import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; +import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.INVALID_DISPLAY; @@ -203,10 +203,11 @@ public class ActivityOptions { "android.activity.taskOverlayCanResume"; /** - * Where the docked stack should be positioned. + * Where the split-screen-primary stack should be positioned. * @hide */ - private static final String KEY_DOCK_CREATE_MODE = "android:activity.dockCreateMode"; + private static final String KEY_SPLIT_SCREEN_CREATE_MODE = + "android:activity.splitScreenCreateMode"; /** * Determines whether to disallow the outgoing activity from entering picture-in-picture as the @@ -292,7 +293,7 @@ public class ActivityOptions { @WindowConfiguration.ActivityType private int mLaunchActivityType = ACTIVITY_TYPE_UNDEFINED; private int mLaunchTaskId = -1; - private int mDockCreateMode = DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; + private int mSplitScreenCreateMode = SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT; private boolean mDisallowEnterPictureInPictureWhileLaunching; private boolean mTaskOverlay; private boolean mTaskOverlayCanResume; @@ -884,7 +885,8 @@ public class ActivityOptions { mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1); mTaskOverlay = opts.getBoolean(KEY_TASK_OVERLAY, false); mTaskOverlayCanResume = opts.getBoolean(KEY_TASK_OVERLAY_CAN_RESUME, false); - mDockCreateMode = opts.getInt(KEY_DOCK_CREATE_MODE, DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT); + mSplitScreenCreateMode = opts.getInt(KEY_SPLIT_SCREEN_CREATE_MODE, + SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT); mDisallowEnterPictureInPictureWhileLaunching = opts.getBoolean( KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING, false); if (opts.containsKey(KEY_ANIM_SPECS)) { @@ -1194,13 +1196,13 @@ public class ActivityOptions { } /** @hide */ - public int getDockCreateMode() { - return mDockCreateMode; + public int getSplitScreenCreateMode() { + return mSplitScreenCreateMode; } /** @hide */ - public void setDockCreateMode(int dockCreateMode) { - mDockCreateMode = dockCreateMode; + public void setSplitScreenCreateMode(int splitScreenCreateMode) { + mSplitScreenCreateMode = splitScreenCreateMode; } /** @hide */ @@ -1369,7 +1371,7 @@ public class ActivityOptions { b.putInt(KEY_LAUNCH_TASK_ID, mLaunchTaskId); b.putBoolean(KEY_TASK_OVERLAY, mTaskOverlay); b.putBoolean(KEY_TASK_OVERLAY_CAN_RESUME, mTaskOverlayCanResume); - b.putInt(KEY_DOCK_CREATE_MODE, mDockCreateMode); + b.putInt(KEY_SPLIT_SCREEN_CREATE_MODE, mSplitScreenCreateMode); b.putBoolean(KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING, mDisallowEnterPictureInPictureWhileLaunching); if (mAnimSpecs != null) { diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java index 2516a3e9..21e454f1 100644 --- a/android/app/ActivityThread.java +++ b/android/app/ActivityThread.java @@ -5533,32 +5533,8 @@ public final class ActivityThread { View.mDebugViewAttributes = mCoreSettings.getInt(Settings.Global.DEBUG_VIEW_ATTRIBUTES, 0) != 0; - /** - * For system applications on userdebug/eng builds, log stack - * traces of disk and network access to dropbox for analysis. - */ - if ((data.appInfo.flags & - (ApplicationInfo.FLAG_SYSTEM | - ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0) { - StrictMode.conditionallyEnableDebugLogging(); - } - - /** - * For apps targetting Honeycomb or later, we don't allow network usage - * on the main event loop / UI thread. This is what ultimately throws - * {@link NetworkOnMainThreadException}. - */ - if (data.appInfo.targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) { - StrictMode.enableDeathOnNetwork(); - } - - /** - * For apps targetting N or later, we don't allow file:// Uri exposure. - * This is what ultimately throws {@link FileUriExposedException}. - */ - if (data.appInfo.targetSdkVersion >= Build.VERSION_CODES.N) { - StrictMode.enableDeathOnFileUriExposure(); - } + StrictMode.initThreadDefaults(data.appInfo); + StrictMode.initVmDefaults(data.appInfo); // We deprecated Build.SERIAL and only apps that target pre NMR1 // SDK can see it. Since access to the serial is now behind a @@ -5655,7 +5631,12 @@ public final class ActivityThread { mResourcesManager.getConfiguration().getLocales()); if (!Process.isIsolated()) { - setupGraphicsSupport(appContext); + final int oldMask = StrictMode.allowThreadDiskWritesMask(); + try { + setupGraphicsSupport(appContext); + } finally { + StrictMode.setThreadPolicyMask(oldMask); + } } // If we use profiles, setup the dex reporter to notify package manager diff --git a/android/app/AppOpsManager.java b/android/app/AppOpsManager.java index 4bd85ae9..b6fb1201 100644 --- a/android/app/AppOpsManager.java +++ b/android/app/AppOpsManager.java @@ -254,8 +254,10 @@ public class AppOpsManager { public static final int OP_ANSWER_PHONE_CALLS = 69; /** @hide Run jobs when in background */ public static final int OP_RUN_ANY_IN_BACKGROUND = 70; + /** @hide Change Wi-Fi connectivity state */ + public static final int OP_CHANGE_WIFI_STATE = 71; /** @hide */ - public static final int _NUM_OP = 71; + public static final int _NUM_OP = 72; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -496,6 +498,7 @@ public class AppOpsManager { OP_INSTANT_APP_START_FOREGROUND, OP_ANSWER_PHONE_CALLS, OP_RUN_ANY_IN_BACKGROUND, + OP_CHANGE_WIFI_STATE, }; /** @@ -574,6 +577,7 @@ public class AppOpsManager { OPSTR_INSTANT_APP_START_FOREGROUND, OPSTR_ANSWER_PHONE_CALLS, null, // OP_RUN_ANY_IN_BACKGROUND + null, // OP_CHANGE_WIFI_STATE }; /** @@ -652,6 +656,7 @@ public class AppOpsManager { "INSTANT_APP_START_FOREGROUND", "ANSWER_PHONE_CALLS", "RUN_ANY_IN_BACKGROUND", + "CHANGE_WIFI_STATE", }; /** @@ -730,6 +735,7 @@ public class AppOpsManager { Manifest.permission.INSTANT_APP_FOREGROUND_SERVICE, Manifest.permission.ANSWER_PHONE_CALLS, null, // no permission for OP_RUN_ANY_IN_BACKGROUND + Manifest.permission.CHANGE_WIFI_STATE, }; /** @@ -809,6 +815,7 @@ public class AppOpsManager { null, // INSTANT_APP_START_FOREGROUND null, // ANSWER_PHONE_CALLS null, // OP_RUN_ANY_IN_BACKGROUND + null, // OP_CHANGE_WIFI_STATE }; /** @@ -887,6 +894,7 @@ public class AppOpsManager { false, // INSTANT_APP_START_FOREGROUND false, // ANSWER_PHONE_CALLS false, // OP_RUN_ANY_IN_BACKGROUND + false, // OP_CHANGE_WIFI_STATE }; /** @@ -964,6 +972,7 @@ public class AppOpsManager { AppOpsManager.MODE_DEFAULT, // OP_INSTANT_APP_START_FOREGROUND AppOpsManager.MODE_ALLOWED, // ANSWER_PHONE_CALLS AppOpsManager.MODE_ALLOWED, // OP_RUN_ANY_IN_BACKGROUND + AppOpsManager.MODE_ALLOWED, // OP_CHANGE_WIFI_STATE }; /** @@ -1045,6 +1054,7 @@ public class AppOpsManager { false, false, // ANSWER_PHONE_CALLS false, // OP_RUN_ANY_IN_BACKGROUND + false, // OP_CHANGE_WIFI_STATE }; /** diff --git a/android/app/DexLoadReporter.java b/android/app/DexLoadReporter.java index f99d1a8e..06434147 100644 --- a/android/app/DexLoadReporter.java +++ b/android/app/DexLoadReporter.java @@ -19,7 +19,6 @@ package android.app; import android.os.FileUtils; import android.os.RemoteException; import android.os.SystemProperties; -import android.system.ErrnoException; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -27,8 +26,6 @@ import com.android.internal.annotations.GuardedBy; import dalvik.system.BaseDexClassLoader; import dalvik.system.VMRuntime; -import libcore.io.Libcore; - import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -155,23 +152,12 @@ import java.util.Set; return; } - File realDexPath; - try { - // Secondary dex profiles are stored in the oat directory, next to the real dex file - // and have the same name with 'cur.prof' appended. We use the realpath because that - // is what installd is using when processing the dex file. - // NOTE: Keep in sync with installd. - realDexPath = new File(Libcore.os.realpath(dexPath)); - } catch (ErrnoException ex) { - Slog.e(TAG, "Failed to get the real path of secondary dex " + dexPath - + ":" + ex.getMessage()); - // Do not continue with registration if we could not retrieve the real path. - return; - } - + // Secondary dex profiles are stored in the oat directory, next to dex file + // and have the same name with 'cur.prof' appended. // NOTE: Keep this in sync with installd expectations. - File secondaryProfileDir = new File(realDexPath.getParent(), "oat"); - File secondaryProfile = new File(secondaryProfileDir, realDexPath.getName() + ".cur.prof"); + File dexPathFile = new File(dexPath); + File secondaryProfileDir = new File(dexPathFile.getParent(), "oat"); + File secondaryProfile = new File(secondaryProfileDir, dexPathFile.getName() + ".cur.prof"); // Create the profile if not already there. // Returns true if the file was created, false if the file already exists. diff --git a/android/app/DialogFragment.java b/android/app/DialogFragment.java index 7e0e4d82..a0fb6eeb 100644 --- a/android/app/DialogFragment.java +++ b/android/app/DialogFragment.java @@ -136,7 +136,10 @@ import java.io.PrintWriter; * * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java * embed} + * + * @deprecated Use {@link android.support.v4.app.DialogFragment} */ +@Deprecated public class DialogFragment extends Fragment implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { diff --git a/android/app/Fragment.java b/android/app/Fragment.java index 93773454..a92684b5 100644 --- a/android/app/Fragment.java +++ b/android/app/Fragment.java @@ -256,7 +256,10 @@ import java.lang.reflect.InvocationTargetException; * <p>After each call to this function, a new entry is on the stack, and * pressing back will pop it to return the user to whatever previous state * the activity UI was in. + * + * @deprecated Use {@link android.support.v4.app.Fragment} */ +@Deprecated public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListener { private static final ArrayMap<String, Class<?>> sClassMap = new ArrayMap<String, Class<?>>(); @@ -414,7 +417,10 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * State information that has been retrieved from a fragment instance * through {@link FragmentManager#saveFragmentInstanceState(Fragment) * FragmentManager.saveFragmentInstanceState}. + * + * @deprecated Use {@link android.support.v4.app.Fragment.SavedState} */ + @Deprecated public static class SavedState implements Parcelable { final Bundle mState; @@ -458,7 +464,10 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene /** * Thrown by {@link Fragment#instantiate(Context, String, Bundle)} when * there is an instantiation failure. + * + * @deprecated Use {@link android.support.v4.app.Fragment.InstantiationException} */ + @Deprecated static public class InstantiationException extends AndroidRuntimeException { public InstantiationException(String msg, Exception cause) { super(msg, cause); @@ -1031,7 +1040,10 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene /** * Return the LoaderManager for this fragment, creating it if needed. + * + * @deprecated Use {@link android.support.v4.app.Fragment#getLoaderManager()} */ + @Deprecated public LoaderManager getLoaderManager() { if (mLoaderManager != null) { return mLoaderManager; diff --git a/android/app/FragmentBreadCrumbs.java b/android/app/FragmentBreadCrumbs.java index d0aa0fd6..e3e47ae6 100644 --- a/android/app/FragmentBreadCrumbs.java +++ b/android/app/FragmentBreadCrumbs.java @@ -65,7 +65,10 @@ public class FragmentBreadCrumbs extends ViewGroup /** * Interface to intercept clicks on the bread crumbs. + * + * @deprecated This widget is no longer supported. */ + @Deprecated public interface OnBreadCrumbClickListener { /** * Called when a bread crumb is clicked. diff --git a/android/app/FragmentContainer.java b/android/app/FragmentContainer.java index f8836bc8..a1dd32ff 100644 --- a/android/app/FragmentContainer.java +++ b/android/app/FragmentContainer.java @@ -24,7 +24,10 @@ import android.view.View; /** * Callbacks to a {@link Fragment}'s container. + * + * @deprecated Use {@link android.support.v4.app.FragmentContainer} */ +@Deprecated public abstract class FragmentContainer { /** * Return the view with the given resource ID. May return {@code null} if the diff --git a/android/app/FragmentController.java b/android/app/FragmentController.java index cff94d8c..cbb58d40 100644 --- a/android/app/FragmentController.java +++ b/android/app/FragmentController.java @@ -37,7 +37,10 @@ import java.util.List; * <p> * It is the responsibility of the host to take care of the Fragment's lifecycle. * The methods provided by {@link FragmentController} are for that purpose. + * + * @deprecated Use {@link android.support.v4.app.FragmentController} */ +@Deprecated public class FragmentController { private final FragmentHostCallback<?> mHost; diff --git a/android/app/FragmentHostCallback.java b/android/app/FragmentHostCallback.java index 5ef23e63..1edc68ed 100644 --- a/android/app/FragmentHostCallback.java +++ b/android/app/FragmentHostCallback.java @@ -37,7 +37,10 @@ import java.io.PrintWriter; * Fragments may be hosted by any object; such as an {@link Activity}. In order to * host fragments, implement {@link FragmentHostCallback}, overriding the methods * applicable to the host. + * + * @deprecated Use {@link android.support.v4.app.FragmentHostCallback} */ +@Deprecated public abstract class FragmentHostCallback<E> extends FragmentContainer { private final Activity mActivity; final Context mContext; diff --git a/android/app/FragmentManager.java b/android/app/FragmentManager.java index 0d5cd021..12e60b87 100644 --- a/android/app/FragmentManager.java +++ b/android/app/FragmentManager.java @@ -74,7 +74,10 @@ import java.util.concurrent.CopyOnWriteArrayList; * {@link android.support.v4.app.FragmentActivity}. See the blog post * <a href="http://android-developers.blogspot.com/2011/03/fragments-for-all.html"> * Fragments For All</a> for more details. + * + * @deprecated Use {@link android.support.v4.app.FragmentManager} */ +@Deprecated public abstract class FragmentManager { /** * Representation of an entry on the fragment back stack, as created @@ -86,7 +89,10 @@ public abstract class FragmentManager { * <p>Note that you should never hold on to a BackStackEntry object; * the identifier as returned by {@link #getId} is the only thing that * will be persisted across activity instances. + * + * @deprecated Use {@link android.support.v4.app.FragmentManager.BackStackEntry} */ + @Deprecated public interface BackStackEntry { /** * Return the unique identifier for the entry. This is the only @@ -129,7 +135,10 @@ public abstract class FragmentManager { /** * Interface to watch for changes to the back stack. + * + * @deprecated Use {@link android.support.v4.app.FragmentManager.OnBackStackChangedListener} */ + @Deprecated public interface OnBackStackChangedListener { /** * Called whenever the contents of the back stack change. @@ -428,7 +437,10 @@ public abstract class FragmentManager { /** * Callback interface for listening to fragment state changes that happen * within a given FragmentManager. + * + * @deprecated Use {@link android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks} */ + @Deprecated public abstract static class FragmentLifecycleCallbacks { /** * Called right before the fragment's {@link Fragment#onAttach(Context)} method is called. diff --git a/android/app/FragmentManagerNonConfig.java b/android/app/FragmentManagerNonConfig.java index 50d3797d..beb1a15a 100644 --- a/android/app/FragmentManagerNonConfig.java +++ b/android/app/FragmentManagerNonConfig.java @@ -27,7 +27,10 @@ import java.util.List; * and passed to the state save and restore process for fragments in * {@link FragmentController#retainNonConfig()} and * {@link FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)}.</p> + * + * @deprecated Use {@link android.support.v4.app.FragmentManagerNonConfig} */ +@Deprecated public class FragmentManagerNonConfig { private final List<Fragment> mFragments; private final List<FragmentManagerNonConfig> mChildNonConfigs; diff --git a/android/app/FragmentTransaction.java b/android/app/FragmentTransaction.java index c910e903..0f4a7fb5 100644 --- a/android/app/FragmentTransaction.java +++ b/android/app/FragmentTransaction.java @@ -21,7 +21,10 @@ import java.lang.annotation.RetentionPolicy; * <a href="{@docRoot}guide/components/fragments.html">Fragments</a> developer * guide.</p> * </div> + * + * @deprecated Use {@link android.support.v4.app.FragmentTransaction} */ +@Deprecated public abstract class FragmentTransaction { /** * Calls {@link #add(int, Fragment, String)} with a 0 containerViewId. diff --git a/android/app/Instrumentation.java b/android/app/Instrumentation.java index e260967f..d49e11f4 100644 --- a/android/app/Instrumentation.java +++ b/android/app/Instrumentation.java @@ -17,6 +17,7 @@ package android.app; import android.annotation.IntDef; +import android.annotation.Nullable; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; @@ -418,22 +419,51 @@ public class Instrumentation { * different process. In addition, if the given Intent resolves to * multiple activities, instead of displaying a dialog for the user to * select an activity, an exception will be thrown. - * + * * <p>The function returns as soon as the activity goes idle following the * call to its {@link Activity#onCreate}. Generally this means it has gone * through the full initialization including {@link Activity#onResume} and * drawn and displayed its initial window. - * + * * @param intent Description of the activity to start. - * + * * @see Context#startActivity + * @see #startActivitySync(Intent, Bundle) */ public Activity startActivitySync(Intent intent) { + return startActivitySync(intent, null /* options */); + } + + /** + * Start a new activity and wait for it to begin running before returning. + * In addition to being synchronous, this method as some semantic + * differences from the standard {@link Context#startActivity} call: the + * activity component is resolved before talking with the activity manager + * (its class name is specified in the Intent that this method ultimately + * starts), and it does not allow you to start activities that run in a + * different process. In addition, if the given Intent resolves to + * multiple activities, instead of displaying a dialog for the user to + * select an activity, an exception will be thrown. + * + * <p>The function returns as soon as the activity goes idle following the + * call to its {@link Activity#onCreate}. Generally this means it has gone + * through the full initialization including {@link Activity#onResume} and + * drawn and displayed its initial window. + * + * @param intent Description of the activity to start. + * @param options Additional options for how the Activity should be started. + * May be null if there are no options. See {@link android.app.ActivityOptions} + * for how to build the Bundle supplied here; there are no supported definitions + * for building it manually. + * + * @see Context#startActivity(Intent, Bundle) + */ + public Activity startActivitySync(Intent intent, @Nullable Bundle options) { validateNotAppThread(); synchronized (mSync) { intent = new Intent(intent); - + ActivityInfo ai = intent.resolveActivityInfo( getTargetContext().getPackageManager(), 0); if (ai == null) { @@ -447,7 +477,7 @@ public class Instrumentation { + myProc + " resolved to different process " + ai.processName + ": " + intent); } - + intent.setComponent(new ComponentName( ai.applicationInfo.packageName, ai.name)); final ActivityWaiter aw = new ActivityWaiter(intent); @@ -457,7 +487,7 @@ public class Instrumentation { } mWaitingActivities.add(aw); - getTargetContext().startActivity(intent); + getTargetContext().startActivity(intent, options); do { try { @@ -465,7 +495,7 @@ public class Instrumentation { } catch (InterruptedException e) { } } while (mWaitingActivities.contains(aw)); - + return aw.activity; } } diff --git a/android/app/ListFragment.java b/android/app/ListFragment.java index 0b96d84d..90b77b39 100644 --- a/android/app/ListFragment.java +++ b/android/app/ListFragment.java @@ -144,7 +144,10 @@ import android.widget.TextView; * * @see #setListAdapter * @see android.widget.ListView + * + * @deprecated Use {@link android.support.v4.app.ListFragment} */ +@Deprecated public class ListFragment extends Fragment { final private Handler mHandler = new Handler(); diff --git a/android/app/LoaderManager.java b/android/app/LoaderManager.java index 56dfc589..7969684a 100644 --- a/android/app/LoaderManager.java +++ b/android/app/LoaderManager.java @@ -54,11 +54,17 @@ import java.lang.reflect.Modifier; * <p>For more information about using loaders, read the * <a href="{@docRoot}guide/topics/fundamentals/loaders.html">Loaders</a> developer guide.</p> * </div> + * + * @deprecated Use {@link android.support.v4.app.LoaderManager} */ +@Deprecated public abstract class LoaderManager { /** * Callback interface for a client to interact with the manager. + * + * @deprecated Use {@link android.support.v4.app.LoaderManager.LoaderCallbacks} */ + @Deprecated public interface LoaderCallbacks<D> { /** * Instantiate and return a new Loader for the given ID. diff --git a/android/app/Notification.java b/android/app/Notification.java index 8226e0fb..d5d95fb8 100644 --- a/android/app/Notification.java +++ b/android/app/Notification.java @@ -22,6 +22,7 @@ import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; @@ -3900,7 +3901,7 @@ public class Notification implements Parcelable final Bundle ex = mN.extras; updateBackgroundColor(contentView); bindNotificationHeader(contentView, p.ambient); - bindLargeIcon(contentView); + bindLargeIcon(contentView, p.hideLargeIcon, p.alwaysShowReply); boolean showProgress = handleProgressBar(p.hasProgress, contentView, ex); if (p.title != null) { contentView.setViewVisibility(R.id.title, View.VISIBLE); @@ -4110,11 +4111,13 @@ public class Notification implements Parcelable } } - private void bindLargeIcon(RemoteViews contentView) { + private void bindLargeIcon(RemoteViews contentView, boolean hideLargeIcon, + boolean alwaysShowReply) { if (mN.mLargeIcon == null && mN.largeIcon != null) { mN.mLargeIcon = Icon.createWithBitmap(mN.largeIcon); } - if (mN.mLargeIcon != null) { + boolean showLargeIcon = mN.mLargeIcon != null && !hideLargeIcon; + if (showLargeIcon) { contentView.setViewVisibility(R.id.right_icon, View.VISIBLE); contentView.setImageViewIcon(R.id.right_icon, mN.mLargeIcon); processLargeLegacyIcon(mN.mLargeIcon, contentView); @@ -4122,32 +4125,45 @@ public class Notification implements Parcelable contentView.setViewLayoutMarginEndDimen(R.id.line1, endMargin); contentView.setViewLayoutMarginEndDimen(R.id.text, endMargin); contentView.setViewLayoutMarginEndDimen(R.id.progress, endMargin); - // Bind the reply action - Action action = findReplyAction(); - contentView.setViewVisibility(R.id.reply_icon_action, action != null - ? View.VISIBLE - : View.GONE); - - if (action != null) { - int contrastColor = resolveContrastColor(); + } + // Bind the reply action + Action action = findReplyAction(); + + boolean actionVisible = action != null && (showLargeIcon || alwaysShowReply); + int replyId = showLargeIcon ? R.id.reply_icon_action : R.id.right_icon; + if (actionVisible) { + // We're only showing the icon as big if we're hiding the large icon + int contrastColor = resolveContrastColor(); + int iconColor; + if (showLargeIcon) { contentView.setDrawableTint(R.id.reply_icon_action, true /* targetBackground */, contrastColor, PorterDuff.Mode.SRC_ATOP); - int iconColor = NotificationColorUtil.isColorLight(contrastColor) - ? Color.BLACK : Color.WHITE; - contentView.setDrawableTint(R.id.reply_icon_action, - false /* targetBackground */, - iconColor, PorterDuff.Mode.SRC_ATOP); contentView.setOnClickPendingIntent(R.id.right_icon, action.actionIntent); - contentView.setOnClickPendingIntent(R.id.reply_icon_action, - action.actionIntent); contentView.setRemoteInputs(R.id.right_icon, action.mRemoteInputs); - contentView.setRemoteInputs(R.id.reply_icon_action, action.mRemoteInputs); - + iconColor = NotificationColorUtil.isColorLight(contrastColor) + ? Color.BLACK : Color.WHITE; + } else { + contentView.setImageViewResource(R.id.right_icon, + R.drawable.ic_reply_notification_large); + contentView.setViewVisibility(R.id.right_icon, View.VISIBLE); + iconColor = contrastColor; } + contentView.setDrawableTint(replyId, + false /* targetBackground */, + iconColor, + PorterDuff.Mode.SRC_ATOP); + contentView.setOnClickPendingIntent(replyId, + action.actionIntent); + contentView.setRemoteInputs(replyId, action.mRemoteInputs); + } else { + contentView.setRemoteInputs(R.id.right_icon, null); } - contentView.setViewVisibility(R.id.right_icon_container, mN.mLargeIcon != null + contentView.setViewVisibility(R.id.reply_icon_action, actionVisible && showLargeIcon + ? View.VISIBLE + : View.GONE); + contentView.setViewVisibility(R.id.right_icon_container, actionVisible || showLargeIcon ? View.VISIBLE : View.GONE); } @@ -6055,18 +6071,12 @@ public class Notification implements Parcelable protected void restoreFromExtras(Bundle extras) { super.restoreFromExtras(extras); - mMessages.clear(); - mHistoricMessages.clear(); mUserDisplayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME); mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE); Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES); - if (messages != null && messages instanceof Parcelable[]) { - mMessages = Message.getMessagesFromBundleArray(messages); - } + mMessages = Message.getMessagesFromBundleArray(messages); Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES); - if (histMessages != null && histMessages instanceof Parcelable[]) { - mHistoricMessages = Message.getMessagesFromBundleArray(histMessages); - } + mHistoricMessages = Message.getMessagesFromBundleArray(histMessages); } /** @@ -6074,38 +6084,34 @@ public class Notification implements Parcelable */ @Override public RemoteViews makeContentView(boolean increasedHeight) { - if (!increasedHeight) { - Message m = findLatestIncomingMessage(); - CharSequence title = mConversationTitle != null - ? mConversationTitle - : (m == null) ? null : m.mSender; - CharSequence text = (m == null) - ? null - : mConversationTitle != null ? makeMessageLine(m, mBuilder) : m.mText; - - return mBuilder.applyStandardTemplate(mBuilder.getBaseLayoutResource(), - mBuilder.mParams.reset().hasProgress(false).title(title).text(text)); - } else { - mBuilder.mOriginalActions = mBuilder.mActions; - mBuilder.mActions = new ArrayList<>(); - RemoteViews remoteViews = makeBigContentView(); - mBuilder.mActions = mBuilder.mOriginalActions; - mBuilder.mOriginalActions = null; - return remoteViews; - } + mBuilder.mOriginalActions = mBuilder.mActions; + mBuilder.mActions = new ArrayList<>(); + RemoteViews remoteViews = makeBigContentView(); + mBuilder.mActions = mBuilder.mOriginalActions; + mBuilder.mOriginalActions = null; + return remoteViews; } private Message findLatestIncomingMessage() { - for (int i = mMessages.size() - 1; i >= 0; i--) { - Message m = mMessages.get(i); + return findLatestIncomingMessage(mMessages); + } + + /** + * @hide + */ + @Nullable + public static Message findLatestIncomingMessage( + List<Message> messages) { + for (int i = messages.size() - 1; i >= 0; i--) { + Message m = messages.get(i); // Incoming messages have a non-empty sender. if (!TextUtils.isEmpty(m.mSender)) { return m; } } - if (!mMessages.isEmpty()) { + if (!messages.isEmpty()) { // No incoming messages, fall back to outgoing message - return mMessages.get(mMessages.size() - 1); + return messages.get(messages.size() - 1); } return null; } @@ -6115,118 +6121,82 @@ public class Notification implements Parcelable */ @Override public RemoteViews makeBigContentView() { - CharSequence title = !TextUtils.isEmpty(super.mBigContentTitle) + CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle) ? super.mBigContentTitle : mConversationTitle; - boolean hasTitle = !TextUtils.isEmpty(title); - - if (mMessages.size() == 1) { - // Special case for a single message: Use the big text style - // so the collapsed and expanded versions match nicely. - CharSequence bigTitle; - CharSequence text; - if (hasTitle) { - bigTitle = title; - text = makeMessageLine(mMessages.get(0), mBuilder); - } else { - bigTitle = mMessages.get(0).mSender; - text = mMessages.get(0).mText; - } - RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( - mBuilder.getBigTextLayoutResource(), - mBuilder.mParams.reset().hasProgress(false).title(bigTitle).text(null)); - BigTextStyle.applyBigTextContentView(mBuilder, contentView, text); - return contentView; - } - + boolean isOneToOne = TextUtils.isEmpty(conversationTitle); + if (isOneToOne) { + // Let's add the conversationTitle in case we didn't have one before and all + // messages are from the same sender + conversationTitle = createConversationTitleFromMessages(); + } else if (hasOnlyWhiteSpaceSenders()) { + isOneToOne = true; + } + boolean hasTitle = !TextUtils.isEmpty(conversationTitle); RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( mBuilder.getMessagingLayoutResource(), - mBuilder.mParams.reset().hasProgress(false).title(title).text(null)); - - int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3, - R.id.inbox_text4, R.id.inbox_text5, R.id.inbox_text6}; - - // Make sure all rows are gone in case we reuse a view. - for (int rowId : rowIds) { - contentView.setViewVisibility(rowId, View.GONE); - } + mBuilder.mParams.reset().hasProgress(false).title(conversationTitle).text(null) + .hideLargeIcon(isOneToOne).alwaysShowReply(true)); + addExtras(mBuilder.mN.extras); + contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor", + mBuilder.resolveContrastColor()); + contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon", + mBuilder.mN.mLargeIcon); + contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsOneToOne", + isOneToOne); + contentView.setBundle(R.id.status_bar_latest_event_content, "setData", + mBuilder.mN.extras); + return contentView; + } - int i=0; - contentView.setViewLayoutMarginBottomDimen(R.id.line1, - hasTitle ? R.dimen.notification_messaging_spacing : 0); - contentView.setInt(R.id.notification_messaging, "setNumIndentLines", - !mBuilder.mN.hasLargeIcon() ? 0 : (hasTitle ? 1 : 2)); - - int contractedChildId = View.NO_ID; - Message contractedMessage = findLatestIncomingMessage(); - int firstHistoricMessage = Math.max(0, mHistoricMessages.size() - - (rowIds.length - mMessages.size())); - while (firstHistoricMessage + i < mHistoricMessages.size() && i < rowIds.length) { - Message m = mHistoricMessages.get(firstHistoricMessage + i); - int rowId = rowIds[i]; - - contentView.setTextViewText(rowId, makeMessageLine(m, mBuilder)); - - if (contractedMessage == m) { - contractedChildId = rowId; + private boolean hasOnlyWhiteSpaceSenders() { + for (int i = 0; i < mMessages.size(); i++) { + Message m = mMessages.get(i); + CharSequence sender = m.getSender(); + if (!isWhiteSpace(sender)) { + return false; } - - i++; } + return true; + } - int firstMessage = Math.max(0, mMessages.size() - rowIds.length); - while (firstMessage + i < mMessages.size() && i < rowIds.length) { - Message m = mMessages.get(firstMessage + i); - int rowId = rowIds[i]; - - contentView.setViewVisibility(rowId, View.VISIBLE); - contentView.setTextViewText(rowId, mBuilder.processTextSpans( - makeMessageLine(m, mBuilder))); - mBuilder.setTextViewColorSecondary(contentView, rowId); - - if (contractedMessage == m) { - contractedChildId = rowId; - } - - i++; + private boolean isWhiteSpace(CharSequence sender) { + if (TextUtils.isEmpty(sender)) { + return true; } - // Clear the remaining views for reapply. Ensures that historic message views can - // reliably be identified as being GONE and having non-null text. - while (i < rowIds.length) { - int rowId = rowIds[i]; - contentView.setTextViewText(rowId, null); - i++; + if (sender.toString().matches("^\\s*$")) { + return true; } - - // Record this here to allow transformation between the contracted and expanded views. - contentView.setInt(R.id.notification_messaging, "setContractedChildId", - contractedChildId); - return contentView; + // Let's check if we only have 0 whitespace chars. Some apps did this as a workaround + // For the presentation that we had. + for (int i = 0; i < sender.length(); i++) { + char c = sender.charAt(i); + if (c != '\u200B') { + return false; + } + } + return true; } - private CharSequence makeMessageLine(Message m, Builder builder) { - BidiFormatter bidi = BidiFormatter.getInstance(); - SpannableStringBuilder sb = new SpannableStringBuilder(); - boolean colorize = builder.isColorized(); - TextAppearanceSpan colorSpan; - CharSequence messageName; - if (TextUtils.isEmpty(m.mSender)) { - CharSequence replyName = mUserDisplayName == null ? "" : mUserDisplayName; - sb.append(bidi.unicodeWrap(replyName), - makeFontColorSpan(colorize - ? builder.getPrimaryTextColor() - : mBuilder.resolveContrastColor()), - 0 /* flags */); - } else { - sb.append(bidi.unicodeWrap(m.mSender), - makeFontColorSpan(colorize - ? builder.getPrimaryTextColor() - : Color.BLACK), - 0 /* flags */); + private CharSequence createConversationTitleFromMessages() { + ArraySet<CharSequence> names = new ArraySet<>(); + for (int i = 0; i < mMessages.size(); i++) { + Message m = mMessages.get(i); + CharSequence sender = m.getSender(); + if (sender != null) { + names.add(sender); + } + } + SpannableStringBuilder title = new SpannableStringBuilder(); + int size = names.size(); + for (int i = 0; i < size; i++) { + CharSequence name = names.valueAt(i); + if (!TextUtils.isEmpty(title)) { + title.append(", "); + } + title.append(BidiFormatter.getInstance().unicodeWrap(name)); } - CharSequence text = m.mText == null ? "" : m.mText; - sb.append(" ").append(bidi.unicodeWrap(text)); - return sb; + return title; } /** @@ -6234,19 +6204,9 @@ public class Notification implements Parcelable */ @Override public RemoteViews makeHeadsUpContentView(boolean increasedHeight) { - if (increasedHeight) { - return makeBigContentView(); - } - Message m = findLatestIncomingMessage(); - CharSequence title = mConversationTitle != null - ? mConversationTitle - : (m == null) ? null : m.mSender; - CharSequence text = (m == null) - ? null - : mConversationTitle != null ? makeMessageLine(m, mBuilder) : m.mText; - - return mBuilder.applyStandardTemplateWithActions(mBuilder.getBigBaseLayoutResource(), - mBuilder.mParams.reset().hasProgress(false).title(title).text(text)); + RemoteViews remoteViews = makeBigContentView(); + remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1); + return remoteViews; } private static TextAppearanceSpan makeFontColorSpan(int color) { @@ -6394,7 +6354,15 @@ public class Notification implements Parcelable return bundles; } - static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) { + /** + * @return A list of messages read from the bundles. + * + * @hide + */ + public static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) { + if (bundles == null) { + return new ArrayList<>(); + } List<Message> messages = new ArrayList<>(bundles.length); for (int i = 0; i < bundles.length; i++) { if (bundles[i] instanceof Bundle) { @@ -8487,6 +8455,8 @@ public class Notification implements Parcelable boolean ambient = false; CharSequence title; CharSequence text; + boolean hideLargeIcon; + public boolean alwaysShowReply; final StandardTemplateParams reset() { hasProgress = true; @@ -8511,6 +8481,16 @@ public class Notification implements Parcelable return this; } + final StandardTemplateParams alwaysShowReply(boolean alwaysShowReply) { + this.alwaysShowReply = alwaysShowReply; + return this; + } + + final StandardTemplateParams hideLargeIcon(boolean hideLargeIcon) { + this.hideLargeIcon = hideLargeIcon; + return this; + } + final StandardTemplateParams ambient(boolean ambient) { Preconditions.checkState(title == null && text == null, "must set ambient before text"); this.ambient = ambient; @@ -8527,7 +8507,6 @@ public class Notification implements Parcelable text = extras.getCharSequence(EXTRA_TEXT); } this.text = b.processLegacyText(text, ambient); - return this; } } diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java index a52dc1e4..f931589b 100644 --- a/android/app/NotificationManager.java +++ b/android/app/NotificationManager.java @@ -758,10 +758,10 @@ public class NotificationManager { } /** - * Checks the ability to read/modify notification do not disturb policy for the calling package. + * Checks the ability to modify notification do not disturb policy for the calling package. * * <p> - * Returns true if the calling package can read/modify notification policy. + * Returns true if the calling package can modify notification policy. * * <p> * Apps can request policy access by sending the user to the activity that matches the system @@ -839,8 +839,6 @@ public class NotificationManager { * Gets the current notification policy. * * <p> - * Only available if policy access is granted to this package. - * See {@link #isNotificationPolicyAccessGranted}. */ public Policy getNotificationPolicy() { INotificationManager service = getService(); diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java index 50f1f364..e48946f2 100644 --- a/android/app/SystemServiceRegistry.java +++ b/android/app/SystemServiceRegistry.java @@ -41,6 +41,8 @@ import android.content.pm.IShortcutService; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ShortcutManager; +import android.content.pm.crossprofile.CrossProfileApps; +import android.content.pm.crossprofile.ICrossProfileApps; import android.content.res.Resources; import android.hardware.ConsumerIrManager; import android.hardware.ISerialManager; @@ -81,6 +83,7 @@ import android.net.INetworkPolicyManager; import android.net.IpSecManager; import android.net.NetworkPolicyManager; import android.net.NetworkScoreManager; +import android.net.NetworkWatchlistManager; import android.net.lowpan.ILowpanManager; import android.net.lowpan.LowpanManager; import android.net.nsd.INsdManager; @@ -134,6 +137,7 @@ import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.telephony.euicc.EuiccManager; import android.util.Log; +import android.util.StatsManager; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.WindowManager; @@ -150,6 +154,7 @@ import com.android.internal.app.IAppOpsService; import com.android.internal.app.IBatteryStats; import com.android.internal.app.ISoundTriggerService; import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.net.INetworkWatchlistManager; import com.android.internal.os.IDropBoxManagerService; import com.android.internal.policy.PhoneLayoutInflater; @@ -304,14 +309,14 @@ final class SystemServiceRegistry { }}); registerService(Context.BATTERY_SERVICE, BatteryManager.class, - new StaticServiceFetcher<BatteryManager>() { + new CachedServiceFetcher<BatteryManager>() { @Override - public BatteryManager createService() throws ServiceNotFoundException { + public BatteryManager createService(ContextImpl ctx) throws ServiceNotFoundException { IBatteryStats stats = IBatteryStats.Stub.asInterface( ServiceManager.getServiceOrThrow(BatteryStats.SERVICE_NAME)); IBatteryPropertiesRegistrar registrar = IBatteryPropertiesRegistrar.Stub .asInterface(ServiceManager.getServiceOrThrow("batteryproperties")); - return new BatteryManager(stats, registrar); + return new BatteryManager(ctx, stats, registrar); }}); registerService(Context.NFC_SERVICE, NfcManager.class, @@ -448,6 +453,13 @@ final class SystemServiceRegistry { ctx.mMainThread.getHandler().getLooper()); }}); + registerService(Context.STATS_MANAGER, StatsManager.class, + new StaticServiceFetcher<StatsManager>() { + @Override + public StatsManager createService() throws ServiceNotFoundException { + return new StatsManager(); + }}); + registerService(Context.STATUS_BAR_SERVICE, StatusBarManager.class, new CachedServiceFetcher<StatusBarManager>() { @Override @@ -862,6 +874,17 @@ final class SystemServiceRegistry { return new ShortcutManager(ctx, IShortcutService.Stub.asInterface(b)); }}); + registerService(Context.NETWORK_WATCHLIST_SERVICE, NetworkWatchlistManager.class, + new CachedServiceFetcher<NetworkWatchlistManager>() { + @Override + public NetworkWatchlistManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = + ServiceManager.getServiceOrThrow(Context.NETWORK_WATCHLIST_SERVICE); + return new NetworkWatchlistManager(ctx, + INetworkWatchlistManager.Stub.asInterface(b)); + }}); + registerService(Context.SYSTEM_HEALTH_SERVICE, SystemHealthManager.class, new CachedServiceFetcher<SystemHealthManager>() { @Override @@ -909,6 +932,18 @@ final class SystemServiceRegistry { public RulesManager createService(ContextImpl ctx) { return new RulesManager(ctx.getOuterContext()); }}); + + registerService(Context.CROSS_PROFILE_APPS_SERVICE, CrossProfileApps.class, + new CachedServiceFetcher<CrossProfileApps>() { + @Override + public CrossProfileApps createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getServiceOrThrow( + Context.CROSS_PROFILE_APPS_SERVICE); + return new CrossProfileApps(ctx.getOuterContext(), + ICrossProfileApps.Stub.asInterface(b)); + } + }); } /** diff --git a/android/app/TimePickerDialog.java b/android/app/TimePickerDialog.java index 0f006b66..8686944b 100644 --- a/android/app/TimePickerDialog.java +++ b/android/app/TimePickerDialog.java @@ -152,6 +152,9 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, public void onClick(View view) { if (mTimePicker.validateInput()) { TimePickerDialog.this.onClick(TimePickerDialog.this, BUTTON_POSITIVE); + // Clearing focus forces the dialog to commit any pending + // changes, e.g. typed text in a NumberPicker. + mTimePicker.clearFocus(); dismiss(); } } diff --git a/android/app/VrManager.java b/android/app/VrManager.java index 5c6ffa39..392387a9 100644 --- a/android/app/VrManager.java +++ b/android/app/VrManager.java @@ -198,4 +198,20 @@ public class VrManager { e.rethrowFromSystemServer(); } } + + /** + * Sets the current standby status of the VR device. Standby mode is only used on standalone vr + * devices. Standby mode is a deep sleep state where it's appropriate to turn off vr mode. + * + * @param standby True if the device is entering standby, false if it's exiting standby. + * @hide + */ + @RequiresPermission(android.Manifest.permission.ACCESS_VR_MANAGER) + public void setStandbyEnabled(boolean standby) { + try { + mService.setStandbyEnabled(standby); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } } diff --git a/android/app/WindowConfiguration.java b/android/app/WindowConfiguration.java index de27b4fd..2c1fad1c 100644 --- a/android/app/WindowConfiguration.java +++ b/android/app/WindowConfiguration.java @@ -500,15 +500,12 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu * @hide */ public boolean supportSplitScreenWindowingMode() { - return supportSplitScreenWindowingMode(mWindowingMode, mActivityType); + return supportSplitScreenWindowingMode(mActivityType); } /** @hide */ - public static boolean supportSplitScreenWindowingMode(int windowingMode, int activityType) { - if (activityType == ACTIVITY_TYPE_ASSISTANT) { - return false; - } - return windowingMode != WINDOWING_MODE_FREEFORM && windowingMode != WINDOWING_MODE_PINNED; + public static boolean supportSplitScreenWindowingMode(int activityType) { + return activityType != ACTIVITY_TYPE_ASSISTANT; } /** @hide */ diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java index 772c6d60..f0226b7e 100644 --- a/android/app/admin/DevicePolicyManager.java +++ b/android/app/admin/DevicePolicyManager.java @@ -3246,6 +3246,7 @@ public class DevicePolicyManager { * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} */ public void wipeData(int flags) { + throwIfParentInstance("wipeData"); final String wipeReasonForUser = mContext.getString( R.string.work_profile_deleted_description_dpm_wipe); wipeDataInternal(flags, wipeReasonForUser); @@ -3270,6 +3271,7 @@ public class DevicePolicyManager { * @throws IllegalArgumentException if the input reason string is null or empty. */ public void wipeDataWithReason(int flags, @NonNull CharSequence reason) { + throwIfParentInstance("wipeDataWithReason"); Preconditions.checkNotNull(reason, "CharSequence is null"); wipeDataInternal(flags, reason.toString()); } @@ -3283,7 +3285,6 @@ public class DevicePolicyManager { * @hide */ private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser) { - throwIfParentInstance("wipeDataWithReason"); if (mService != null) { try { mService.wipeDataWithReason(flags, wipeReasonForUser); @@ -6096,8 +6097,8 @@ public class DevicePolicyManager { /** * Flag used by {@link #createAndManageUser} to specify that the user should be created - * ephemeral. - * @hide + * ephemeral. Ephemeral users will be removed after switching to another user or rebooting the + * device. */ public static final int MAKE_USER_EPHEMERAL = 0x0002; diff --git a/android/app/job/JobInfo.java b/android/app/job/JobInfo.java index b640bd5b..530d84b4 100644 --- a/android/app/job/JobInfo.java +++ b/android/app/job/JobInfo.java @@ -16,6 +16,12 @@ package android.app.job; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.util.TimeUtils.formatDuration; import android.annotation.BytesLong; @@ -25,6 +31,8 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.content.ClipData; import android.content.ComponentName; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; import android.net.Uri; import android.os.BaseBundle; import android.os.Bundle; @@ -56,6 +64,7 @@ public class JobInfo implements Parcelable { NETWORK_TYPE_ANY, NETWORK_TYPE_UNMETERED, NETWORK_TYPE_NOT_ROAMING, + NETWORK_TYPE_CELLULAR, NETWORK_TYPE_METERED, }) @Retention(RetentionPolicy.SOURCE) @@ -69,8 +78,21 @@ public class JobInfo implements Parcelable { public static final int NETWORK_TYPE_UNMETERED = 2; /** This job requires network connectivity that is not roaming. */ public static final int NETWORK_TYPE_NOT_ROAMING = 3; - /** This job requires metered connectivity such as most cellular data networks. */ - public static final int NETWORK_TYPE_METERED = 4; + /** This job requires network connectivity that is a cellular network. */ + public static final int NETWORK_TYPE_CELLULAR = 4; + + /** + * This job requires metered connectivity such as most cellular data + * networks. + * + * @deprecated Cellular networks may be unmetered, or Wi-Fi networks may be + * metered, so this isn't a good way of selecting a specific + * transport. Instead, use {@link #NETWORK_TYPE_CELLULAR} or + * {@link android.net.NetworkRequest.Builder#addTransportType(int)} + * if your job requires a specific network transport. + */ + @Deprecated + public static final int NETWORK_TYPE_METERED = NETWORK_TYPE_CELLULAR; /** Sentinel value indicating that bytes are unknown. */ public static final int NETWORK_BYTES_UNKNOWN = -1; @@ -253,7 +275,7 @@ public class JobInfo implements Parcelable { private final long triggerContentMaxDelay; private final boolean hasEarlyConstraint; private final boolean hasLateConstraint; - private final int networkType; + private final NetworkRequest networkRequest; private final long networkBytes; private final long minLatencyMillis; private final long maxExecutionDelayMillis; @@ -385,10 +407,37 @@ public class JobInfo implements Parcelable { } /** - * The kind of connectivity requirements that the job has. + * Return the basic description of the kind of network this job requires. + * + * @deprecated This method attempts to map {@link #getRequiredNetwork()} + * into the set of simple constants, which results in a loss of + * fidelity. Callers should move to using + * {@link #getRequiredNetwork()} directly. + * @see Builder#setRequiredNetworkType(int) */ + @Deprecated public @NetworkType int getNetworkType() { - return networkType; + if (networkRequest == null) { + return NETWORK_TYPE_NONE; + } else if (networkRequest.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)) { + return NETWORK_TYPE_UNMETERED; + } else if (networkRequest.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING)) { + return NETWORK_TYPE_NOT_ROAMING; + } else if (networkRequest.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) { + return NETWORK_TYPE_CELLULAR; + } else { + return NETWORK_TYPE_ANY; + } + } + + /** + * Return the detailed description of the kind of network this job requires, + * or {@code null} if no specific kind of network is required. + * + * @see Builder#setRequiredNetwork(NetworkRequest) + */ + public @Nullable NetworkRequest getRequiredNetwork() { + return networkRequest; } /** @@ -438,8 +487,7 @@ public class JobInfo implements Parcelable { * job does not recur periodically. */ public long getIntervalMillis() { - final long minInterval = getMinPeriodMillis(); - return intervalMillis >= minInterval ? intervalMillis : minInterval; + return intervalMillis; } /** @@ -447,10 +495,7 @@ public class JobInfo implements Parcelable { * execute at any time in a window of flex length at the end of the period. */ public long getFlexMillis() { - long interval = getIntervalMillis(); - long percentClamp = 5 * interval / 100; - long clampedFlex = Math.max(flexMillis, Math.max(percentClamp, getMinFlexMillis())); - return clampedFlex <= interval ? clampedFlex : interval; + return flexMillis; } /** @@ -459,8 +504,7 @@ public class JobInfo implements Parcelable { * to 30 seconds, minimum is currently 10 seconds. */ public long getInitialBackoffMillis() { - final long minBackoff = getMinBackoffMillis(); - return initialBackoffMillis >= minBackoff ? initialBackoffMillis : minBackoff; + return initialBackoffMillis; } /** @@ -538,7 +582,7 @@ public class JobInfo implements Parcelable { if (hasLateConstraint != j.hasLateConstraint) { return false; } - if (networkType != j.networkType) { + if (!Objects.equals(networkRequest, j.networkRequest)) { return false; } if (networkBytes != j.networkBytes) { @@ -601,7 +645,9 @@ public class JobInfo implements Parcelable { hashCode = 31 * hashCode + Long.hashCode(triggerContentMaxDelay); hashCode = 31 * hashCode + Boolean.hashCode(hasEarlyConstraint); hashCode = 31 * hashCode + Boolean.hashCode(hasLateConstraint); - hashCode = 31 * hashCode + networkType; + if (networkRequest != null) { + hashCode = 31 * hashCode + networkRequest.hashCode(); + } hashCode = 31 * hashCode + Long.hashCode(networkBytes); hashCode = 31 * hashCode + Long.hashCode(minLatencyMillis); hashCode = 31 * hashCode + Long.hashCode(maxExecutionDelayMillis); @@ -632,7 +678,11 @@ public class JobInfo implements Parcelable { triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR); triggerContentUpdateDelay = in.readLong(); triggerContentMaxDelay = in.readLong(); - networkType = in.readInt(); + if (in.readInt() != 0) { + networkRequest = NetworkRequest.CREATOR.createFromParcel(in); + } else { + networkRequest = null; + } networkBytes = in.readLong(); minLatencyMillis = in.readLong(); maxExecutionDelayMillis = in.readLong(); @@ -661,7 +711,7 @@ public class JobInfo implements Parcelable { : null; triggerContentUpdateDelay = b.mTriggerContentUpdateDelay; triggerContentMaxDelay = b.mTriggerContentMaxDelay; - networkType = b.mNetworkType; + networkRequest = b.mNetworkRequest; networkBytes = b.mNetworkBytes; minLatencyMillis = b.mMinLatencyMillis; maxExecutionDelayMillis = b.mMaxExecutionDelayMillis; @@ -699,7 +749,12 @@ public class JobInfo implements Parcelable { out.writeTypedArray(triggerContentUris, flags); out.writeLong(triggerContentUpdateDelay); out.writeLong(triggerContentMaxDelay); - out.writeInt(networkType); + if (networkRequest != null) { + out.writeInt(1); + networkRequest.writeToParcel(out, flags); + } else { + out.writeInt(0); + } out.writeLong(networkBytes); out.writeLong(minLatencyMillis); out.writeLong(maxExecutionDelayMillis); @@ -833,7 +888,7 @@ public class JobInfo implements Parcelable { private int mFlags; // Requirements. private int mConstraintFlags; - private int mNetworkType; + private NetworkRequest mNetworkRequest; private long mNetworkBytes = NETWORK_BYTES_UNKNOWN; private ArrayList<TriggerContentUri> mTriggerContentUris; private long mTriggerContentUpdateDelay = -1; @@ -934,24 +989,84 @@ public class JobInfo implements Parcelable { } /** - * Set some description of the kind of network type your job needs to - * have. Not calling this function means the network is not necessary, - * as the default is {@link #NETWORK_TYPE_NONE}. Bear in mind that - * calling this function defines network as a strict requirement for - * your job. If the network requested is not available your job will - * never run. See {@link #setOverrideDeadline(long)} to change this - * behaviour. + * Set basic description of the kind of network your job requires. If + * you need more precise control over network capabilities, see + * {@link #setRequiredNetwork(NetworkRequest)}. + * <p> + * If your job doesn't need a network connection, you don't need to call + * this method, as the default value is {@link #NETWORK_TYPE_NONE}. + * <p> + * Calling this method defines network as a strict requirement for your + * job. If the network requested is not available your job will never + * run. See {@link #setOverrideDeadline(long)} to change this behavior. + * Calling this method will override any requirements previously defined + * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only + * want to call one of these methods. * <p class="note"> - * Note: When your job executes in + * When your job executes in * {@link JobService#onStartJob(JobParameters)}, be sure to use the * specific network returned by {@link JobParameters#getNetwork()}, * otherwise you'll use the default network which may not meet this * constraint. * + * @see #setRequiredNetwork(NetworkRequest) + * @see JobInfo#getNetworkType() * @see JobParameters#getNetwork() */ public Builder setRequiredNetworkType(@NetworkType int networkType) { - mNetworkType = networkType; + if (networkType == NETWORK_TYPE_NONE) { + return setRequiredNetwork(null); + } else { + final NetworkRequest.Builder builder = new NetworkRequest.Builder(); + + // All types require validated Internet + builder.addCapability(NET_CAPABILITY_INTERNET); + builder.addCapability(NET_CAPABILITY_VALIDATED); + builder.removeCapability(NET_CAPABILITY_NOT_VPN); + + if (networkType == NETWORK_TYPE_ANY) { + // No other capabilities + } else if (networkType == NETWORK_TYPE_UNMETERED) { + builder.addCapability(NET_CAPABILITY_NOT_METERED); + } else if (networkType == NETWORK_TYPE_NOT_ROAMING) { + builder.addCapability(NET_CAPABILITY_NOT_ROAMING); + } else if (networkType == NETWORK_TYPE_CELLULAR) { + builder.addTransportType(TRANSPORT_CELLULAR); + } + + return setRequiredNetwork(builder.build()); + } + } + + /** + * Set detailed description of the kind of network your job requires. + * <p> + * If your job doesn't need a network connection, you don't need to call + * this method, as the default is {@code null}. + * <p> + * Calling this method defines network as a strict requirement for your + * job. If the network requested is not available your job will never + * run. See {@link #setOverrideDeadline(long)} to change this behavior. + * Calling this method will override any requirements previously defined + * by {@link #setRequiredNetworkType(int)}; you typically only want to + * call one of these methods. + * <p class="note"> + * When your job executes in + * {@link JobService#onStartJob(JobParameters)}, be sure to use the + * specific network returned by {@link JobParameters#getNetwork()}, + * otherwise you'll use the default network which may not meet this + * constraint. + * + * @param networkRequest The detailed description of the kind of network + * this job requires, or {@code null} if no specific kind of + * network is required. Defining a {@link NetworkSpecifier} + * is only supported for jobs that aren't persisted. + * @see #setRequiredNetworkType(int) + * @see JobInfo#getRequiredNetwork() + * @see JobParameters#getNetwork() + */ + public Builder setRequiredNetwork(@Nullable NetworkRequest networkRequest) { + mNetworkRequest = networkRequest; return this; } @@ -1140,6 +1255,21 @@ public class JobInfo implements Parcelable { * higher. */ public Builder setPeriodic(long intervalMillis, long flexMillis) { + final long minPeriod = getMinPeriodMillis(); + if (intervalMillis < minPeriod) { + Log.w(TAG, "Requested interval " + formatDuration(intervalMillis) + " for job " + + mJobId + " is too small; raising to " + formatDuration(minPeriod)); + intervalMillis = minPeriod; + } + + final long percentClamp = 5 * intervalMillis / 100; + final long minFlex = Math.max(percentClamp, getMinFlexMillis()); + if (flexMillis < minFlex) { + Log.w(TAG, "Requested flex " + formatDuration(flexMillis) + " for job " + mJobId + + " is too small; raising to " + formatDuration(minFlex)); + flexMillis = minFlex; + } + mIsPeriodic = true; mIntervalMillis = intervalMillis; mFlexMillis = flexMillis; @@ -1189,6 +1319,13 @@ public class JobInfo implements Parcelable { */ public Builder setBackoffCriteria(long initialBackoffMillis, @BackoffPolicy int backoffPolicy) { + final long minBackoff = getMinBackoffMillis(); + if (initialBackoffMillis < minBackoff) { + Log.w(TAG, "Requested backoff " + formatDuration(initialBackoffMillis) + " for job " + + mJobId + " is too small; raising to " + formatDuration(minBackoff)); + initialBackoffMillis = minBackoff; + } + mBackoffPolicySet = true; mInitialBackoffMillis = initialBackoffMillis; mBackoffPolicy = backoffPolicy; @@ -1213,16 +1350,22 @@ public class JobInfo implements Parcelable { public JobInfo build() { // Allow jobs with no constraints - What am I, a database? if (!mHasEarlyConstraint && !mHasLateConstraint && mConstraintFlags == 0 && - mNetworkType == NETWORK_TYPE_NONE && + mNetworkRequest == null && mTriggerContentUris == null) { throw new IllegalArgumentException("You're trying to build a job with no " + "constraints, this is not allowed."); } // Check that network estimates require network type - if (mNetworkBytes > 0 && mNetworkType == NETWORK_TYPE_NONE) { + if (mNetworkBytes > 0 && mNetworkRequest == null) { throw new IllegalArgumentException( "Can't provide estimated network usage without requiring a network"); } + // We can't serialize network specifiers + if (mIsPersisted && mNetworkRequest != null + && mNetworkRequest.networkCapabilities.getNetworkSpecifier() != null) { + throw new IllegalArgumentException( + "Network specifiers aren't supported for persistent jobs"); + } // Check that a deadline was not set on a periodic job. if (mIsPeriodic) { if (mMaxExecutionDelayMillis != 0L) { @@ -1257,31 +1400,7 @@ public class JobInfo implements Parcelable { " back-off policy, so calling setBackoffCriteria with" + " setRequiresDeviceIdle is an error."); } - JobInfo job = new JobInfo(this); - if (job.isPeriodic()) { - if (job.intervalMillis != job.getIntervalMillis()) { - StringBuilder builder = new StringBuilder(); - builder.append("Specified interval for ") - .append(String.valueOf(mJobId)) - .append(" is "); - formatDuration(mIntervalMillis, builder); - builder.append(". Clamped to "); - formatDuration(job.getIntervalMillis(), builder); - Log.w(TAG, builder.toString()); - } - if (job.flexMillis != job.getFlexMillis()) { - StringBuilder builder = new StringBuilder(); - builder.append("Specified flex for ") - .append(String.valueOf(mJobId)) - .append(" is "); - formatDuration(mFlexMillis, builder); - builder.append(". Clamped to "); - formatDuration(job.getFlexMillis(), builder); - Log.w(TAG, builder.toString()); - } - } - return job; + return new JobInfo(this); } } - } diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java index f6b6b869..616a5be3 100644 --- a/android/app/slice/Slice.java +++ b/android/app/slice/Slice.java @@ -21,8 +21,12 @@ import android.annotation.Nullable; import android.annotation.StringDef; import android.app.PendingIntent; import android.app.RemoteInput; +import android.content.ContentProvider; import android.content.ContentResolver; +import android.content.Context; import android.content.IContentProvider; +import android.content.Intent; +import android.content.pm.ResolveInfo; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; @@ -54,7 +58,12 @@ public final class Slice implements Parcelable { public @interface SliceHint{ } /** - * Hint that this content is a title of other content in the slice. + * Hint that this content is a title of other content in the slice. This can also indicate that + * the content should be used in the shortcut representation of the slice (icon, label, action), + * normally this should be indicated by adding the hint on the action containing that content. + * + * @see SliceView#MODE_SHORTCUT + * @see SliceItem#TYPE_ACTION */ public static final String HINT_TITLE = "title"; /** @@ -100,6 +109,21 @@ public final class Slice implements Parcelable { */ public static final String HINT_NO_TINT = "no_tint"; /** + * Hint to indicate that this content should not be shown in the {@link SliceView#MODE_SMALL} + * and {@link SliceView#MODE_LARGE} modes of SliceView. This content may be used to populate + * the {@link SliceView#MODE_SHORTCUT} format of the slice. + * @hide + */ + public static final String HINT_HIDDEN = "hidden"; + /** + * Hint to indicate that this content has a toggle action associated with it. To indicate that + * the toggle is on, use {@link #HINT_SELECTED}. When the toggle state changes, the intent + * associated with it will be sent along with an extra {@link #EXTRA_TOGGLE_STATE} which can be + * retrieved to see the new state of the toggle. + * @hide + */ + public static final String HINT_TOGGLE = "toggle"; + /** * Hint to indicate that this slice is incomplete and an update will be sent once * loading is complete. Slices which contain HINT_PARTIAL will not be cached by the * OS and should not be cached by apps. @@ -112,6 +136,11 @@ public final class Slice implements Parcelable { * @hide */ public static final String HINT_ALT = "alt"; + /** + * Key to retrieve an extra added to an intent when a control is changed. + * @hide + */ + public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE"; private final SliceItem[] mItems; private final @SliceHint String[] mHints; @@ -398,4 +427,58 @@ public final class Slice implements Parcelable { resolver.releaseProvider(provider); } } + + /** + * Turns a slice intent into slice content. Expects an explicit intent. If there is no + * {@link ContentProvider} associated with the given intent this will throw + * {@link IllegalArgumentException}. + * + * @param context The context to use. + * @param intent The intent associated with a slice. + * @return The Slice provided by the app or null if none is given. + * @see Slice + * @see SliceProvider#onMapIntentToUri(Intent) + * @see Intent + */ + public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent) { + Preconditions.checkNotNull(intent, "intent"); + Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null, + "Slice intent must be explicit " + intent); + ContentResolver resolver = context.getContentResolver(); + + // Check if the intent has data for the slice uri on it and use that + final Uri intentData = intent.getData(); + if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) { + return bindSlice(resolver, intentData); + } + // Otherwise ask the app + List<ResolveInfo> providers = + context.getPackageManager().queryIntentContentProviders(intent, 0); + if (providers == null) { + throw new IllegalArgumentException("Unable to resolve intent " + intent); + } + String authority = providers.get(0).providerInfo.authority; + Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).build(); + IContentProvider provider = resolver.acquireProvider(uri); + if (provider == null) { + throw new IllegalArgumentException("Unknown URI " + uri); + } + try { + Bundle extras = new Bundle(); + extras.putParcelable(SliceProvider.EXTRA_INTENT, intent); + final Bundle res = provider.call(resolver.getPackageName(), + SliceProvider.METHOD_MAP_INTENT, null, extras); + if (res == null) { + return null; + } + return res.getParcelable(SliceProvider.EXTRA_SLICE); + } catch (RemoteException e) { + // Arbitrary and not worth documenting, as Activity + // Manager will kill this process shortly anyway. + return null; + } finally { + resolver.releaseProvider(provider); + } + } } diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java index 33825b4b..05f4ce6e 100644 --- a/android/app/slice/SliceProvider.java +++ b/android/app/slice/SliceProvider.java @@ -16,46 +16,69 @@ package android.app.slice; import android.Manifest.permission; +import android.annotation.NonNull; +import android.app.slice.widget.SliceView; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; +import android.content.Intent; +import android.content.IntentFilter; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; +import android.os.Process; import android.os.StrictMode; import android.os.StrictMode.ThreadPolicy; +import android.os.UserHandle; import android.util.Log; import java.util.concurrent.CountDownLatch; /** - * A SliceProvider allows app to provide content to be displayed in system - * spaces. This content is templated and can contain actions, and the behavior - * of how it is surfaced is specific to the system surface. + * A SliceProvider allows an app to provide content to be displayed in system spaces. This content + * is templated and can contain actions, and the behavior of how it is surfaced is specific to the + * system surface. + * <p> + * Slices are not currently live content. They are bound once and shown to the user. If the content + * changes due to a callback from user interaction, then + * {@link ContentResolver#notifyChange(Uri, ContentObserver)} should be used to notify the system. + * </p> + * <p> + * The provider needs to be declared in the manifest to provide the authority for the app. The + * authority for most slices is expected to match the package of the application. + * </p> * - * <p>Slices are not currently live content. They are bound once and shown to the - * user. If the content changes due to a callback from user interaction, then - * {@link ContentResolver#notifyChange(Uri, ContentObserver)} - * should be used to notify the system.</p> - * - * <p>The provider needs to be declared in the manifest to provide the authority - * for the app. The authority for most slices is expected to match the package - * of the application.</p> * <pre class="prettyprint"> * {@literal * <provider * android:name="com.android.mypkg.MySliceProvider" * android:authorities="com.android.mypkg" />} * </pre> + * <p> + * Slices can be identified by a Uri or by an Intent. To link an Intent with a slice, the provider + * must have an {@link IntentFilter} matching the slice intent. When a slice is being requested via + * an intent, {@link #onMapIntentToUri(Intent)} can be called and is expected to return an + * appropriate Uri representing the slice. + * + * <pre class="prettyprint"> + * {@literal + * <provider + * android:name="com.android.mypkg.MySliceProvider" + * android:authorities="com.android.mypkg"> + * <intent-filter> + * <action android:name="android.intent.action.MY_SLICE_INTENT" /> + * </intent-filter> + * </provider>} + * </pre> * * @see Slice */ public abstract class SliceProvider extends ContentProvider { - /** * This is the Android platform's MIME type for a slice: URI * containing a slice implemented through {@link SliceProvider}. @@ -74,6 +97,14 @@ public abstract class SliceProvider extends ContentProvider { /** * @hide */ + public static final String METHOD_MAP_INTENT = "map_slice"; + /** + * @hide + */ + public static final String EXTRA_INTENT = "slice_intent"; + /** + * @hide + */ public static final String EXTRA_SLICE = "slice"; private static final boolean DEBUG = false; @@ -94,6 +125,20 @@ public abstract class SliceProvider extends ContentProvider { // TODO: Provide alternate notifyChange that takes in the slice (i.e. notifyChange(Uri, Slice)). public abstract Slice onBindSlice(Uri sliceUri); + /** + * This method must be overridden if an {@link IntentFilter} is specified on the SliceProvider. + * In that case, this method can be called and is expected to return a non-null Uri representing + * a slice. Otherwise this will throw {@link UnsupportedOperationException}. + * + * @return Uri representing the slice associated with the provided intent. + * @see {@link Slice} + * @see {@link SliceView#setSlice(Intent)} + */ + public @NonNull Uri onMapIntentToUri(Intent intent) { + throw new UnsupportedOperationException( + "This provider has not implemented intent to uri mapping"); + } + @Override public final int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { @@ -143,14 +188,31 @@ public abstract class SliceProvider extends ContentProvider { @Override public Bundle call(String method, String arg, Bundle extras) { if (method.equals(METHOD_SLICE)) { - getContext().enforceCallingPermission(permission.BIND_SLICE, - "Slice binding requires the permission BIND_SLICE"); Uri uri = extras.getParcelable(EXTRA_BIND_URI); + if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) { + getContext().enforceUriPermission(uri, permission.BIND_SLICE, + permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + "Slice binding requires the permission BIND_SLICE"); + } Slice s = handleBindSlice(uri); Bundle b = new Bundle(); b.putParcelable(EXTRA_SLICE, s); return b; + } else if (method.equals(METHOD_MAP_INTENT)) { + getContext().enforceCallingPermission(permission.BIND_SLICE, + "Slice binding requires the permission BIND_SLICE"); + Intent intent = extras.getParcelable(EXTRA_INTENT); + Uri uri = onMapIntentToUri(intent); + Bundle b = new Bundle(); + if (uri != null) { + Slice s = handleBindSlice(uri); + b.putParcelable(EXTRA_SLICE, s); + } else { + b.putParcelable(EXTRA_SLICE, null); + } + return b; } return super.call(method, arg, extras); } diff --git a/android/app/slice/widget/GridView.java b/android/app/slice/widget/GridView.java index 67a3c671..793abc05 100644 --- a/android/app/slice/widget/GridView.java +++ b/android/app/slice/widget/GridView.java @@ -126,6 +126,9 @@ public class GridView extends LinearLayout implements SliceListView { * Returns true if this item is just an image. */ private boolean addItem(SliceItem item) { + if (item.hasHint(Slice.HINT_HIDDEN)) { + return false; + } if (item.getType() == SliceItem.TYPE_IMAGE) { ImageView v = new ImageView(getContext()); v.setImageIcon(item.getIcon()); @@ -145,6 +148,9 @@ public class GridView extends LinearLayout implements SliceListView { items.addAll(item.getSlice().getItems()); } items.forEach(i -> { + if (i.hasHint(Slice.HINT_HIDDEN)) { + return; + } Context context = getContext(); switch (i.getType()) { case SliceItem.TYPE_TEXT: diff --git a/android/app/slice/widget/LargeTemplateView.java b/android/app/slice/widget/LargeTemplateView.java index f45b2a8f..788f6fb6 100644 --- a/android/app/slice/widget/LargeTemplateView.java +++ b/android/app/slice/widget/LargeTemplateView.java @@ -85,9 +85,14 @@ public class LargeTemplateView extends SliceModeView { addList(slice, items); } else { slice.getItems().forEach(item -> { - if (item.hasHint(Slice.HINT_ACTIONS)) { + if (item.hasHint(Slice.HINT_HIDDEN)) { + // If it's hidden we don't show it + return; + } else if (item.hasHint(Slice.HINT_ACTIONS)) { + // Action groups don't show in lists return; } else if (item.getType() == SliceItem.TYPE_COLOR) { + // A color is not a list item return; } else if (item.getType() == SliceItem.TYPE_SLICE && item.hasHint(Slice.HINT_LIST)) { @@ -108,8 +113,12 @@ public class LargeTemplateView extends SliceModeView { private void addList(Slice slice, List<SliceItem> items) { List<SliceItem> sliceItems = slice.getItems(); - sliceItems.forEach(i -> i.addHint(Slice.HINT_LIST_ITEM)); - items.addAll(sliceItems); + sliceItems.forEach(i -> { + if (!i.hasHint(Slice.HINT_HIDDEN) && i.getType() != SliceItem.TYPE_COLOR) { + i.addHint(Slice.HINT_LIST_ITEM); + items.add(i); + } + }); } /** diff --git a/android/app/slice/widget/ShortcutView.java b/android/app/slice/widget/ShortcutView.java index 0bca8ce2..0b7ad0d6 100644 --- a/android/app/slice/widget/ShortcutView.java +++ b/android/app/slice/widget/ShortcutView.java @@ -24,13 +24,20 @@ import android.app.slice.SliceQuery; import android.app.slice.widget.SliceView.SliceModeView; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.Resources; import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.net.Uri; import com.android.internal.R; +import java.util.List; + /** * @hide */ @@ -38,27 +45,26 @@ public class ShortcutView extends SliceModeView { private static final String TAG = "ShortcutView"; - private PendingIntent mAction; private Uri mUri; + private PendingIntent mAction; + private SliceItem mLabel; + private SliceItem mIcon; + private int mLargeIconSize; private int mSmallIconSize; public ShortcutView(Context context) { super(context); - mSmallIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.slice_icon_size); + final Resources res = getResources(); + mSmallIconSize = res.getDimensionPixelSize(R.dimen.slice_icon_size); + mLargeIconSize = res.getDimensionPixelSize(R.dimen.slice_shortcut_size); } @Override public void setSlice(Slice slice) { removeAllViews(); - SliceItem sliceItem = SliceQuery.find(slice, SliceItem.TYPE_ACTION); - SliceItem iconItem = SliceQuery.getPrimaryIcon(slice); - SliceItem textItem = sliceItem != null - ? SliceQuery.find(sliceItem, SliceItem.TYPE_TEXT) - : SliceQuery.find(slice, SliceItem.TYPE_TEXT); - SliceItem colorItem = sliceItem != null - ? SliceQuery.find(sliceItem, SliceItem.TYPE_COLOR) - : SliceQuery.find(slice, SliceItem.TYPE_COLOR); + determineShortcutItems(getContext(), slice); + SliceItem colorItem = SliceQuery.find(slice, SliceItem.TYPE_COLOR); if (colorItem == null) { colorItem = SliceQuery.find(slice, SliceItem.TYPE_COLOR); } @@ -67,13 +73,11 @@ public class ShortcutView extends SliceModeView { ShapeDrawable circle = new ShapeDrawable(new OvalShape()); circle.setTint(color); setBackground(circle); - if (iconItem != null) { - final boolean isLarge = iconItem.hasHint(Slice.HINT_LARGE); + if (mIcon != null) { + final boolean isLarge = mIcon.hasHint(Slice.HINT_LARGE); final int iconSize = isLarge ? mLargeIconSize : mSmallIconSize; - SliceViewUtil.createCircledIcon(getContext(), color, iconSize, iconItem.getIcon(), + SliceViewUtil.createCircledIcon(getContext(), color, iconSize, mIcon.getIcon(), isLarge, this /* parent */); - mAction = sliceItem != null ? sliceItem.getAction() - : null; mUri = slice.getUri(); setClickable(true); } else { @@ -103,4 +107,69 @@ public class ShortcutView extends SliceModeView { } return true; } + + /** + * Looks at the slice and determines which items are best to use to compose the shortcut. + */ + private void determineShortcutItems(Context context, Slice slice) { + List<String> h = slice.getHints(); + SliceItem sliceItem = new SliceItem(slice, SliceItem.TYPE_SLICE, + h.toArray(new String[h.size()])); + SliceItem titleItem = SliceQuery.find(slice, SliceItem.TYPE_ACTION, + Slice.HINT_TITLE, null); + + if (titleItem != null) { + // Preferred case: hinted action containing hinted image and text + mAction = titleItem.getAction(); + mIcon = SliceQuery.find(titleItem.getSlice(), SliceItem.TYPE_IMAGE, Slice.HINT_TITLE, + null); + mLabel = SliceQuery.find(titleItem.getSlice(), SliceItem.TYPE_TEXT, Slice.HINT_TITLE, + null); + } else { + // No hinted action; just use the first one + SliceItem actionItem = SliceQuery.find(sliceItem, SliceItem.TYPE_ACTION, (String) null, + null); + mAction = (actionItem != null) ? actionItem.getAction() : null; + } + // First fallback: any hinted image and text + if (mIcon == null) { + mIcon = SliceQuery.find(sliceItem, SliceItem.TYPE_IMAGE, Slice.HINT_TITLE, + null); + } + if (mLabel == null) { + mLabel = SliceQuery.find(sliceItem, SliceItem.TYPE_TEXT, Slice.HINT_TITLE, + null); + } + // Second fallback: first image and text + if (mIcon == null) { + mIcon = SliceQuery.find(sliceItem, SliceItem.TYPE_IMAGE, (String) null, + null); + } + if (mLabel == null) { + mLabel = SliceQuery.find(sliceItem, SliceItem.TYPE_TEXT, (String) null, + null); + } + // Final fallback: use app info + if (mIcon == null || mLabel == null || mAction == null) { + PackageManager pm = context.getPackageManager(); + ProviderInfo providerInfo = pm.resolveContentProvider( + slice.getUri().getAuthority(), 0); + ApplicationInfo appInfo = providerInfo.applicationInfo; + if (appInfo != null) { + if (mIcon == null) { + Drawable icon = appInfo.loadDefaultIcon(pm); + mIcon = new SliceItem(SliceViewUtil.createIconFromDrawable(icon), + SliceItem.TYPE_IMAGE, new String[] {Slice.HINT_LARGE}); + } + if (mLabel == null) { + mLabel = new SliceItem(pm.getApplicationLabel(appInfo), + SliceItem.TYPE_TEXT, null); + } + if (mAction == null) { + mAction = PendingIntent.getActivity(context, 0, + pm.getLaunchIntentForPackage(appInfo.packageName), 0); + } + } + } + } } diff --git a/android/app/slice/widget/SliceView.java b/android/app/slice/widget/SliceView.java index 5bafbc03..fa1b64ce 100644 --- a/android/app/slice/widget/SliceView.java +++ b/android/app/slice/widget/SliceView.java @@ -115,7 +115,9 @@ public class SliceView extends ViewGroup { */ public static final String MODE_LARGE = "SLICE_LARGE"; /** - * Mode indicating this slice should be presented as an icon. + * Mode indicating this slice should be presented as an icon. A shortcut requires an intent, + * icon, and label. This can be indicated by using {@link Slice#HINT_TITLE} on an action in a + * slice. */ public static final String MODE_SHORTCUT = "SLICE_ICON"; @@ -181,10 +183,25 @@ public class SliceView extends ViewGroup { } /** + * Populates this view with the {@link Slice} associated with the provided {@link Intent}. To + * use this method your app must have the permission + * {@link android.Manifest.permission#BIND_SLICE}). + * <p> + * Setting a slice differs from {@link #showSlice(Slice)} because it will ensure the view is + * updated with the slice identified by the provided intent changes. The lifecycle of this + * observer is handled by SliceView in {@link #onAttachedToWindow()} and + * {@link #onDetachedFromWindow()}. To unregister this observer outside of that you can call + * {@link #clearSlice}. + * + * @return true if a slice was found for the provided intent. * @hide */ - public void showSlice(Intent intent) { - // TODO + public boolean setSlice(@Nullable Intent intent) { + Slice s = Slice.bindSlice(mContext, intent); + if (s != null) { + return setSlice(s.getUri()); + } + return s != null; } /** @@ -197,8 +214,7 @@ public class SliceView extends ViewGroup { * is handled by SliceView in {@link #onAttachedToWindow()} and {@link #onDetachedFromWindow()}. * To unregister this observer outside of that you can call {@link #clearSlice}. * - * @return true if the a slice was found for the provided uri. - * @see #clearSlice + * @return true if a slice was found for the provided uri. */ public boolean setSlice(@NonNull Uri sliceUri) { Preconditions.checkNotNull(sliceUri, @@ -210,11 +226,15 @@ public class SliceView extends ViewGroup { validate(sliceUri); Slice s = Slice.bindSlice(mContext.getContentResolver(), sliceUri); if (s != null) { + if (mObserver != null) { + getContext().getContentResolver().unregisterContentObserver(mObserver); + } mObserver = new SliceObserver(new Handler(Looper.getMainLooper())); if (isAttachedToWindow()) { registerSlice(sliceUri); } - showSlice(s); + mCurrentSlice = s; + reinflate(); } return s != null; } diff --git a/android/app/slice/widget/SliceViewUtil.java b/android/app/slice/widget/SliceViewUtil.java index 03669983..1cf0055b 100644 --- a/android/app/slice/widget/SliceViewUtil.java +++ b/android/app/slice/widget/SliceViewUtil.java @@ -28,6 +28,7 @@ import android.graphics.Paint; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.view.Gravity; @@ -141,6 +142,21 @@ public class SliceViewUtil { /** * @hide */ + public static Icon createIconFromDrawable(Drawable d) { + if (d instanceof BitmapDrawable) { + return Icon.createWithBitmap(((BitmapDrawable) d).getBitmap()); + } + Bitmap b = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(b); + d.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + d.draw(canvas); + return Icon.createWithBitmap(b); + } + + /** + * @hide + */ public static void createCircledIcon(Context context, int color, int iconSize, Icon icon, boolean isLarge, ViewGroup parent) { ImageView v = new ImageView(context); diff --git a/android/app/usage/UsageStatsManager.java b/android/app/usage/UsageStatsManager.java index c827432a..3a3e16e0 100644 --- a/android/app/usage/UsageStatsManager.java +++ b/android/app/usage/UsageStatsManager.java @@ -261,7 +261,10 @@ public final class UsageStatsManager { /** * @hide + * Changes the app standby state to the provided bucket. */ + @SystemApi + @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE) public void setAppStandbyBucket(String packageName, @StandbyBuckets int bucket) { try { mService.setAppStandbyBucket(packageName, bucket, mContext.getUserId()); diff --git a/android/app/usage/UsageStatsManagerInternal.java b/android/app/usage/UsageStatsManagerInternal.java index dbaace2f..29e7439f 100644 --- a/android/app/usage/UsageStatsManagerInternal.java +++ b/android/app/usage/UsageStatsManagerInternal.java @@ -118,7 +118,15 @@ public abstract class UsageStatsManagerInternal { AppIdleStateChangeListener listener); public static abstract class AppIdleStateChangeListener { - public abstract void onAppIdleStateChanged(String packageName, int userId, boolean idle); + + /** Callback to inform listeners that the idle state has changed to a new bucket. */ + public abstract void onAppIdleStateChanged(String packageName, int userId, boolean idle, + int bucket); + + /** + * Callback to inform listeners that the parole state has changed. This means apps are + * allowed to do work even if they're idle or in a low bucket. + */ public abstract void onParoleStateChanged(boolean isParoleOn); } diff --git a/android/arch/lifecycle/AndroidViewModel.java b/android/arch/lifecycle/AndroidViewModel.java index 106b2ef0..e8895bd0 100644 --- a/android/arch/lifecycle/AndroidViewModel.java +++ b/android/arch/lifecycle/AndroidViewModel.java @@ -37,6 +37,7 @@ public class AndroidViewModel extends ViewModel { /** * Return the application. */ + @SuppressWarnings("TypeParameterUnusedInFormals") @NonNull public <T extends Application> T getApplication() { //noinspection unchecked diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java index 1ddcb1a9..f1352446 100644 --- a/android/arch/lifecycle/ComputableLiveData.java +++ b/android/arch/lifecycle/ComputableLiveData.java @@ -1,136 +1,9 @@ -/* - * Copyright (C) 2017 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. - */ - +//ComputableLiveData interface for tests package android.arch.lifecycle; - -import android.arch.core.executor.ArchTaskExecutor; -import android.support.annotation.MainThread; -import android.support.annotation.NonNull; -import android.support.annotation.RestrictTo; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A LiveData class that can be invalidated & computed on demand. - * <p> - * This is an internal class for now, might be public if we see the necessity. - * - * @param <T> The type of the live data - * @hide internal - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +import android.arch.lifecycle.LiveData; public abstract class ComputableLiveData<T> { - - private final LiveData<T> mLiveData; - - private AtomicBoolean mInvalid = new AtomicBoolean(true); - private AtomicBoolean mComputing = new AtomicBoolean(false); - - /** - * Creates a computable live data which is computed when there are active observers. - * <p> - * It can also be invalidated via {@link #invalidate()} which will result in a call to - * {@link #compute()} if there are active observers (or when they start observing) - */ - @SuppressWarnings("WeakerAccess") - public ComputableLiveData() { - mLiveData = new LiveData<T>() { - @Override - protected void onActive() { - // TODO if we make this class public, we should accept an executor - ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); - } - }; - } - - /** - * Returns the LiveData managed by this class. - * - * @return A LiveData that is controlled by ComputableLiveData. - */ - @SuppressWarnings("WeakerAccess") - @NonNull - public LiveData<T> getLiveData() { - return mLiveData; - } - - @VisibleForTesting - final Runnable mRefreshRunnable = new Runnable() { - @WorkerThread - @Override - public void run() { - boolean computed; - do { - computed = false; - // compute can happen only in 1 thread but no reason to lock others. - if (mComputing.compareAndSet(false, true)) { - // as long as it is invalid, keep computing. - try { - T value = null; - while (mInvalid.compareAndSet(true, false)) { - computed = true; - value = compute(); - } - if (computed) { - mLiveData.postValue(value); - } - } finally { - // release compute lock - mComputing.set(false); - } - } - // check invalid after releasing compute lock to avoid the following scenario. - // Thread A runs compute() - // Thread A checks invalid, it is false - // Main thread sets invalid to true - // Thread B runs, fails to acquire compute lock and skips - // Thread A releases compute lock - // We've left invalid in set state. The check below recovers. - } while (computed && mInvalid.get()); - } - }; - - // invalidation check always happens on the main thread - @VisibleForTesting - final Runnable mInvalidationRunnable = new Runnable() { - @MainThread - @Override - public void run() { - boolean isActive = mLiveData.hasActiveObservers(); - if (mInvalid.compareAndSet(false, true)) { - if (isActive) { - // TODO if we make this class public, we should accept an executor. - ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); - } - } - } - }; - - /** - * Invalidates the LiveData. - * <p> - * When there are active observers, this will trigger a call to {@link #compute()}. - */ - public void invalidate() { - ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable); - } - - @SuppressWarnings("WeakerAccess") - @WorkerThread - protected abstract T compute(); + public ComputableLiveData(){} + abstract protected T compute(); + public LiveData<T> getLiveData() {return null;} + public void invalidate() {} } diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java index 5b09c32f..3aea6acb 100644 --- a/android/arch/lifecycle/LiveData.java +++ b/android/arch/lifecycle/LiveData.java @@ -1,410 +1,4 @@ -/* - * Copyright (C) 2017 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. - */ - +//LiveData interface for tests package android.arch.lifecycle; - -import static android.arch.lifecycle.Lifecycle.State.DESTROYED; -import static android.arch.lifecycle.Lifecycle.State.STARTED; - -import android.arch.core.executor.ArchTaskExecutor; -import android.arch.core.internal.SafeIterableMap; -import android.arch.lifecycle.Lifecycle.State; -import android.support.annotation.MainThread; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.Iterator; -import java.util.Map; - -/** - * LiveData is a data holder class that can be observed within a given lifecycle. - * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and - * this observer will be notified about modifications of the wrapped data only if the paired - * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is - * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via - * {@link #observeForever(Observer)} is considered as always active and thus will be always notified - * about modifications. For those observers, you should manually call - * {@link #removeObserver(Observer)}. - * - * <p> An observer added with a Lifecycle will be automatically removed if the corresponding - * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for - * activities and fragments where they can safely observe LiveData and not worry about leaks: - * they will be instantly unsubscribed when they are destroyed. - * - * <p> - * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods - * to get notified when number of active {@link Observer}s change between 0 and 1. - * This allows LiveData to release any heavy resources when it does not have any Observers that - * are actively observing. - * <p> - * This class is designed to hold individual data fields of {@link ViewModel}, - * but can also be used for sharing data between different modules in your application - * in a decoupled fashion. - * - * @param <T> The type of data held by this instance - * @see ViewModel - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -// TODO: Thread checks are too strict right now, we may consider automatically moving them to main -// thread. -public abstract class LiveData<T> { - private final Object mDataLock = new Object(); - static final int START_VERSION = -1; - private static final Object NOT_SET = new Object(); - - private static final LifecycleOwner ALWAYS_ON = new LifecycleOwner() { - - private LifecycleRegistry mRegistry = init(); - - private LifecycleRegistry init() { - LifecycleRegistry registry = new LifecycleRegistry(this); - registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); - registry.handleLifecycleEvent(Lifecycle.Event.ON_START); - registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); - return registry; - } - - @Override - public Lifecycle getLifecycle() { - return mRegistry; - } - }; - - private SafeIterableMap<Observer<T>, LifecycleBoundObserver> mObservers = - new SafeIterableMap<>(); - - // how many observers are in active state - private int mActiveCount = 0; - private volatile Object mData = NOT_SET; - // when setData is called, we set the pending data and actual data swap happens on the main - // thread - private volatile Object mPendingData = NOT_SET; - private int mVersion = START_VERSION; - - private boolean mDispatchingValue; - @SuppressWarnings("FieldCanBeLocal") - private boolean mDispatchInvalidated; - private final Runnable mPostValueRunnable = new Runnable() { - @Override - public void run() { - Object newValue; - synchronized (mDataLock) { - newValue = mPendingData; - mPendingData = NOT_SET; - } - //noinspection unchecked - setValue((T) newValue); - } - }; - - private void considerNotify(LifecycleBoundObserver observer) { - if (!observer.active) { - return; - } - // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet. - // - // we still first check observer.active to keep it as the entrance for events. So even if - // the observer moved to an active state, if we've not received that event, we better not - // notify for a more predictable notification order. - if (!isActiveState(observer.owner.getLifecycle().getCurrentState())) { - observer.activeStateChanged(false); - return; - } - if (observer.lastVersion >= mVersion) { - return; - } - observer.lastVersion = mVersion; - //noinspection unchecked - observer.observer.onChanged((T) mData); - } - - private void dispatchingValue(@Nullable LifecycleBoundObserver initiator) { - if (mDispatchingValue) { - mDispatchInvalidated = true; - return; - } - mDispatchingValue = true; - do { - mDispatchInvalidated = false; - if (initiator != null) { - considerNotify(initiator); - initiator = null; - } else { - for (Iterator<Map.Entry<Observer<T>, LifecycleBoundObserver>> iterator = - mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { - considerNotify(iterator.next().getValue()); - if (mDispatchInvalidated) { - break; - } - } - } - } while (mDispatchInvalidated); - mDispatchingValue = false; - } - - /** - * Adds the given observer to the observers list within the lifespan of the given - * owner. The events are dispatched on the main thread. If LiveData already has data - * set, it will be delivered to the observer. - * <p> - * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED} - * or {@link Lifecycle.State#RESUMED} state (active). - * <p> - * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will - * automatically be removed. - * <p> - * When data changes while the {@code owner} is not active, it will not receive any updates. - * If it becomes active again, it will receive the last available data automatically. - * <p> - * LiveData keeps a strong reference to the observer and the owner as long as the - * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to - * the observer & the owner. - * <p> - * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData - * ignores the call. - * <p> - * If the given owner, observer tuple is already in the list, the call is ignored. - * If the observer is already in the list with another owner, LiveData throws an - * {@link IllegalArgumentException}. - * - * @param owner The LifecycleOwner which controls the observer - * @param observer The observer that will receive the events - */ - @MainThread - public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) { - if (owner.getLifecycle().getCurrentState() == DESTROYED) { - // ignore - return; - } - LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); - LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper); - if (existing != null && existing.owner != wrapper.owner) { - throw new IllegalArgumentException("Cannot add the same observer" - + " with different lifecycles"); - } - if (existing != null) { - return; - } - owner.getLifecycle().addObserver(wrapper); - } - - /** - * Adds the given observer to the observers list. This call is similar to - * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which - * is always active. This means that the given observer will receive all events and will never - * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop - * observing this LiveData. - * While LiveData has one of such observers, it will be considered - * as active. - * <p> - * If the observer was already added with an owner to this LiveData, LiveData throws an - * {@link IllegalArgumentException}. - * - * @param observer The observer that will receive the events - */ - @MainThread - public void observeForever(@NonNull Observer<T> observer) { - observe(ALWAYS_ON, observer); - } - - /** - * Removes the given observer from the observers list. - * - * @param observer The Observer to receive events. - */ - @MainThread - public void removeObserver(@NonNull final Observer<T> observer) { - assertMainThread("removeObserver"); - LifecycleBoundObserver removed = mObservers.remove(observer); - if (removed == null) { - return; - } - removed.owner.getLifecycle().removeObserver(removed); - removed.activeStateChanged(false); - } - - /** - * Removes all observers that are tied to the given {@link LifecycleOwner}. - * - * @param owner The {@code LifecycleOwner} scope for the observers to be removed. - */ - @MainThread - public void removeObservers(@NonNull final LifecycleOwner owner) { - assertMainThread("removeObservers"); - for (Map.Entry<Observer<T>, LifecycleBoundObserver> entry : mObservers) { - if (entry.getValue().owner == owner) { - removeObserver(entry.getKey()); - } - } - } - - /** - * Posts a task to a main thread to set the given value. So if you have a following code - * executed in the main thread: - * <pre class="prettyprint"> - * liveData.postValue("a"); - * liveData.setValue("b"); - * </pre> - * The value "b" would be set at first and later the main thread would override it with - * the value "a". - * <p> - * If you called this method multiple times before a main thread executed a posted task, only - * the last value would be dispatched. - * - * @param value The new value - */ - protected void postValue(T value) { - boolean postTask; - synchronized (mDataLock) { - postTask = mPendingData == NOT_SET; - mPendingData = value; - } - if (!postTask) { - return; - } - ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); - } - - /** - * Sets the value. If there are active observers, the value will be dispatched to them. - * <p> - * This method must be called from the main thread. If you need set a value from a background - * thread, you can use {@link #postValue(Object)} - * - * @param value The new value - */ - @MainThread - protected void setValue(T value) { - assertMainThread("setValue"); - mVersion++; - mData = value; - dispatchingValue(null); - } - - /** - * Returns the current value. - * Note that calling this method on a background thread does not guarantee that the latest - * value set will be received. - * - * @return the current value - */ - @Nullable - public T getValue() { - Object data = mData; - if (data != NOT_SET) { - //noinspection unchecked - return (T) data; - } - return null; - } - - int getVersion() { - return mVersion; - } - - /** - * Called when the number of active observers change to 1 from 0. - * <p> - * This callback can be used to know that this LiveData is being used thus should be kept - * up to date. - */ - protected void onActive() { - - } - - /** - * Called when the number of active observers change from 1 to 0. - * <p> - * This does not mean that there are no observers left, there may still be observers but their - * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED} - * (like an Activity in the back stack). - * <p> - * You can check if there are observers via {@link #hasObservers()}. - */ - protected void onInactive() { - - } - - /** - * Returns true if this LiveData has observers. - * - * @return true if this LiveData has observers - */ - public boolean hasObservers() { - return mObservers.size() > 0; - } - - /** - * Returns true if this LiveData has active observers. - * - * @return true if this LiveData has active observers - */ - public boolean hasActiveObservers() { - return mActiveCount > 0; - } - - class LifecycleBoundObserver implements GenericLifecycleObserver { - public final LifecycleOwner owner; - public final Observer<T> observer; - public boolean active; - public int lastVersion = START_VERSION; - - LifecycleBoundObserver(LifecycleOwner owner, Observer<T> observer) { - this.owner = owner; - this.observer = observer; - } - - @Override - public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { - if (owner.getLifecycle().getCurrentState() == DESTROYED) { - removeObserver(observer); - return; - } - // immediately set active state, so we'd never dispatch anything to inactive - // owner - activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState())); - } - - void activeStateChanged(boolean newActive) { - if (newActive == active) { - return; - } - active = newActive; - boolean wasInactive = LiveData.this.mActiveCount == 0; - LiveData.this.mActiveCount += active ? 1 : -1; - if (wasInactive && active) { - onActive(); - } - if (LiveData.this.mActiveCount == 0 && !active) { - onInactive(); - } - if (active) { - dispatchingValue(this); - } - } - } - - static boolean isActiveState(State state) { - return state.isAtLeast(STARTED); - } - - private void assertMainThread(String methodName) { - if (!ArchTaskExecutor.getInstance().isMainThread()) { - throw new IllegalStateException("Cannot invoke " + methodName + " on a background" - + " thread"); - } - } +public class LiveData<T> { } diff --git a/android/arch/lifecycle/LiveDataReactiveStreams.java b/android/arch/lifecycle/LiveDataReactiveStreams.java index 2b25bc9b..ba76f8e8 100644 --- a/android/arch/lifecycle/LiveDataReactiveStreams.java +++ b/android/arch/lifecycle/LiveDataReactiveStreams.java @@ -24,7 +24,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import java.lang.ref.WeakReference; +import java.util.concurrent.atomic.AtomicReference; /** * Adapts {@link LiveData} input and output to the ReactiveStreams spec. @@ -53,83 +53,114 @@ public final class LiveDataReactiveStreams { public static <T> Publisher<T> toPublisher( final LifecycleOwner lifecycle, final LiveData<T> liveData) { - return new Publisher<T>() { + return new LiveDataPublisher<>(lifecycle, liveData); + } + + private static final class LiveDataPublisher<T> implements Publisher<T> { + final LifecycleOwner mLifecycle; + final LiveData<T> mLiveData; + + LiveDataPublisher(final LifecycleOwner lifecycle, final LiveData<T> liveData) { + this.mLifecycle = lifecycle; + this.mLiveData = liveData; + } + + @Override + public void subscribe(Subscriber<? super T> subscriber) { + subscriber.onSubscribe(new LiveDataSubscription<T>(subscriber, mLifecycle, mLiveData)); + } + + static final class LiveDataSubscription<T> implements Subscription, Observer<T> { + final Subscriber<? super T> mSubscriber; + final LifecycleOwner mLifecycle; + final LiveData<T> mLiveData; + + volatile boolean mCanceled; + // used on main thread only boolean mObserving; - boolean mCanceled; long mRequested; + // used on main thread only @Nullable T mLatest; + LiveDataSubscription(final Subscriber<? super T> subscriber, + final LifecycleOwner lifecycle, final LiveData<T> liveData) { + this.mSubscriber = subscriber; + this.mLifecycle = lifecycle; + this.mLiveData = liveData; + } + + @Override + public void onChanged(T t) { + if (mCanceled) { + return; + } + if (mRequested > 0) { + mLatest = null; + mSubscriber.onNext(t); + if (mRequested != Long.MAX_VALUE) { + mRequested--; + } + } else { + mLatest = t; + } + } + @Override - public void subscribe(final Subscriber<? super T> subscriber) { - final Observer<T> observer = new Observer<T>() { + public void request(final long n) { + if (mCanceled) { + return; + } + ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() { @Override - public void onChanged(@Nullable T t) { + public void run() { if (mCanceled) { return; } - if (mRequested > 0) { - mLatest = null; - subscriber.onNext(t); - if (mRequested != Long.MAX_VALUE) { - mRequested--; + if (n <= 0L) { + mCanceled = true; + if (mObserving) { + mLiveData.removeObserver(LiveDataSubscription.this); + mObserving = false; } - } else { - mLatest = t; + mLatest = null; + mSubscriber.onError( + new IllegalArgumentException("Non-positive request")); + return; } - } - }; - subscriber.onSubscribe(new Subscription() { - @Override - public void request(final long n) { - if (n < 0 || mCanceled) { - return; + // Prevent overflowage. + mRequested = mRequested + n >= mRequested + ? mRequested + n : Long.MAX_VALUE; + if (!mObserving) { + mObserving = true; + mLiveData.observe(mLifecycle, LiveDataSubscription.this); + } else if (mLatest != null) { + onChanged(mLatest); + mLatest = null; } - ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() { - @Override - public void run() { - if (mCanceled) { - return; - } - // Prevent overflowage. - mRequested = mRequested + n >= mRequested - ? mRequested + n : Long.MAX_VALUE; - if (!mObserving) { - mObserving = true; - liveData.observe(lifecycle, observer); - } else if (mLatest != null) { - observer.onChanged(mLatest); - mLatest = null; - } - } - }); } + }); + } + @Override + public void cancel() { + if (mCanceled) { + return; + } + mCanceled = true; + ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() { @Override - public void cancel() { - if (mCanceled) { - return; + public void run() { + if (mObserving) { + mLiveData.removeObserver(LiveDataSubscription.this); + mObserving = false; } - ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() { - @Override - public void run() { - if (mCanceled) { - return; - } - if (mObserving) { - liveData.removeObserver(observer); - mObserving = false; - } - mLatest = null; - mCanceled = true; - } - }); + mLatest = null; } }); } - - }; + } } /** @@ -145,6 +176,10 @@ public final class LiveDataReactiveStreams { * Therefore, in the case of a hot RxJava Observable, when a new LiveData {@link Observer} is * added, it will automatically notify with the last value held in LiveData, * which might not be the last value emitted by the Publisher. + * <p> + * Note that LiveData does NOT handle errors and it expects that errors are treated as states + * in the data that's held. In case of an error being emitted by the publisher, an error will + * be propagated to the main thread and the app will crash. * * @param <T> The type of data hold by this instance. */ @@ -166,67 +201,80 @@ public final class LiveDataReactiveStreams { * added, it will automatically notify with the last value held in LiveData, * which might not be the last value emitted by the Publisher. * + * <p> + * Note that LiveData does NOT handle errors and it expects that errors are treated as states + * in the data that's held. In case of an error being emitted by the publisher, an error will + * be propagated to the main thread and the app will crash. + * * @param <T> The type of data hold by this instance. */ private static class PublisherLiveData<T> extends LiveData<T> { - private WeakReference<Subscription> mSubscriptionRef; private final Publisher mPublisher; - private final Object mLock = new Object(); + final AtomicReference<LiveDataSubscriber> mSubscriber; PublisherLiveData(@NonNull final Publisher publisher) { mPublisher = publisher; + mSubscriber = new AtomicReference<>(); } @Override protected void onActive() { super.onActive(); + LiveDataSubscriber s = new LiveDataSubscriber(); + mSubscriber.set(s); + mPublisher.subscribe(s); + } - mPublisher.subscribe(new Subscriber<T>() { - @Override - public void onSubscribe(Subscription s) { - // Don't worry about backpressure. If the stream is too noisy then - // backpressure can be handled upstream. - synchronized (mLock) { - s.request(Long.MAX_VALUE); - mSubscriptionRef = new WeakReference<>(s); - } - } + @Override + protected void onInactive() { + super.onInactive(); + LiveDataSubscriber s = mSubscriber.getAndSet(null); + if (s != null) { + s.cancelSubscription(); + } + } - @Override - public void onNext(final T t) { - postValue(t); - } + final class LiveDataSubscriber extends AtomicReference<Subscription> + implements Subscriber<T> { - @Override - public void onError(Throwable t) { - synchronized (mLock) { - mSubscriptionRef = null; - } - // Errors should be handled upstream, so propagate as a crash. - throw new RuntimeException(t); + @Override + public void onSubscribe(Subscription s) { + if (compareAndSet(null, s)) { + s.request(Long.MAX_VALUE); + } else { + s.cancel(); } + } - @Override - public void onComplete() { - synchronized (mLock) { - mSubscriptionRef = null; - } - } - }); + @Override + public void onNext(T item) { + postValue(item); + } - } + @Override + public void onError(final Throwable ex) { + mSubscriber.compareAndSet(this, null); - @Override - protected void onInactive() { - super.onInactive(); - synchronized (mLock) { - WeakReference<Subscription> subscriptionRef = mSubscriptionRef; - if (subscriptionRef != null) { - Subscription subscription = subscriptionRef.get(); - if (subscription != null) { - subscription.cancel(); + ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() { + @Override + public void run() { + // Errors should be handled upstream, so propagate as a crash. + throw new RuntimeException("LiveData does not handle errors. Errors from " + + "publishers should be handled upstream and propagated as " + + "state", ex); } - mSubscriptionRef = null; + }); + } + + @Override + public void onComplete() { + mSubscriber.compareAndSet(this, null); + } + + public void cancelSubscription() { + Subscription s = get(); + if (s != null) { + s.cancel(); } } } diff --git a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java index 7278847c..83e543c3 100644 --- a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java +++ b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java @@ -16,6 +16,9 @@ package android.arch.lifecycle; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.fail; + import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -34,6 +37,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; import io.reactivex.Flowable; import io.reactivex.disposables.Disposable; @@ -115,6 +119,41 @@ public class LiveDataReactiveStreamsTest { } @Test + public void convertsFromPublisherSubscribeWithDelay() { + PublishProcessor<String> processor = PublishProcessor.create(); + processor.delaySubscription(100, TimeUnit.SECONDS, sBackgroundScheduler); + LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); + + liveData.observe(mLifecycleOwner, mObserver); + + processor.onNext("foo"); + liveData.removeObserver(mObserver); + sBackgroundScheduler.triggerActions(); + liveData.observe(mLifecycleOwner, mObserver); + + processor.onNext("bar"); + processor.onNext("baz"); + + assertThat(mLiveDataOutput, is(Arrays.asList("foo", "foo", "bar", "baz"))); + } + + @Test + public void convertsFromPublisherThrowsException() { + PublishProcessor<String> processor = PublishProcessor.create(); + LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); + + liveData.observe(mLifecycleOwner, mObserver); + + IllegalStateException exception = new IllegalStateException("test exception"); + try { + processor.onError(exception); + fail("Runtime Exception expected"); + } catch (RuntimeException ex) { + assertEquals(ex.getCause(), exception); + } + } + + @Test public void convertsFromPublisherWithMultipleObservers() { final List<String> output2 = new ArrayList<>(); PublishProcessor<String> processor = PublishProcessor.create(); @@ -125,7 +164,7 @@ public class LiveDataReactiveStreamsTest { processor.onNext("foo"); processor.onNext("bar"); - // The second mObserver should only get the newest value and any later values. + // The second observer should only get the newest value and any later values. liveData.observe(mLifecycleOwner, new Observer<String>() { @Override public void onChanged(@Nullable String s) { @@ -140,6 +179,32 @@ public class LiveDataReactiveStreamsTest { } @Test + public void convertsFromPublisherWithMultipleObserversAfterInactive() { + final List<String> output2 = new ArrayList<>(); + PublishProcessor<String> processor = PublishProcessor.create(); + LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); + + liveData.observe(mLifecycleOwner, mObserver); + + processor.onNext("foo"); + processor.onNext("bar"); + + // The second observer should only get the newest value and any later values. + liveData.observe(mLifecycleOwner, new Observer<String>() { + @Override + public void onChanged(@Nullable String s) { + output2.add(s); + } + }); + + liveData.removeObserver(mObserver); + processor.onNext("baz"); + + assertThat(mLiveDataOutput, is(Arrays.asList("foo", "bar"))); + assertThat(output2, is(Arrays.asList("bar", "baz"))); + } + + @Test public void convertsFromPublisherAfterInactive() { PublishProcessor<String> processor = PublishProcessor.create(); LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); @@ -156,7 +221,7 @@ public class LiveDataReactiveStreamsTest { } @Test - public void convertsFromPublisherManagesSubcriptions() { + public void convertsFromPublisherManagesSubscriptions() { PublishProcessor<String> processor = PublishProcessor.create(); LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); @@ -198,7 +263,7 @@ public class LiveDataReactiveStreamsTest { assertThat( mOutputProcessor.getValues(new String[]{}), - is(new String[] {"foo", "bar", "baz"})); + is(new String[]{"foo", "bar", "baz"})); } @Test @@ -263,10 +328,10 @@ public class LiveDataReactiveStreamsTest { final Subscription subscription = subscriptionSubject.blockingSingle(); subscription.request(1); - assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {})); + assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{})); liveData.setValue("foo"); - assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {"foo"})); + assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"})); subscription.request(2); liveData.setValue("baz"); @@ -274,7 +339,7 @@ public class LiveDataReactiveStreamsTest { assertThat( mOutputProcessor.getValues(new String[]{}), - is(new String[] {"foo", "baz", "fizz"})); + is(new String[]{"foo", "baz", "fizz"})); // 'nyan' will be dropped as there is nothing currently requesting a stream. liveData.setValue("nyan"); @@ -282,13 +347,13 @@ public class LiveDataReactiveStreamsTest { assertThat( mOutputProcessor.getValues(new String[]{}), - is(new String[] {"foo", "baz", "fizz"})); + is(new String[]{"foo", "baz", "fizz"})); // When a new request comes in, the latest value will be pushed. subscription.request(1); assertThat( mOutputProcessor.getValues(new String[]{}), - is(new String[] {"foo", "baz", "fizz", "cat"})); + is(new String[]{"foo", "baz", "fizz", "cat"})); } @Test @@ -301,17 +366,17 @@ public class LiveDataReactiveStreamsTest { liveData.setValue("foo"); - assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {})); + assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{})); sBackgroundScheduler.triggerActions(); - assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {"foo"})); + assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"})); liveData.setValue("bar"); liveData.setValue("baz"); - assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[] {"foo"})); + assertThat(mOutputProcessor.getValues(new String[]{}), is(new String[]{"foo"})); sBackgroundScheduler.triggerActions(); assertThat(mOutputProcessor.getValues( new String[]{}), - is(new String[] {"foo", "bar", "baz"})); + is(new String[]{"foo", "bar", "baz"})); } } diff --git a/android/arch/lifecycle/LiveDataTest.java b/android/arch/lifecycle/LiveDataTest.java index 647d5d7a..c1dc54da 100644 --- a/android/arch/lifecycle/LiveDataTest.java +++ b/android/arch/lifecycle/LiveDataTest.java @@ -45,6 +45,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mockito; @@ -52,11 +53,22 @@ import org.mockito.Mockito; @RunWith(JUnit4.class) public class LiveDataTest { private PublicLiveData<String> mLiveData; + private MethodExec mActiveObserversChanged; + private LifecycleOwner mOwner; - private LifecycleOwner mOwner2; private LifecycleRegistry mRegistry; + + private LifecycleOwner mOwner2; private LifecycleRegistry mRegistry2; - private MethodExec mActiveObserversChanged; + + private LifecycleOwner mOwner3; + private Lifecycle mLifecycle3; + private Observer<String> mObserver3; + + private LifecycleOwner mOwner4; + private Lifecycle mLifecycle4; + private Observer<String> mObserver4; + private boolean mInObserver; @Before @@ -67,12 +79,10 @@ public class LiveDataTest { mLiveData.activeObserversChanged = mActiveObserversChanged; mOwner = mock(LifecycleOwner.class); - mRegistry = new LifecycleRegistry(mOwner); when(mOwner.getLifecycle()).thenReturn(mRegistry); mOwner2 = mock(LifecycleOwner.class); - mRegistry2 = new LifecycleRegistry(mOwner2); when(mOwner2.getLifecycle()).thenReturn(mRegistry2); @@ -80,6 +90,19 @@ public class LiveDataTest { } @Before + public void initNonLifecycleRegistry() { + mOwner3 = mock(LifecycleOwner.class); + mLifecycle3 = mock(Lifecycle.class); + mObserver3 = (Observer<String>) mock(Observer.class); + when(mOwner3.getLifecycle()).thenReturn(mLifecycle3); + + mOwner4 = mock(LifecycleOwner.class); + mLifecycle4 = mock(Lifecycle.class); + mObserver4 = (Observer<String>) mock(Observer.class); + when(mOwner4.getLifecycle()).thenReturn(mLifecycle4); + } + + @Before public void swapExecutorDelegate() { ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor()); } @@ -572,100 +595,195 @@ public class LiveDataTest { verify(mActiveObserversChanged, never()).onCall(anyBoolean()); } - /** - * Verifies that if a lifecycle's state changes without an event, and changes to something that - * LiveData would become inactive in response to, LiveData will detect the change upon new data - * being set and become inactive. Also verifies that once the lifecycle enters into a state - * that LiveData should become active to, that it does indeed become active. - */ @Test - public void liveDataActiveStateIsManagedCorrectlyWithoutEvent_oneObserver() { - Observer<String> observer = (Observer<String>) mock(Observer.class); - mLiveData.observe(mOwner, observer); + public void setValue_lifecycleIsCreatedNoEvent_liveDataBecomesInactiveAndObserverNotCalled() { - mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + // Arrange. + + mLiveData.observe(mOwner3, mObserver3); + + GenericLifecycleObserver lifecycleObserver = getGenericLifecycleObserver(mLifecycle3); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED); - // Marking state as CREATED should call onInactive. - reset(mActiveObserversChanged); - mRegistry.markState(Lifecycle.State.CREATED); - verify(mActiveObserversChanged).onCall(false); reset(mActiveObserversChanged); + reset(mObserver3); + + // Act. - // Setting a new value should trigger LiveData to realize the Lifecycle it is observing - // is in a state where the LiveData should be inactive, so the LiveData will call onInactive - // and the Observer shouldn't be affected. mLiveData.setValue("1"); - // state is already CREATED so should not call again - verify(mActiveObserversChanged, never()).onCall(anyBoolean()); - verify(observer, never()).onChanged(anyString()); + // Assert. + + verify(mActiveObserversChanged).onCall(false); + verify(mObserver3, never()).onChanged(anyString()); + } + + /* + * Arrange: LiveData was made inactive via SetValue (because the Lifecycle it was + * observing was in the CREATED state and no event was dispatched). + * Act: Lifecycle enters Started state and dispatches event. + * Assert: LiveData becomes active and dispatches new value to observer. + */ + @Test + public void test_liveDataInactiveViaSetValueThenLifecycleResumes() { + + // Arrange. + + mLiveData.observe(mOwner3, mObserver3); + + GenericLifecycleObserver lifecycleObserver = getGenericLifecycleObserver(mLifecycle3); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED); + mLiveData.setValue("1"); - // Sanity check. Because we've only marked the state as CREATED, sending ON_START - // should re-dispatch events. reset(mActiveObserversChanged); - reset(observer); - mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + reset(mObserver3); + + // Act. + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + lifecycleObserver.onStateChanged(mOwner3, Lifecycle.Event.ON_START); + + // Assert. + verify(mActiveObserversChanged).onCall(true); - verify(observer).onChanged("1"); + verify(mObserver3).onChanged("1"); } - /** - * This test verifies that LiveData will detect changes in LifecycleState that would make it - * inactive upon the setting of new data, but only if all of the Lifecycles it's observing - * are all in those states. It also makes sure that once it is inactive, that it will become - * active again once one of the lifecycles it's observing moves to an appropriate state. + /* + * Arrange: One of two Lifecycles enter the CREATED state without sending an event. + * Act: Lifecycle's setValue method is called with new value. + * Assert: LiveData stays active and new value is dispatched to Lifecycle that is still at least + * STARTED. */ @Test - public void liveDataActiveStateIsManagedCorrectlyWithoutEvent_twoObservers() { - Observer<String> observer1 = mock(Observer.class); - Observer<String> observer2 = mock(Observer.class); + public void setValue_oneOfTwoLifecyclesAreCreatedNoEvent() { - mLiveData.observe(mOwner, observer1); - mLiveData.observe(mOwner2, observer2); + // Arrange. - mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); - mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + mLiveData.observe(mOwner3, mObserver3); + mLiveData.observe(mOwner4, mObserver4); + + GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3); + GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START); + lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED); - // Marking the state to created won't change LiveData to be inactive. reset(mActiveObserversChanged); - mRegistry.markState(Lifecycle.State.CREATED); - verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + reset(mObserver3); + reset(mObserver4); + + // Act. - // After setting a value, the LiveData will stay active because there is still a STARTED - // lifecycle being observed. The one Observer associated with the STARTED lifecycle will - // also have been called, but the other Observer will not have been called. - reset(observer1); - reset(observer2); mLiveData.setValue("1"); + + // Assert. + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); - verify(observer1, never()).onChanged(anyString()); - verify(observer2).onChanged("1"); + verify(mObserver3, never()).onChanged(anyString()); + verify(mObserver4).onChanged("1"); + } - // Now we set the other Lifecycle to be inactive, live data should become inactive. - reset(observer1); - reset(observer2); - mRegistry2.markState(Lifecycle.State.CREATED); - verify(mActiveObserversChanged).onCall(false); - verify(observer1, never()).onChanged(anyString()); - verify(observer2, never()).onChanged(anyString()); + /* + * Arrange: Two observed Lifecycles enter the CREATED state without sending an event. + * Act: Lifecycle's setValue method is called with new value. + * Assert: LiveData becomes inactive and nothing is dispatched to either observer. + */ + @Test + public void setValue_twoLifecyclesAreCreatedNoEvent() { + + // Arrange. + + mLiveData.observe(mOwner3, mObserver3); + mLiveData.observe(mOwner4, mObserver4); + + GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3); + GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START); + lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED); + when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.CREATED); - // Now we post another value, because both lifecycles are in the Created state, live data - // will not dispatch any values reset(mActiveObserversChanged); - mLiveData.setValue("2"); - verify(mActiveObserversChanged, never()).onCall(anyBoolean()); - verify(observer1, never()).onChanged(anyString()); - verify(observer2, never()).onChanged(anyString()); + reset(mObserver3); + reset(mObserver4); + + // Act. + + mLiveData.setValue("1"); + + // Assert. + + verify(mActiveObserversChanged).onCall(false); + verify(mObserver3, never()).onChanged(anyString()); + verify(mObserver3, never()).onChanged(anyString()); + } + + /* + * Arrange: LiveData was made inactive via SetValue (because both Lifecycles it was + * observing were in the CREATED state and no event was dispatched). + * Act: One Lifecycle enters STARTED state and dispatches lifecycle event. + * Assert: LiveData becomes active and dispatches new value to observer associated with started + * Lifecycle. + */ + @Test + public void test_liveDataInactiveViaSetValueThenOneLifecycleResumes() { + + // Arrange. + + mLiveData.observe(mOwner3, mObserver3); + mLiveData.observe(mOwner4, mObserver4); + + GenericLifecycleObserver lifecycleObserver3 = getGenericLifecycleObserver(mLifecycle3); + GenericLifecycleObserver lifecycleObserver4 = getGenericLifecycleObserver(mLifecycle4); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START); + lifecycleObserver4.onStateChanged(mOwner4, Lifecycle.Event.ON_START); + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.CREATED); + when(mLifecycle4.getCurrentState()).thenReturn(Lifecycle.State.CREATED); + + mLiveData.setValue("1"); - // Now that the first Lifecycle has been moved back to the Resumed state, the LiveData will - // be made active and it's associated Observer will be called with the new value, but the - // Observer associated with the Lifecycle that is still in the Created state won't be - // called. reset(mActiveObserversChanged); - mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + reset(mObserver3); + reset(mObserver4); + + // Act. + + when(mLifecycle3.getCurrentState()).thenReturn(Lifecycle.State.STARTED); + lifecycleObserver3.onStateChanged(mOwner3, Lifecycle.Event.ON_START); + + // Assert. + verify(mActiveObserversChanged).onCall(true); - verify(observer1).onChanged("2"); - verify(observer2, never()).onChanged(anyString()); + verify(mObserver3).onChanged("1"); + verify(mObserver4, never()).onChanged(anyString()); + } + + private GenericLifecycleObserver getGenericLifecycleObserver(Lifecycle lifecycle) { + ArgumentCaptor<GenericLifecycleObserver> captor = + ArgumentCaptor.forClass(GenericLifecycleObserver.class); + verify(lifecycle).addObserver(captor.capture()); + return (captor.getValue()); } @SuppressWarnings("WeakerAccess") diff --git a/android/arch/lifecycle/ViewModelProvider.java b/android/arch/lifecycle/ViewModelProvider.java index 29cbab8e..a7b3aeba 100644 --- a/android/arch/lifecycle/ViewModelProvider.java +++ b/android/arch/lifecycle/ViewModelProvider.java @@ -138,6 +138,7 @@ public class ViewModelProvider { */ public static class NewInstanceFactory implements Factory { + @SuppressWarnings("ClassNewInstance") @NonNull @Override public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { diff --git a/android/arch/persistence/room/EntityDeletionOrUpdateAdapter.java b/android/arch/persistence/room/EntityDeletionOrUpdateAdapter.java index 6f4aa68a..373b122d 100644 --- a/android/arch/persistence/room/EntityDeletionOrUpdateAdapter.java +++ b/android/arch/persistence/room/EntityDeletionOrUpdateAdapter.java @@ -45,6 +45,7 @@ public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteState * * @return An SQL query that can delete or update instances of T. */ + @Override protected abstract String createQuery(); /** diff --git a/android/arch/persistence/room/Room.java b/android/arch/persistence/room/Room.java index 2850b55e..9b168fce 100644 --- a/android/arch/persistence/room/Room.java +++ b/android/arch/persistence/room/Room.java @@ -72,6 +72,7 @@ public class Room { return new RoomDatabase.Builder<>(context, klass, null); } + @SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"}) @NonNull static <T, C> T getGeneratedImplementation(Class<C> klass, String suffix) { final String fullPackage = klass.getPackage().getName(); diff --git a/android/arch/persistence/room/testing/MigrationTestHelper.java b/android/arch/persistence/room/testing/MigrationTestHelper.java index 18e0a146..2e93bbe4 100644 --- a/android/arch/persistence/room/testing/MigrationTestHelper.java +++ b/android/arch/persistence/room/testing/MigrationTestHelper.java @@ -341,7 +341,7 @@ public class MigrationTestHelper extends TestWatcher { return 0; } - class MigratingDelegate extends RoomOpenHelperDelegate { + static class MigratingDelegate extends RoomOpenHelperDelegate { private final boolean mVerifyDroppedTables; MigratingDelegate(DatabaseBundle databaseBundle, boolean verifyDroppedTables) { diff --git a/android/bluetooth/BluetoothSocket.java b/android/bluetooth/BluetoothSocket.java index 4035ee1b..05699134 100644 --- a/android/bluetooth/BluetoothSocket.java +++ b/android/bluetooth/BluetoothSocket.java @@ -375,7 +375,7 @@ public final class BluetoothSocket implements Closeable { IBluetooth bluetoothProxy = BluetoothAdapter.getDefaultAdapter().getBluetoothService(null); if (bluetoothProxy == null) throw new IOException("Bluetooth is off"); - mPfd = bluetoothProxy.connectSocket(mDevice, mType, + mPfd = bluetoothProxy.getSocketManager().connectSocket(mDevice, mType, mUuid, mPort, getSecurityFlags()); synchronized (this) { if (DBG) Log.d(TAG, "connect(), SocketState: " + mSocketState + ", mPfd: " + mPfd); @@ -417,7 +417,7 @@ public final class BluetoothSocket implements Closeable { return -1; } try { - mPfd = bluetoothProxy.createSocketChannel(mType, mServiceName, + mPfd = bluetoothProxy.getSocketManager().createSocketChannel(mType, mServiceName, mUuid, mPort, getSecurityFlags()); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); diff --git a/android/content/AsyncTaskLoader.java b/android/content/AsyncTaskLoader.java index b7545bf5..6e9f09cb 100644 --- a/android/content/AsyncTaskLoader.java +++ b/android/content/AsyncTaskLoader.java @@ -49,7 +49,10 @@ import java.util.concurrent.Executor; * fragment} * * @param <D> the data type to be loaded. + * + * @deprecated Use {@link android.support.v4.content.AsyncTaskLoader} */ +@Deprecated public abstract class AsyncTaskLoader<D> extends Loader<D> { static final String TAG = "AsyncTaskLoader"; static final boolean DEBUG = false; diff --git a/android/content/ContentProviderClient.java b/android/content/ContentProviderClient.java index f8c139fe..2d490a03 100644 --- a/android/content/ContentProviderClient.java +++ b/android/content/ContentProviderClient.java @@ -22,6 +22,7 @@ import android.content.res.AssetFileDescriptor; import android.database.CrossProcessCursorWrapper; import android.database.Cursor; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; import android.os.DeadObjectException; @@ -102,8 +103,16 @@ public class ContentProviderClient implements AutoCloseable { if (sAnrHandler == null) { sAnrHandler = new Handler(Looper.getMainLooper(), null, true /* async */); } + + // If the remote process hangs, we're going to kill it, so we're + // technically okay doing blocking calls. + Binder.allowBlocking(mContentProvider.asBinder()); } else { mAnrRunnable = null; + + // If we're no longer watching for hangs, revert back to default + // blocking behavior. + Binder.defaultBlocking(mContentProvider.asBinder()); } } } @@ -511,6 +520,10 @@ public class ContentProviderClient implements AutoCloseable { private boolean closeInternal() { mCloseGuard.close(); if (mClosed.compareAndSet(false, true)) { + // We can't do ANR checks after we cease to exist! Reset any + // blocking behavior changes we might have made. + setDetectNotResponding(0); + if (mStable) { return mContentResolver.releaseProvider(mContentProvider); } else { diff --git a/android/content/Context.java b/android/content/Context.java index c165fb3e..19e24ad5 100644 --- a/android/content/Context.java +++ b/android/content/Context.java @@ -3413,6 +3413,8 @@ public abstract class Context { public static final String NETWORK_STATS_SERVICE = "netstats"; /** {@hide} */ public static final String NETWORK_POLICY_SERVICE = "netpolicy"; + /** {@hide} */ + public static final String NETWORK_WATCHLIST_SERVICE = "network_watchlist"; /** * Use with {@link #getSystemService} to retrieve a {@link @@ -4042,6 +4044,13 @@ public abstract class Context { public static final String STATS_COMPANION_SERVICE = "statscompanion"; /** + * Use with {@link #getSystemService} to retrieve an {@link android.stats.StatsManager}. + * @hide + */ + @SystemApi + public static final String STATS_MANAGER = "stats"; + + /** * Use with {@link #getSystemService} to retrieve a {@link * android.content.om.OverlayManager} for managing overlay packages. * @@ -4071,6 +4080,14 @@ public abstract class Context { public static final String TIME_ZONE_RULES_MANAGER_SERVICE = "timezone"; /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.content.pm.crossprofile.CrossProfileApps} for cross profile operations. + * + * @see #getSystemService + */ + public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/android/content/CursorLoader.java b/android/content/CursorLoader.java index c78871c3..33386e5c 100644 --- a/android/content/CursorLoader.java +++ b/android/content/CursorLoader.java @@ -38,7 +38,10 @@ import java.util.Arrays; * in the desired paramters with {@link #setUri(Uri)}, {@link #setSelection(String)}, * {@link #setSelectionArgs(String[])}, {@link #setSortOrder(String)}, * and {@link #setProjection(String[])}. + * + * @deprecated Use {@link android.support.v4.content.CursorLoader} */ +@Deprecated public class CursorLoader extends AsyncTaskLoader<Cursor> { final ForceLoadContentObserver mObserver; diff --git a/android/content/Intent.java b/android/content/Intent.java index e47de752..bad452ce 100644 --- a/android/content/Intent.java +++ b/android/content/Intent.java @@ -1728,6 +1728,9 @@ public class Intent implements Parcelable, Cloneable { * <p> * Output: If {@link #EXTRA_RETURN_RESULT}, returns whether the install * succeeded. + * <p> + * Requires {@link android.Manifest.permission#REQUEST_DELETE_PACKAGES} + * since {@link Build.VERSION_CODES#P}. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_UNINSTALL_PACKAGE = "android.intent.action.UNINSTALL_PACKAGE"; @@ -3444,11 +3447,12 @@ public class Intent implements Parcelable, Cloneable { /** * A broadcast action to trigger a factory reset. * - * <p> The sender must hold the {@link android.Manifest.permission#MASTER_CLEAR} permission. + * <p>The sender must hold the {@link android.Manifest.permission#MASTER_CLEAR} permission. The + * reason for the factory reset should be specified as {@link #EXTRA_REASON}. * * <p>Not for use by third-party applications. * - * @see #EXTRA_FORCE_MASTER_CLEAR + * @see #EXTRA_FORCE_FACTORY_RESET * * {@hide} */ @@ -4827,7 +4831,13 @@ public class Intent implements Parcelable, Cloneable { /** @hide */ public static final int EXTRA_TIME_PREF_VALUE_USE_LOCALE_DEFAULT = 2; - /** {@hide} */ + /** + * Intent extra: the reason that the operation associated with this intent is being performed. + * + * <p>Type: String + * @hide + */ + @SystemApi public static final String EXTRA_REASON = "android.intent.extra.REASON"; /** diff --git a/android/content/Loader.java b/android/content/Loader.java index 3faf13b6..80f9a14c 100644 --- a/android/content/Loader.java +++ b/android/content/Loader.java @@ -48,7 +48,10 @@ import java.io.PrintWriter; * </div> * * @param <D> The result returned when the load is complete + * + * @deprecated Use {@link android.support.v4.content.Loader} */ +@Deprecated public class Loader<D> { int mId; OnLoadCompleteListener<D> mListener; @@ -66,7 +69,10 @@ public class Loader<D> { * is told it has changed. You do not normally need to use this yourself; * it is used for you by {@link CursorLoader} to take care of executing * an update when the cursor's backing data changes. + * + * @deprecated Use {@link android.support.v4.content.Loader.ForceLoadContentObserver} */ + @Deprecated public final class ForceLoadContentObserver extends ContentObserver { public ForceLoadContentObserver() { super(new Handler()); @@ -90,7 +96,10 @@ public class Loader<D> { * to find out when a Loader it is managing has completed so that this can * be reported to its client. This interface should only be used if a * Loader is not being used in conjunction with LoaderManager. + * + * @deprecated Use {@link android.support.v4.content.Loader.OnLoadCompleteListener} */ + @Deprecated public interface OnLoadCompleteListener<D> { /** * Called on the thread that created the Loader when the load is complete. @@ -108,7 +117,10 @@ public class Loader<D> { * to find out when a Loader it is managing has been canceled so that it * can schedule the next Loader. This interface should only be used if a * Loader is not being used in conjunction with LoaderManager. + * + * @deprecated Use {@link android.support.v4.content.Loader.OnLoadCanceledListener} */ + @Deprecated public interface OnLoadCanceledListener<D> { /** * Called on the thread that created the Loader when the load is canceled. diff --git a/android/content/SharedPreferences.java b/android/content/SharedPreferences.java index 4b09feda..d3652e88 100644 --- a/android/content/SharedPreferences.java +++ b/android/content/SharedPreferences.java @@ -30,6 +30,11 @@ import java.util.Set; * when they are committed to storage. Objects that are returned from the * various <code>get</code> methods must be treated as immutable by the application. * + * <p>Note: This class provides strong consistency guarantees. It is using expensive operations + * which might slow down an app. Frequently changing properties or properties where loss can be + * tolerated should use other mechanisms. For more details read the comments on + * {@link Editor#commit()} and {@link Editor#apply()}. + * * <p><em>Note: This class does not support use across multiple processes.</em> * * <div class="special reference"> diff --git a/android/content/pm/ActivityInfo.java b/android/content/pm/ActivityInfo.java index 41667c4c..837c00a7 100644 --- a/android/content/pm/ActivityInfo.java +++ b/android/content/pm/ActivityInfo.java @@ -455,7 +455,6 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { */ public static final int FLAG_TURN_SCREEN_ON = 0x1000000; - /** * @hide Bit in {@link #flags}: If set, this component will only be seen * by the system user. Only works with broadcast receivers. Set from the @@ -1001,20 +1000,12 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { * Returns true if the activity's orientation is fixed. * @hide */ - public boolean isFixedOrientation() { + boolean isFixedOrientation() { return isFixedOrientationLandscape() || isFixedOrientationPortrait() || screenOrientation == SCREEN_ORIENTATION_LOCKED; } /** - * Returns true if the specified orientation is considered fixed. - * @hide - */ - static public boolean isFixedOrientation(int orientation) { - return isFixedOrientationLandscape(orientation) || isFixedOrientationPortrait(orientation); - } - - /** * Returns true if the activity's orientation is fixed to landscape. * @hide */ diff --git a/android/content/pm/LauncherApps.java b/android/content/pm/LauncherApps.java index b94a410b..9e54e235 100644 --- a/android/content/pm/LauncherApps.java +++ b/android/content/pm/LauncherApps.java @@ -282,13 +282,13 @@ public class LauncherApps { public static final int FLAG_GET_MANIFEST = FLAG_MATCH_MANIFEST; /** - * @hide include all pinned shortcuts by any launchers, not just by the caller, + * Include all pinned shortcuts by any launchers, not just by the caller, * in the result. - * If the caller doesn't havve the {@link android.Manifest.permission#ACCESS_SHORTCUTS} - * permission, this flag will be ignored. + * + * The caller must be the selected assistant app to use this flag, or have the system + * {@code ACCESS_SHORTCUTS} permission. */ - @TestApi - public static final int FLAG_MATCH_ALL_PINNED = 1 << 10; + public static final int FLAG_MATCH_PINNED_BY_ANY_LAUNCHER = 1 << 10; /** * FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED | FLAG_MATCH_MANIFEST @@ -302,7 +302,7 @@ public class LauncherApps { * @hide */ public static final int FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED = - FLAG_MATCH_ALL_KINDS | FLAG_MATCH_ALL_PINNED; + FLAG_MATCH_ALL_KINDS | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER; /** @hide kept for unit tests */ @Deprecated @@ -671,9 +671,13 @@ public class LauncherApps { } /** - * Returns whether the caller can access the shortcut information. + * Returns whether the caller can access the shortcut information. Access is currently + * available to: * - * <p>Only the default launcher can access the shortcut information. + * <ul> + * <li>The current launcher (or default launcher if there is no set current launcher).</li> + * <li>The currently active voice interaction service.</li> + * </ul> * * <p>Note when this method returns {@code false}, it may be a temporary situation because * the user is trying a new launcher application. The user may decide to change the default diff --git a/android/content/pm/PackageInstaller.java b/android/content/pm/PackageInstaller.java index f4fdcaa4..86288396 100644 --- a/android/content/pm/PackageInstaller.java +++ b/android/content/pm/PackageInstaller.java @@ -444,6 +444,9 @@ public class PackageInstaller { * @param packageName The package to uninstall. * @param statusReceiver Where to deliver the result. */ + @RequiresPermission(anyOf = { + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull String packageName, @NonNull IntentSender statusReceiver) { uninstall(packageName, 0 /*flags*/, statusReceiver); } @@ -476,6 +479,9 @@ public class PackageInstaller { * @param versionedPackage The versioned package to uninstall. * @param statusReceiver Where to deliver the result. */ + @RequiresPermission(anyOf = { + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull VersionedPackage versionedPackage, @NonNull IntentSender statusReceiver) { uninstall(versionedPackage, 0 /*flags*/, statusReceiver); @@ -1184,10 +1190,10 @@ public class PackageInstaller { } /** - * Sets the UID that initiated package installation. This is informational + * Sets the UID that initiated the package installation. This is informational * and may be used as a signal for anti-malware purposes. * - * @see PackageManager#EXTRA_VERIFICATION_INSTALLER_UID + * @see Intent#EXTRA_ORIGINATING_UID */ public void setOriginatingUid(int originatingUid) { this.originatingUid = originatingUid; diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java index b48829cf..1c5cf15d 100644 --- a/android/content/pm/PackageParser.java +++ b/android/content/pm/PackageParser.java @@ -102,6 +102,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; +import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -1708,13 +1709,33 @@ public class PackageParser { */ public static ApkLite parseApkLite(File apkFile, int flags) throws PackageParserException { - final String apkPath = apkFile.getAbsolutePath(); + return parseApkLiteInner(apkFile, null, null, flags); + } + + /** + * Utility method that retrieves lightweight details about a single APK + * file, including package name, split name, and install location. + * + * @param fd already open file descriptor of an apk file + * @param debugPathName arbitrary text name for this file, for debug output + * @param flags optional parse flags, such as + * {@link #PARSE_COLLECT_CERTIFICATES} + */ + public static ApkLite parseApkLite(FileDescriptor fd, String debugPathName, int flags) + throws PackageParserException { + return parseApkLiteInner(null, fd, debugPathName, flags); + } + + private static ApkLite parseApkLiteInner(File apkFile, FileDescriptor fd, String debugPathName, + int flags) throws PackageParserException { + final String apkPath = fd != null ? debugPathName : apkFile.getAbsolutePath(); AssetManager assets = null; XmlResourceParser parser = null; try { assets = newConfiguredAssetManager(); - int cookie = assets.addAssetPath(apkPath); + int cookie = fd != null + ? assets.addAssetFd(fd, debugPathName) : assets.addAssetPath(apkPath); if (cookie == 0) { throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK, "Failed to parse " + apkPath); diff --git a/android/content/pm/ShortcutServiceInternal.java b/android/content/pm/ShortcutServiceInternal.java index 7fc25d82..dadfaa9f 100644 --- a/android/content/pm/ShortcutServiceInternal.java +++ b/android/content/pm/ShortcutServiceInternal.java @@ -73,6 +73,9 @@ public abstract class ShortcutServiceInternal { public abstract boolean hasShortcutHostPermission(int launcherUserId, @NonNull String callingPackage, int callingPid, int callingUid); + public abstract void setShortcutHostPackage(@NonNull String type, @Nullable String packageName, + int userId); + public abstract boolean requestPinAppWidget(@NonNull String callingPackage, @NonNull AppWidgetProviderInfo appWidget, @Nullable Bundle extras, @Nullable IntentSender resultIntent, int userId); diff --git a/android/content/pm/crossprofile/CrossProfileApps.java b/android/content/pm/crossprofile/CrossProfileApps.java new file mode 100644 index 00000000..c441b5f3 --- /dev/null +++ b/android/content/pm/crossprofile/CrossProfileApps.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2017 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 android.content.pm.crossprofile; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; + +import java.util.List; + +/** + * Class for handling cross profile operations. Apps can use this class to interact with its + * instance in any profile that is in {@link #getTargetUserProfiles()}. For example, app can + * use this class to start its main activity in managed profile. + */ +public class CrossProfileApps { + private final Context mContext; + private final ICrossProfileApps mService; + + /** @hide */ + public CrossProfileApps(Context context, ICrossProfileApps service) { + mContext = context; + mService = service; + } + + /** + * Starts the specified main activity of the caller package in the specified profile. + * + * @param component The ComponentName of the activity to launch, it must be exported and has + * action {@link android.content.Intent#ACTION_MAIN}, category + * {@link android.content.Intent#CATEGORY_LAUNCHER}. Otherwise, SecurityException will + * be thrown. + * @param user The UserHandle of the profile, must be one of the users returned by + * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will + * be thrown. + * @param sourceBounds The Rect containing the source bounds of the clicked icon, see + * {@link android.content.Intent#setSourceBounds(Rect)}. + * @param startActivityOptions Options to pass to startActivity + */ + public void startMainActivity(@NonNull ComponentName component, @NonNull UserHandle user, + @Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions) { + try { + mService.startActivityAsUser(mContext.getPackageName(), + component, sourceBounds, startActivityOptions, user); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** + * Return a list of user profiles that that the caller can use when calling other APIs in this + * class. + * <p> + * A user profile would be considered as a valid target user profile, provided that: + * <ul> + * <li>It gets caller app installed</li> + * <li>It is not equal to the calling user</li> + * <li>It is in the same profile group of calling user profile</li> + * <li>It is enabled</li> + * </ul> + * + * @see UserManager#getUserProfiles() + */ + public @NonNull List<UserHandle> getTargetUserProfiles() { + try { + return mService.getTargetUserProfiles(mContext.getPackageName()); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } +} diff --git a/android/content/res/AssetManager.java b/android/content/res/AssetManager.java index f0adcd6c..78665609 100644 --- a/android/content/res/AssetManager.java +++ b/android/content/res/AssetManager.java @@ -28,8 +28,7 @@ import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; -import dalvik.annotation.optimization.FastNative; - +import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -694,7 +693,35 @@ public final class AssetManager implements AutoCloseable { private native final int addAssetPathNative(String path, boolean appAsLib); - /** + /** + * Add an additional set of assets to the asset manager from an already open + * FileDescriptor. Not for use by applications. + * This does not give full AssetManager functionality for these assets, + * since the origin of the file is not known for purposes of sharing, + * overlay resolution, and other features. However it does allow you + * to do simple access to the contents of the given fd as an apk file. + * Performs a dup of the underlying fd, so you must take care of still closing + * the FileDescriptor yourself (and can do that whenever you want). + * Returns the cookie of the added asset, or 0 on failure. + * {@hide} + */ + public int addAssetFd(FileDescriptor fd, String debugPathName) { + return addAssetFdInternal(fd, debugPathName, false); + } + + private int addAssetFdInternal(FileDescriptor fd, String debugPathName, + boolean appAsLib) { + synchronized (this) { + int res = addAssetFdNative(fd, debugPathName, appAsLib); + makeStringBlocks(mStringBlocks); + return res; + } + } + + private native int addAssetFdNative(FileDescriptor fd, String debugPathName, + boolean appAsLib); + + /** * Add a set of assets to overlay an already added set of assets. * * This is only intended for application resources. System wide resources diff --git a/android/content/res/Configuration.java b/android/content/res/Configuration.java index dfd3bbf0..26efda10 100644 --- a/android/content/res/Configuration.java +++ b/android/content/res/Configuration.java @@ -2105,6 +2105,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration break; case DENSITY_DPI_NONE: parts.add("nodpi"); + break; default: parts.add(config.densityDpi + "dpi"); break; diff --git a/android/content/res/FontResourcesParser.java b/android/content/res/FontResourcesParser.java index 28e9fce3..6a4aae66 100644 --- a/android/content/res/FontResourcesParser.java +++ b/android/content/res/FontResourcesParser.java @@ -80,13 +80,15 @@ public class FontResourcesParser { private int mWeight; private int mItalic; private int mTtcIndex; + private String mVariationSettings; private int mResourceId; public FontFileResourceEntry(@NonNull String fileName, int weight, int italic, - int ttcIndex) { + @Nullable String variationSettings, int ttcIndex) { mFileName = fileName; mWeight = weight; mItalic = italic; + mVariationSettings = variationSettings; mTtcIndex = ttcIndex; } @@ -102,6 +104,10 @@ public class FontResourcesParser { return mItalic; } + public @Nullable String getVariationSettings() { + return mVariationSettings; + } + public int getTtcIndex() { return mTtcIndex; } @@ -211,6 +217,8 @@ public class FontResourcesParser { Typeface.RESOLVE_BY_FONT_TABLE); int italic = array.getInt(R.styleable.FontFamilyFont_fontStyle, Typeface.RESOLVE_BY_FONT_TABLE); + String variationSettings = array.getString( + R.styleable.FontFamilyFont_fontVariationSettings); int ttcIndex = array.getInt(R.styleable.FontFamilyFont_ttcIndex, 0); String filename = array.getString(R.styleable.FontFamilyFont_font); array.recycle(); @@ -220,7 +228,7 @@ public class FontResourcesParser { if (filename == null) { return null; } - return new FontFileResourceEntry(filename, weight, italic, ttcIndex); + return new FontFileResourceEntry(filename, weight, italic, variationSettings, ttcIndex); } private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { diff --git a/android/content/res/ResourcesImpl.java b/android/content/res/ResourcesImpl.java index 386239cf..3239212a 100644 --- a/android/content/res/ResourcesImpl.java +++ b/android/content/res/ResourcesImpl.java @@ -49,6 +49,8 @@ import android.util.TypedValue; import android.util.Xml; import android.view.DisplayAdjustments; +import com.android.internal.util.GrowingArrayUtils; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -117,6 +119,13 @@ public class ResourcesImpl { private final ConfigurationBoundResourceCache<StateListAnimator> mStateListAnimatorCache = new ConfigurationBoundResourceCache<>(); + // A stack of all the resourceIds already referenced when parsing a resource. This is used to + // detect circular references in the xml. + // Using a ThreadLocal variable ensures that we have different stacks for multiple parallel + // calls to ResourcesImpl + private final ThreadLocal<LookupStack> mLookupStack = + ThreadLocal.withInitial(() -> new LookupStack()); + /** Size of the cyclical cache used to map XML files to blocks. */ private static final int XML_BLOCK_CACHE_SIZE = 4; @@ -784,19 +793,29 @@ public class ResourcesImpl { final Drawable dr; Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); + LookupStack stack = mLookupStack.get(); try { - if (file.endsWith(".xml")) { - final XmlResourceParser rp = loadXmlResourceParser( - file, id, value.assetCookie, "drawable"); - dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme); - rp.close(); - } else { - final InputStream is = mAssets.openNonAsset( - value.assetCookie, file, AssetManager.ACCESS_STREAMING); - dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); - is.close(); + // Perform a linear search to check if we have already referenced this resource before. + if (stack.contains(id)) { + throw new Exception("Recursive reference in drawable"); } - } catch (Exception | StackOverflowError e) { + stack.push(id); + try { + if (file.endsWith(".xml")) { + final XmlResourceParser rp = loadXmlResourceParser( + file, id, value.assetCookie, "drawable"); + dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme); + rp.close(); + } else { + final InputStream is = mAssets.openNonAsset( + value.assetCookie, file, AssetManager.ACCESS_STREAMING); + dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); + is.close(); + } + } finally { + stack.pop(); + } + } catch (Exception e) { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); final NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); @@ -1377,4 +1396,29 @@ public class ResourcesImpl { } } } + + private static class LookupStack { + + // Pick a reasonable default size for the array, it is grown as needed. + private int[] mIds = new int[4]; + private int mSize = 0; + + public void push(int id) { + mIds = GrowingArrayUtils.append(mIds, mSize, id); + mSize++; + } + + public boolean contains(int id) { + for (int i = 0; i < mSize; i++) { + if (mIds[i] == id) { + return true; + } + } + return false; + } + + public void pop() { + mSize--; + } + } } diff --git a/android/content/res/Resources_Delegate.java b/android/content/res/Resources_Delegate.java index d9c97fe2..a32d5282 100644 --- a/android/content/res/Resources_Delegate.java +++ b/android/content/res/Resources_Delegate.java @@ -887,30 +887,19 @@ public class Resources_Delegate { @LayoutlibDelegate static XmlResourceParser getXml(Resources resources, int id) throws NotFoundException { - Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag); - - if (value != null) { - String v = value.getSecond().getValue(); + Pair<String, ResourceValue> v = getResourceValue(resources, id, mPlatformResourceFlag); - if (v != null) { - // check this is a file - File f = new File(v); - if (f.isFile()) { - try { - XmlPullParser parser = ParserFactory.create(f); + if (v != null) { + ResourceValue value = v.getSecond(); - return new BridgeXmlBlockParser(parser, getContext(resources), - mPlatformResourceFlag[0]); - } catch (XmlPullParserException e) { - NotFoundException newE = new NotFoundException(); - newE.initCause(e); - throw newE; - } catch (FileNotFoundException e) { - NotFoundException newE = new NotFoundException(); - newE.initCause(e); - throw newE; - } - } + try { + return ResourceHelper.getXmlBlockParser(getContext(resources), value); + } catch (XmlPullParserException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to configure parser for " + value.getValue(), e, null /*data*/); + // we'll return null below. + } catch (FileNotFoundException e) { + // this shouldn't happen since we check above. } } diff --git a/android/database/MergeCursor.java b/android/database/MergeCursor.java index 2c25db76..272cfa24 100644 --- a/android/database/MergeCursor.java +++ b/android/database/MergeCursor.java @@ -17,7 +17,7 @@ package android.database; /** - * A convience class that lets you present an array of Cursors as a single linear Cursor. + * A convenience class that lets you present an array of Cursors as a single linear Cursor. * The schema of the cursors presented is entirely up to the creator of the MergeCursor, and * may be different if that is desired. Calls to getColumns, getColumnIndex, etc will return the * value for the row that the MergeCursor is currently pointing at. diff --git a/android/database/sqlite/SQLiteConnection.java b/android/database/sqlite/SQLiteConnection.java index c28583ea..2c93a7fe 100644 --- a/android/database/sqlite/SQLiteConnection.java +++ b/android/database/sqlite/SQLiteConnection.java @@ -289,12 +289,19 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private void setWalModeFromConfiguration() { if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { - if ((mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) { + final boolean walEnabled = + (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; + // Use compatibility WAL unless an app explicitly set journal/synchronous mode + final boolean useCompatibilityWal = mConfiguration.journalMode == null + && mConfiguration.syncMode == null && mConfiguration.useCompatibilityWal; + if (walEnabled || useCompatibilityWal) { setJournalMode("WAL"); setSyncMode(SQLiteGlobal.getWALSyncMode()); } else { - setJournalMode(SQLiteGlobal.getDefaultJournalMode()); - setSyncMode(SQLiteGlobal.getDefaultSyncMode()); + setJournalMode(mConfiguration.journalMode == null + ? SQLiteGlobal.getDefaultJournalMode() : mConfiguration.journalMode); + setSyncMode(mConfiguration.syncMode == null + ? SQLiteGlobal.getDefaultSyncMode() : mConfiguration.syncMode); } } } @@ -308,12 +315,10 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } private static String canonicalizeSyncMode(String value) { - if (value.equals("0")) { - return "OFF"; - } else if (value.equals("1")) { - return "NORMAL"; - } else if (value.equals("2")) { - return "FULL"; + switch (value) { + case "0": return "OFF"; + case "1": return "NORMAL"; + case "2": return "FULL"; } return value; } @@ -414,7 +419,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled != mConfiguration.foreignKeyConstraintsEnabled; boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags) - & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; + & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0 + || configuration.useCompatibilityWal != mConfiguration.useCompatibilityWal; boolean localeChanged = !configuration.locale.equals(mConfiguration.locale); // Update configuration parameters. diff --git a/android/database/sqlite/SQLiteCursor.java b/android/database/sqlite/SQLiteCursor.java index 2dc5ca43..13e6f718 100644 --- a/android/database/sqlite/SQLiteCursor.java +++ b/android/database/sqlite/SQLiteCursor.java @@ -22,6 +22,8 @@ import android.database.DatabaseUtils; import android.os.StrictMode; import android.util.Log; +import com.android.internal.util.Preconditions; + import java.util.HashMap; import java.util.Map; @@ -60,6 +62,9 @@ public class SQLiteCursor extends AbstractWindowedCursor { /** Used to find out where a cursor was allocated in case it never got released. */ private final Throwable mStackTrace; + /** Controls fetching of rows relative to requested position **/ + private boolean mFillWindowForwardOnly; + /** * Execute a query and provide access to its result set through a Cursor * interface. For a query such as: {@code SELECT name, birth, phone FROM @@ -136,18 +141,19 @@ public class SQLiteCursor extends AbstractWindowedCursor { private void fillWindow(int requiredPos) { clearOrCreateWindow(getDatabase().getPath()); - try { + Preconditions.checkArgumentNonnegative(requiredPos, + "requiredPos cannot be negative, but was " + requiredPos); + if (mCount == NO_COUNT) { - int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); - mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); + mCount = mQuery.fillWindow(mWindow, requiredPos, requiredPos, true); mCursorWindowCapacity = mWindow.getNumRows(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "received count(*) from native_fill_window: " + mCount); } } else { - int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, - mCursorWindowCapacity); + int startPos = mFillWindowForwardOnly ? requiredPos : DatabaseUtils + .cursorPickFillWindowStartPosition(requiredPos, mCursorWindowCapacity); mQuery.fillWindow(mWindow, startPos, requiredPos, false); } } catch (RuntimeException ex) { @@ -252,6 +258,20 @@ public class SQLiteCursor extends AbstractWindowedCursor { } /** + * Controls fetching of rows relative to requested position. + * + * <p>Calling this method defines how rows will be loaded, but it doesn't affect rows that + * are already in the window. This setting is preserved if a new window is + * {@link #setWindow(CursorWindow) set} + * + * @param fillWindowForwardOnly if true, rows will be fetched starting from requested position + * up to the window's capacity. Default value is false. + */ + public void setFillWindowForwardOnly(boolean fillWindowForwardOnly) { + mFillWindowForwardOnly = fillWindowForwardOnly; + } + + /** * Release the native resources, if they haven't been released yet. */ @Override diff --git a/android/database/sqlite/SQLiteDatabase.java b/android/database/sqlite/SQLiteDatabase.java index df0e262b..863fb198 100644 --- a/android/database/sqlite/SQLiteDatabase.java +++ b/android/database/sqlite/SQLiteDatabase.java @@ -262,7 +262,8 @@ public final class SQLiteDatabase extends SQLiteClosable { private SQLiteDatabase(final String path, final int openFlags, CursorFactory cursorFactory, DatabaseErrorHandler errorHandler, - int lookasideSlotSize, int lookasideSlotCount, long idleConnectionTimeoutMs) { + int lookasideSlotSize, int lookasideSlotCount, long idleConnectionTimeoutMs, + String journalMode, String syncMode) { mCursorFactory = cursorFactory; mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler(); mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags); @@ -285,6 +286,9 @@ public final class SQLiteDatabase extends SQLiteClosable { } } mConfigurationLocked.idleConnectionTimeoutMs = effectiveTimeoutMs; + mConfigurationLocked.journalMode = journalMode; + mConfigurationLocked.syncMode = syncMode; + mConfigurationLocked.useCompatibilityWal = SQLiteGlobal.isCompatibilityWalSupported(); } @Override @@ -720,7 +724,7 @@ public final class SQLiteDatabase extends SQLiteClosable { SQLiteDatabase db = new SQLiteDatabase(path, openParams.mOpenFlags, openParams.mCursorFactory, openParams.mErrorHandler, openParams.mLookasideSlotSize, openParams.mLookasideSlotCount, - openParams.mIdleConnectionTimeout); + openParams.mIdleConnectionTimeout, openParams.mJournalMode, openParams.mSyncMode); db.open(); return db; } @@ -746,7 +750,8 @@ public final class SQLiteDatabase extends SQLiteClosable { */ public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory, @DatabaseOpenFlags int flags, @Nullable DatabaseErrorHandler errorHandler) { - SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler, -1, -1, -1); + SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler, -1, -1, -1, null, + null); db.open(); return db; } @@ -2070,15 +2075,21 @@ public final class SQLiteDatabase extends SQLiteClosable { synchronized (mLock) { throwIfNotOpenLocked(); - if ((mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) == 0) { + final boolean oldUseCompatibilityWal = mConfigurationLocked.useCompatibilityWal; + final int oldFlags = mConfigurationLocked.openFlags; + if (!oldUseCompatibilityWal && (oldFlags & ENABLE_WRITE_AHEAD_LOGGING) == 0) { return; } mConfigurationLocked.openFlags &= ~ENABLE_WRITE_AHEAD_LOGGING; + // If an app explicitly disables WAL, do not even use compatibility mode + mConfigurationLocked.useCompatibilityWal = false; + try { mConnectionPoolLocked.reconfigure(mConfigurationLocked); } catch (RuntimeException ex) { - mConfigurationLocked.openFlags |= ENABLE_WRITE_AHEAD_LOGGING; + mConfigurationLocked.openFlags = oldFlags; + mConfigurationLocked.useCompatibilityWal = oldUseCompatibilityWal; throw ex; } } @@ -2295,17 +2306,21 @@ public final class SQLiteDatabase extends SQLiteClosable { private final DatabaseErrorHandler mErrorHandler; private final int mLookasideSlotSize; private final int mLookasideSlotCount; - private long mIdleConnectionTimeout; + private final long mIdleConnectionTimeout; + private final String mJournalMode; + private final String mSyncMode; private OpenParams(int openFlags, CursorFactory cursorFactory, DatabaseErrorHandler errorHandler, int lookasideSlotSize, int lookasideSlotCount, - long idleConnectionTimeout) { + long idleConnectionTimeout, String journalMode, String syncMode) { mOpenFlags = openFlags; mCursorFactory = cursorFactory; mErrorHandler = errorHandler; mLookasideSlotSize = lookasideSlotSize; mLookasideSlotCount = lookasideSlotCount; mIdleConnectionTimeout = idleConnectionTimeout; + mJournalMode = journalMode; + mSyncMode = syncMode; } /** @@ -2372,6 +2387,28 @@ public final class SQLiteDatabase extends SQLiteClosable { } /** + * Returns <a href="https://sqlite.org/pragma.html#pragma_journal_mode">journal mode</a>. + * This journal mode will only be used if {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} + * flag is not set, otherwise a platform will use "WAL" journal mode. + * @see Builder#setJournalMode(String) + */ + @Nullable + public String getJournalMode() { + return mJournalMode; + } + + /** + * Returns <a href="https://sqlite.org/pragma.html#pragma_synchronous">synchronous mode</a>. + * This value will only be used when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag + * is not set, otherwise a system wide default will be used. + * @see Builder#setSynchronousMode(String) + */ + @Nullable + public String getSynchronousMode() { + return mSyncMode; + } + + /** * Creates a new instance of builder {@link Builder#Builder(OpenParams) initialized} with * {@code this} parameters. * @hide @@ -2391,6 +2428,8 @@ public final class SQLiteDatabase extends SQLiteClosable { private int mOpenFlags; private CursorFactory mCursorFactory; private DatabaseErrorHandler mErrorHandler; + private String mJournalMode; + private String mSyncMode; public Builder() { } @@ -2401,6 +2440,8 @@ public final class SQLiteDatabase extends SQLiteClosable { mOpenFlags = params.mOpenFlags; mCursorFactory = params.mCursorFactory; mErrorHandler = params.mErrorHandler; + mJournalMode = params.mJournalMode; + mSyncMode = params.mSyncMode; } /** @@ -2532,6 +2573,30 @@ public final class SQLiteDatabase extends SQLiteClosable { return this; } + + /** + * Sets <a href="https://sqlite.org/pragma.html#pragma_journal_mode">journal mode</a> + * to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag is not set. + */ + @NonNull + public Builder setJournalMode(@NonNull String journalMode) { + Preconditions.checkNotNull(journalMode); + mJournalMode = journalMode; + return this; + } + + /** + * Sets <a href="https://sqlite.org/pragma.html#pragma_synchronous">synchronous mode</a> + * to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag is not set. + * @return + */ + @NonNull + public Builder setSynchronousMode(@NonNull String syncMode) { + Preconditions.checkNotNull(syncMode); + mSyncMode = syncMode; + return this; + } + /** * Creates an instance of {@link OpenParams} with the options that were previously set * on this builder @@ -2539,7 +2604,7 @@ public final class SQLiteDatabase extends SQLiteClosable { @NonNull public OpenParams build() { return new OpenParams(mOpenFlags, mCursorFactory, mErrorHandler, mLookasideSlotSize, - mLookasideSlotCount, mIdleConnectionTimeout); + mLookasideSlotCount, mIdleConnectionTimeout, mJournalMode, mSyncMode); } } } @@ -2554,4 +2619,6 @@ public final class SQLiteDatabase extends SQLiteClosable { }) @Retention(RetentionPolicy.SOURCE) public @interface DatabaseOpenFlags {} + } + diff --git a/android/database/sqlite/SQLiteDatabaseConfiguration.java b/android/database/sqlite/SQLiteDatabaseConfiguration.java index 34c9b339..a14df1eb 100644 --- a/android/database/sqlite/SQLiteDatabaseConfiguration.java +++ b/android/database/sqlite/SQLiteDatabaseConfiguration.java @@ -111,6 +111,27 @@ public final class SQLiteDatabaseConfiguration { public long idleConnectionTimeoutMs = Long.MAX_VALUE; /** + * Enables compatibility WAL mode. Applications cannot explicitly choose compatibility WAL mode, + * therefore it is not exposed as a flag. + * + * <p>In this mode, only database journal mode will be changed, connection pool + * size will still be limited to a single connection. + */ + public boolean useCompatibilityWal; + + /** + * Journal mode to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} is not set. + * <p>Default is returned by {@link SQLiteGlobal#getDefaultJournalMode()} + */ + public String journalMode; + + /** + * Synchronous mode to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} is not set. + * <p>Default is returned by {@link SQLiteGlobal#getDefaultSyncMode()} + */ + public String syncMode; + + /** * Creates a database configuration with the required parameters for opening a * database and default values for all other parameters. * @@ -170,6 +191,9 @@ public final class SQLiteDatabaseConfiguration { lookasideSlotSize = other.lookasideSlotSize; lookasideSlotCount = other.lookasideSlotCount; idleConnectionTimeoutMs = other.idleConnectionTimeoutMs; + useCompatibilityWal = other.useCompatibilityWal; + journalMode = other.journalMode; + syncMode = other.syncMode; } /** diff --git a/android/database/sqlite/SQLiteGlobal.java b/android/database/sqlite/SQLiteGlobal.java index 94d5555c..d6d9764c 100644 --- a/android/database/sqlite/SQLiteGlobal.java +++ b/android/database/sqlite/SQLiteGlobal.java @@ -81,6 +81,16 @@ public final class SQLiteGlobal { } /** + * Returns true if compatibility WAL mode is supported. In this mode, only + * database journal mode is changed. Connection pool will use at most one connection. + */ + public static boolean isCompatibilityWalSupported() { + return SystemProperties.getBoolean("debug.sqlite.compatibility_wal_supported", + Resources.getSystem().getBoolean( + com.android.internal.R.bool.db_compatibility_wal_supported)); + } + + /** * Gets the journal size limit in bytes. */ public static int getJournalSizeLimit() { diff --git a/android/database/sqlite/SQLiteOpenHelper.java b/android/database/sqlite/SQLiteOpenHelper.java index cc9e0f4d..49f357e6 100644 --- a/android/database/sqlite/SQLiteOpenHelper.java +++ b/android/database/sqlite/SQLiteOpenHelper.java @@ -17,6 +17,8 @@ package android.database.sqlite; import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.database.DatabaseErrorHandler; import android.database.SQLException; @@ -24,6 +26,8 @@ import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.os.FileUtils; import android.util.Log; +import com.android.internal.util.Preconditions; + import java.io.File; /** @@ -69,7 +73,8 @@ public abstract class SQLiteOpenHelper { * {@link #onUpgrade} will be used to upgrade the database; if the database is * newer, {@link #onDowngrade} will be used to downgrade the database */ - public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) { + public SQLiteOpenHelper(@Nullable Context context, @Nullable String name, + @Nullable CursorFactory factory, int version) { this(context, name, factory, version, null); } @@ -90,12 +95,33 @@ public abstract class SQLiteOpenHelper { * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database * corruption, or null to use the default error handler. */ - public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, - DatabaseErrorHandler errorHandler) { + public SQLiteOpenHelper(@Nullable Context context, @Nullable String name, + @Nullable CursorFactory factory, int version, + @Nullable DatabaseErrorHandler errorHandler) { this(context, name, factory, version, 0, errorHandler); } /** + * Create a helper object to create, open, and/or manage a database. + * This method always returns very quickly. The database is not actually + * created or opened until one of {@link #getWritableDatabase} or + * {@link #getReadableDatabase} is called. + * + * @param context to use to open or create the database + * @param name of the database file, or null for an in-memory database + * @param version number of the database (starting at 1); if the database is older, + * {@link #onUpgrade} will be used to upgrade the database; if the database is + * newer, {@link #onDowngrade} will be used to downgrade the database + * @param openParams configuration parameters that are used for opening {@link SQLiteDatabase}. + * Please note that {@link SQLiteDatabase#CREATE_IF_NECESSARY} flag will always be + * set when the helper opens the database + */ + public SQLiteOpenHelper(@Nullable Context context, @Nullable String name, int version, + @NonNull SQLiteDatabase.OpenParams openParams) { + this(context, name, version, 0, openParams.toBuilder()); + } + + /** * Same as {@link #SQLiteOpenHelper(Context, String, CursorFactory, int, DatabaseErrorHandler)} * but also accepts an integer minimumSupportedVersion as a convenience for upgrading very old * versions of this database that are no longer supported. If a database with older version that @@ -118,17 +144,26 @@ public abstract class SQLiteOpenHelper { * @see #onUpgrade(SQLiteDatabase, int, int) * @hide */ - public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, - int minimumSupportedVersion, DatabaseErrorHandler errorHandler) { + public SQLiteOpenHelper(@Nullable Context context, @Nullable String name, + @Nullable CursorFactory factory, int version, + int minimumSupportedVersion, @Nullable DatabaseErrorHandler errorHandler) { + this(context, name, version, minimumSupportedVersion, + new SQLiteDatabase.OpenParams.Builder()); + mOpenParamsBuilder.setCursorFactory(factory); + mOpenParamsBuilder.setErrorHandler(errorHandler); + } + + private SQLiteOpenHelper(@Nullable Context context, @Nullable String name, int version, + int minimumSupportedVersion, + @NonNull SQLiteDatabase.OpenParams.Builder openParamsBuilder) { + Preconditions.checkNotNull(openParamsBuilder); if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version); mContext = context; mName = name; mNewVersion = version; mMinimumSupportedVersion = Math.max(0, minimumSupportedVersion); - mOpenParamsBuilder = new SQLiteDatabase.OpenParams.Builder(); - mOpenParamsBuilder.setCursorFactory(factory); - mOpenParamsBuilder.setErrorHandler(errorHandler); + mOpenParamsBuilder = openParamsBuilder; mOpenParamsBuilder.addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY); } diff --git a/android/ext/services/notification/Assistant.java b/android/ext/services/notification/Assistant.java index f535368b..6fe89755 100644 --- a/android/ext/services/notification/Assistant.java +++ b/android/ext/services/notification/Assistant.java @@ -23,16 +23,37 @@ import static android.service.notification.NotificationListenerService.Ranking import android.app.INotificationManager; import android.content.Context; import android.ext.services.R; +import android.os.AsyncTask; import android.os.Bundle; +import android.os.Environment; +import android.os.storage.StorageManager; import android.service.notification.Adjustment; import android.service.notification.NotificationAssistantService; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; +import android.util.AtomicFile; import android.util.Log; import android.util.Slog; +import android.util.Xml; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.XmlUtils; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Map; /** * Notification assistant that provides guidance on notification channel blocking @@ -41,19 +62,112 @@ public class Assistant extends NotificationAssistantService { private static final String TAG = "ExtAssistant"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final ArrayList<Integer> DISMISS_WITH_PREJUDICE = new ArrayList<>(); + private static final String TAG_ASSISTANT = "assistant"; + private static final String TAG_IMPRESSION = "impression-set"; + private static final String ATT_KEY = "key"; + private static final int DB_VERSION = 1; + private static final String ATTR_VERSION = "version"; + + private static final ArrayList<Integer> PREJUDICAL_DISMISSALS = new ArrayList<>(); static { - DISMISS_WITH_PREJUDICE.add(REASON_CANCEL); - DISMISS_WITH_PREJUDICE.add(REASON_LISTENER_CANCEL); + PREJUDICAL_DISMISSALS.add(REASON_CANCEL); + PREJUDICAL_DISMISSALS.add(REASON_LISTENER_CANCEL); } // key : impressions tracker - // TODO: persist across reboots + // TODO: prune deleted channels and apps ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>(); // SBN key : channel id ArrayMap<String, String> mLiveNotifications = new ArrayMap<>(); private Ranking mFakeRanking = null; + private AtomicFile mFile = null; + + public Assistant() { + } + + private void loadFile() { + if (DEBUG) Slog.d(TAG, "loadFile"); + AsyncTask.execute(() -> { + InputStream infile = null; + try { + infile = mFile.openRead(); + readXml(infile); + } catch (FileNotFoundException e) { + // No data yet + } catch (IOException e) { + Log.e(TAG, "Unable to read channel impressions", e); + } catch (NumberFormatException | XmlPullParserException e) { + Log.e(TAG, "Unable to parse channel impressions", e); + } finally { + IoUtils.closeQuietly(infile); + } + }); + } + + protected void readXml(InputStream stream) + throws XmlPullParserException, NumberFormatException, IOException { + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, StandardCharsets.UTF_8.name()); + final int outerDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + if (!TAG_ASSISTANT.equals(parser.getName())) { + continue; + } + final int impressionOuterDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, impressionOuterDepth)) { + if (!TAG_IMPRESSION.equals(parser.getName())) { + continue; + } + String key = parser.getAttributeValue(null, ATT_KEY); + ChannelImpressions ci = new ChannelImpressions(); + ci.populateFromXml(parser); + synchronized (mkeyToImpressions) { + ci.append(mkeyToImpressions.get(key)); + mkeyToImpressions.put(key, ci); + } + } + } + } + + private void saveFile() throws IOException { + AsyncTask.execute(() -> { + final FileOutputStream stream; + try { + stream = mFile.startWrite(); + } catch (IOException e) { + Slog.w(TAG, "Failed to save policy file", e); + return; + } + try { + final XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, StandardCharsets.UTF_8.name()); + writeXml(out); + mFile.finishWrite(stream); + } catch (IOException e) { + Slog.w(TAG, "Failed to save impressions file, restoring backup", e); + mFile.failWrite(stream); + } + }); + } + + protected void writeXml(XmlSerializer out) throws IOException { + out.startDocument(null, true); + out.startTag(null, TAG_ASSISTANT); + out.attribute(null, ATTR_VERSION, Integer.toString(DB_VERSION)); + synchronized (mkeyToImpressions) { + for (Map.Entry<String, ChannelImpressions> entry + : mkeyToImpressions.entrySet()) { + // TODO: ensure channel still exists + out.startTag(null, TAG_IMPRESSION); + out.attribute(null, ATT_KEY, entry.getKey()); + entry.getValue().writeXml(out); + out.endTag(null, TAG_IMPRESSION); + } + } + out.endTag(null, TAG_ASSISTANT); + out.endDocument(); + } @Override public Adjustment onNotificationEnqueued(StatusBarNotification sbn) { @@ -87,26 +201,38 @@ public class Assistant extends NotificationAssistantService { public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, NotificationStats stats, int reason) { try { + boolean updatedImpressions = false; String channelId = mLiveNotifications.remove(sbn.getKey()); String key = getKey(sbn.getPackageName(), sbn.getUserId(), channelId); - ChannelImpressions ci = mkeyToImpressions.getOrDefault(key, new ChannelImpressions()); - if (stats.hasSeen()) { - ci.incrementViews(); + synchronized (mkeyToImpressions) { + ChannelImpressions ci = mkeyToImpressions.getOrDefault(key, + new ChannelImpressions()); + if (stats.hasSeen()) { + ci.incrementViews(); + updatedImpressions = true; + } + if (PREJUDICAL_DISMISSALS.contains(reason)) { + if ((!sbn.isAppGroup() || sbn.getNotification().isGroupChild()) + && !stats.hasInteracted() + && stats.getDismissalSurface() != NotificationStats.DISMISSAL_AOD + && stats.getDismissalSurface() != NotificationStats.DISMISSAL_PEEK + && stats.getDismissalSurface() != NotificationStats.DISMISSAL_OTHER) { + if (DEBUG) Log.i(TAG, "increment dismissals " + key); + ci.incrementDismissals(); + updatedImpressions = true; + } else { + if (DEBUG) Slog.i(TAG, "reset streak " + key); + if (ci.getStreak() > 0) { + updatedImpressions = true; + } + ci.resetStreak(); + } + } + mkeyToImpressions.put(key, ci); } - if (DISMISS_WITH_PREJUDICE.contains(reason) - && !sbn.isAppGroup() - && !sbn.getNotification().isGroupChild() - && !stats.hasInteracted() - && stats.getDismissalSurface() != NotificationStats.DISMISSAL_AOD - && stats.getDismissalSurface() != NotificationStats.DISMISSAL_PEEK - && stats.getDismissalSurface() != NotificationStats.DISMISSAL_OTHER) { - if (DEBUG) Log.i(TAG, "increment dismissals"); - ci.incrementDismissals(); - } else { - if (DEBUG) Slog.i(TAG, "reset streak"); - ci.resetStreak(); + if (updatedImpressions) { + saveFile(); } - mkeyToImpressions.put(key, ci); } catch (Throwable e) { Slog.e(TAG, "Error occurred processing removal", e); } @@ -121,6 +247,11 @@ public class Assistant extends NotificationAssistantService { public void onListenerConnected() { if (DEBUG) Log.i(TAG, "CONNECTED"); try { + mFile = new AtomicFile(new File(new File( + Environment.getDataUserCePackageDirectory( + StorageManager.UUID_PRIVATE_INTERNAL, getUserId(), getPackageName()), + "assistant"), "block_stats.xml")); + loadFile(); for (StatusBarNotification sbn : getActiveNotifications()) { onNotificationPosted(sbn); } @@ -129,7 +260,7 @@ public class Assistant extends NotificationAssistantService { } } - private String getKey(String pkg, int userId, String channelId) { + protected String getKey(String pkg, int userId, String channelId) { return pkg + "|" + userId + "|" + channelId; } @@ -151,6 +282,11 @@ public class Assistant extends NotificationAssistantService { } // for testing + + protected void setFile(AtomicFile file) { + mFile = file; + } + protected void setFakeRanking(Ranking ranking) { mFakeRanking = ranking; } @@ -162,4 +298,16 @@ public class Assistant extends NotificationAssistantService { protected void setContext(Context context) { mSystemContext = context; } + + protected ChannelImpressions getImpressions(String key) { + synchronized (mkeyToImpressions) { + return mkeyToImpressions.get(key); + } + } + + protected void insertImpressions(String key, ChannelImpressions ci) { + synchronized (mkeyToImpressions) { + mkeyToImpressions.put(key, ci); + } + } }
\ No newline at end of file diff --git a/android/ext/services/notification/ChannelImpressions.java b/android/ext/services/notification/ChannelImpressions.java index 30567ccd..4ad4b241 100644 --- a/android/ext/services/notification/ChannelImpressions.java +++ b/android/ext/services/notification/ChannelImpressions.java @@ -18,14 +18,23 @@ package android.ext.services.notification; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; import android.util.Log; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; + public final class ChannelImpressions implements Parcelable { private static final String TAG = "ExtAssistant.CI"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); static final double DISMISS_TO_VIEW_RATIO_LIMIT = .8; static final int STREAK_LIMIT = 2; + static final String ATT_DISMISSALS = "dismisses"; + static final String ATT_VIEWS = "views"; + static final String ATT_STREAK = "streak"; private int mDismissals = 0; private int mViews = 0; @@ -62,6 +71,14 @@ public final class ChannelImpressions implements Parcelable { mStreak++; } + public void append(ChannelImpressions additionalImpressions) { + if (additionalImpressions != null) { + mViews += additionalImpressions.getViews(); + mStreak += additionalImpressions.getStreak(); + mDismissals += additionalImpressions.getDismissals(); + } + } + public void incrementViews() { mViews++; } @@ -134,4 +151,36 @@ public final class ChannelImpressions implements Parcelable { sb.append('}'); return sb.toString(); } + + protected void populateFromXml(XmlPullParser parser) { + mDismissals = safeInt(parser, ATT_DISMISSALS, 0); + mStreak = safeInt(parser, ATT_STREAK, 0); + mViews = safeInt(parser, ATT_VIEWS, 0); + } + + protected void writeXml(XmlSerializer out) throws IOException { + if (mDismissals != 0) { + out.attribute(null, ATT_DISMISSALS, String.valueOf(mDismissals)); + } + if (mStreak != 0) { + out.attribute(null, ATT_STREAK, String.valueOf(mStreak)); + } + if (mViews != 0) { + out.attribute(null, ATT_VIEWS, String.valueOf(mViews)); + } + } + + private static int safeInt(XmlPullParser parser, String att, int defValue) { + final String val = parser.getAttributeValue(null, att); + return tryParseInt(val, defValue); + } + + private static int tryParseInt(String value, int defValue) { + if (TextUtils.isEmpty(value)) return defValue; + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defValue; + } + } } diff --git a/android/graphics/Paint.java b/android/graphics/Paint.java index 1a06a568..317144a2 100644 --- a/android/graphics/Paint.java +++ b/android/graphics/Paint.java @@ -88,7 +88,7 @@ public class Paint { * A map from a string representation of the LocaleList to Minikin's language list ID. */ @GuardedBy("sCacheLock") - private static final HashMap<String, Integer> sMinikinLangListIdCache = new HashMap<>(); + private static final HashMap<String, Integer> sMinikinLocaleListIdCache = new HashMap<>(); /** * @hide @@ -1445,16 +1445,16 @@ public class Paint { private void syncTextLocalesWithMinikin() { final String languageTags = mLocales.toLanguageTags(); - final Integer minikinLangListId; + final Integer minikinLocaleListId; synchronized (sCacheLock) { - minikinLangListId = sMinikinLangListIdCache.get(languageTags); - if (minikinLangListId == null) { + minikinLocaleListId = sMinikinLocaleListIdCache.get(languageTags); + if (minikinLocaleListId == null) { final int newID = nSetTextLocales(mNativePaint, languageTags); - sMinikinLangListIdCache.put(languageTags, newID); + sMinikinLocaleListIdCache.put(languageTags, newID); return; } } - nSetTextLocalesByMinikinLangListId(mNativePaint, minikinLangListId.intValue()); + nSetTextLocalesByMinikinLocaleListId(mNativePaint, minikinLocaleListId.intValue()); } /** @@ -2918,8 +2918,8 @@ public class Paint { @CriticalNative private static native void nSetTextAlign(long paintPtr, int align); @CriticalNative - private static native void nSetTextLocalesByMinikinLangListId(long paintPtr, - int mMinikinLangListId); + private static native void nSetTextLocalesByMinikinLocaleListId(long paintPtr, + int mMinikinLocaleListId); @CriticalNative private static native void nSetShadowLayer(long paintPtr, float radius, float dx, float dy, int color); diff --git a/android/graphics/Paint_Delegate.java b/android/graphics/Paint_Delegate.java index ef452034..62427020 100644 --- a/android/graphics/Paint_Delegate.java +++ b/android/graphics/Paint_Delegate.java @@ -950,7 +950,7 @@ public class Paint_Delegate { } @LayoutlibDelegate - /*package*/ static void nSetTextLocalesByMinikinLangListId(long paintPtr, + /*package*/ static void nSetTextLocalesByMinikinLocaleListId(long paintPtr, int mMinikinLangListId) { // FIXME } diff --git a/android/graphics/Rect.java b/android/graphics/Rect.java index 3dc928de..aff942da 100644 --- a/android/graphics/Rect.java +++ b/android/graphics/Rect.java @@ -475,6 +475,19 @@ public final class Rect implements Parcelable { } /** + * If the specified rectangle intersects this rectangle, set this rectangle to that + * intersection, otherwise set this rectangle to the empty rectangle. + * @see #inset(int, int, int, int) but without checking if the rects overlap. + * @hide + */ + public void intersectUnchecked(Rect other) { + left = Math.max(left, other.left); + top = Math.max(top, other.top); + right = Math.min(right, other.right); + bottom = Math.min(bottom, other.bottom); + } + + /** * If rectangles a and b intersect, return true and set this rectangle to * that intersection, otherwise return false and do not change this * rectangle. No check is performed to see if either rectangle is empty. diff --git a/android/graphics/Typeface.java b/android/graphics/Typeface.java index 9961ed64..3d65bd22 100644 --- a/android/graphics/Typeface.java +++ b/android/graphics/Typeface.java @@ -250,10 +250,10 @@ public class Typeface { FontFamily fontFamily = new FontFamily(); for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) { - // TODO: Add variation font support. (b/37853920) if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(), 0 /* resourceCookie */, false /* isAsset */, fontFile.getTtcIndex(), - fontFile.getWeight(), fontFile.getItalic(), null /* axes */)) { + fontFile.getWeight(), fontFile.getItalic(), + FontVariationAxis.fromFontVariationSettings(fontFile.getVariationSettings()))) { return null; } } diff --git a/android/graphics/Typeface_Delegate.java b/android/graphics/Typeface_Delegate.java index b9c03531..d793adee 100644 --- a/android/graphics/Typeface_Delegate.java +++ b/android/graphics/Typeface_Delegate.java @@ -16,19 +16,32 @@ package android.graphics; +import com.android.SdkConstants; import com.android.ide.common.rendering.api.LayoutLog; import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; +import com.android.layoutlib.bridge.android.RenderParamsFlags; import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.layoutlib.bridge.impl.ParserFactory; +import com.android.layoutlib.bridge.impl.RenderAction; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.res.FontResourcesParser; import android.graphics.FontFamily_Delegate.FontVariant; import android.graphics.fonts.FontVariationAxis; import android.text.FontConfig; import android.util.ArrayMap; import java.awt.Font; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import java.lang.ref.SoftReference; import java.nio.ByteBuffer; import java.nio.file.Files; @@ -231,12 +244,72 @@ public final class Typeface_Delegate { return fontFamily; } + /** + * Loads a single font or font family from disk + */ + @Nullable + public static Typeface createFromDisk(@NonNull BridgeContext context, @NonNull String path, + boolean isFramework) { + // Check if this is an asset that we've already loaded dynamically + Typeface typeface = Typeface.findFromCache(context.getAssets(), path); + if (typeface != null) { + return typeface; + } + + String lowerCaseValue = path.toLowerCase(); + if (lowerCaseValue.endsWith(SdkConstants.DOT_XML)) { + // create a block parser for the file + Boolean psiParserSupport = context.getLayoutlibCallback().getFlag( + RenderParamsFlags.FLAG_KEY_XML_FILE_PARSER_SUPPORT); + XmlPullParser parser = null; + if (psiParserSupport != null && psiParserSupport) { + parser = context.getLayoutlibCallback().getXmlFileParser(path); + } else { + File f = new File(path); + if (f.isFile()) { + try { + parser = ParserFactory.create(f); + } catch (XmlPullParserException | FileNotFoundException e) { + // this is an error and not warning since the file existence is checked + // before + // attempting to parse it. + Bridge.getLog().error(null, "Failed to parse file " + path, e, + null /*data*/); + } + } + } + + if (parser != null) { + BridgeXmlBlockParser blockParser = + new BridgeXmlBlockParser(parser, context, isFramework); + try { + FontResourcesParser.FamilyResourceEntry entry = + FontResourcesParser.parse(blockParser, context.getResources()); + typeface = Typeface.createFromResources(entry, context.getAssets(), path); + } catch (XmlPullParserException | IOException e) { + Bridge.getLog().error(null, "Failed to parse file " + path, e, null /*data*/); + } finally { + blockParser.ensurePopped(); + } + } else { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + String.format("File %s does not exist (or is not a file)", path), + null /*data*/); + } + } else { + typeface = Typeface.createFromResources(context.getAssets(), path, 0); + } + + return typeface; + } + @LayoutlibDelegate /*package*/ static Typeface create(String familyName, int style) { if (familyName != null && Files.exists(Paths.get(familyName))) { // Workaround for b/64137851 // Support lib will call this method after failing to create the TypefaceCompat. - return Typeface.createFromFile(familyName); + return Typeface_Delegate.createFromDisk(RenderAction.getCurrentContext(), familyName, + false); } return Typeface.create_Original(familyName, style); } diff --git a/android/graphics/drawable/AnimatedVectorDrawable.java b/android/graphics/drawable/AnimatedVectorDrawable.java index 90d6ab86..e74dc6dc 100644 --- a/android/graphics/drawable/AnimatedVectorDrawable.java +++ b/android/graphics/drawable/AnimatedVectorDrawable.java @@ -132,7 +132,7 @@ import dalvik.annotation.optimization.FastNative; * <td>translateY</td> * </tr> * <tr> - * <td rowspan="8"><path></td> + * <td rowspan="9"><path></td> * <td>pathData</td> * </tr> * <tr> @@ -154,6 +154,9 @@ import dalvik.annotation.optimization.FastNative; * <td>trimPathStart</td> * </tr> * <tr> + * <td>trimPathEnd</td> + * </tr> + * <tr> * <td>trimPathOffset</td> * </tr> * <tr> diff --git a/android/graphics/drawable/RippleBackground.java b/android/graphics/drawable/RippleBackground.java index 3bf4f902..dea194e4 100644 --- a/android/graphics/drawable/RippleBackground.java +++ b/android/graphics/drawable/RippleBackground.java @@ -36,138 +36,69 @@ class RippleBackground extends RippleComponent { private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); - private static final int OPACITY_ENTER_DURATION = 600; - private static final int OPACITY_ENTER_DURATION_FAST = 120; - private static final int OPACITY_EXIT_DURATION = 480; + private static final int OPACITY_DURATION = 80; - // Hardware rendering properties. - private CanvasProperty<Paint> mPropPaint; - private CanvasProperty<Float> mPropRadius; - private CanvasProperty<Float> mPropX; - private CanvasProperty<Float> mPropY; + private ObjectAnimator mAnimator; - // Software rendering properties. private float mOpacity = 0; /** Whether this ripple is bounded. */ private boolean mIsBounded; - public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded, - boolean forceSoftware) { - super(owner, bounds, forceSoftware); + private boolean mFocused = false; + private boolean mHovered = false; + + public RippleBackground(RippleDrawable owner, Rect bounds, boolean isBounded) { + super(owner, bounds); mIsBounded = isBounded; } public boolean isVisible() { - return mOpacity > 0 || isHardwareAnimating(); + return mOpacity > 0; } - @Override - protected boolean drawSoftware(Canvas c, Paint p) { - boolean hasContent = false; - + public void draw(Canvas c, Paint p) { final int origAlpha = p.getAlpha(); - final int alpha = (int) (origAlpha * mOpacity + 0.5f); + final int alpha = Math.min((int) (origAlpha * mOpacity + 0.5f), 255); if (alpha > 0) { p.setAlpha(alpha); c.drawCircle(0, 0, mTargetRadius, p); p.setAlpha(origAlpha); - hasContent = true; } - - return hasContent; - } - - @Override - protected boolean drawHardware(DisplayListCanvas c) { - c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); - return true; - } - - @Override - protected Animator createSoftwareEnter(boolean fast) { - // Linear enter based on current opacity. - final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION; - final int duration = (int) ((1 - mOpacity) * maxDuration); - - final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); - opacity.setAutoCancel(true); - opacity.setDuration(duration); - opacity.setInterpolator(LINEAR_INTERPOLATOR); - - return opacity; } - @Override - protected Animator createSoftwareExit() { - final AnimatorSet set = new AnimatorSet(); - - // Linear exit after enter is completed. - final ObjectAnimator exit = ObjectAnimator.ofFloat(this, OPACITY, 0); - exit.setInterpolator(LINEAR_INTERPOLATOR); - exit.setDuration(OPACITY_EXIT_DURATION); - exit.setAutoCancel(true); - - final AnimatorSet.Builder builder = set.play(exit); - - // Linear "fast" enter based on current opacity. - final int fastEnterDuration = mIsBounded ? - (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0; - if (fastEnterDuration > 0) { - final ObjectAnimator enter = ObjectAnimator.ofFloat(this, OPACITY, 1); - enter.setInterpolator(LINEAR_INTERPOLATOR); - enter.setDuration(fastEnterDuration); - enter.setAutoCancel(true); - - builder.after(enter); + public void setState(boolean focused, boolean hovered, boolean animateChanged) { + if (mHovered != hovered || mFocused != focused) { + mHovered = hovered; + mFocused = focused; + onStateChanged(animateChanged); } - - return set; } - @Override - protected RenderNodeAnimatorSet createHardwareExit(Paint p) { - final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); - - final int targetAlpha = p.getAlpha(); - final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f); - p.setAlpha(currentAlpha); - - mPropPaint = CanvasProperty.createPaint(p); - mPropRadius = CanvasProperty.createFloat(mTargetRadius); - mPropX = CanvasProperty.createFloat(0); - mPropY = CanvasProperty.createFloat(0); - - final int fastEnterDuration = mIsBounded ? - (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0; - - // Linear exit after enter is completed. - final RenderNodeAnimator exit = new RenderNodeAnimator( - mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0); - exit.setInterpolator(LINEAR_INTERPOLATOR); - exit.setDuration(OPACITY_EXIT_DURATION); - if (fastEnterDuration > 0) { - exit.setStartDelay(fastEnterDuration); - exit.setStartValue(targetAlpha); + private void onStateChanged(boolean animateChanged) { + float newOpacity = 0.0f; + if (mHovered) newOpacity += 1.0f; + if (mFocused) newOpacity += 1.0f; + if (mAnimator != null) { + mAnimator.cancel(); + mAnimator = null; } - set.add(exit); - - // Linear "fast" enter based on current opacity. - if (fastEnterDuration > 0) { - final RenderNodeAnimator enter = new RenderNodeAnimator( - mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha); - enter.setInterpolator(LINEAR_INTERPOLATOR); - enter.setDuration(fastEnterDuration); - set.add(enter); + if (animateChanged) { + mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity); + mAnimator.setDuration(OPACITY_DURATION); + mAnimator.setInterpolator(LINEAR_INTERPOLATOR); + mAnimator.start(); + } else { + mOpacity = newOpacity; } - - return set; } - @Override - protected void jumpValuesToExit() { - mOpacity = 0; + public void jumpToFinal() { + if (mAnimator != null) { + mAnimator.end(); + mAnimator = null; + } } private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> { diff --git a/android/graphics/drawable/RippleComponent.java b/android/graphics/drawable/RippleComponent.java index e83513c6..0e38826e 100644 --- a/android/graphics/drawable/RippleComponent.java +++ b/android/graphics/drawable/RippleComponent.java @@ -27,23 +27,14 @@ import android.view.RenderNodeAnimator; import java.util.ArrayList; /** - * Abstract class that handles hardware/software hand-off and lifecycle for - * animated ripple foreground and background components. + * Abstract class that handles size & positioning common to the ripple & focus states. */ abstract class RippleComponent { - private final RippleDrawable mOwner; + protected final RippleDrawable mOwner; /** Bounds used for computing max radius. May be modified by the owner. */ protected final Rect mBounds; - /** Whether we can use hardware acceleration for the exit animation. */ - private boolean mHasDisplayListCanvas; - - private boolean mHasPendingHardwareAnimator; - private RenderNodeAnimatorSet mHardwareAnimator; - - private Animator mSoftwareAnimator; - /** Whether we have an explicit maximum radius. */ private boolean mHasMaxRadius; @@ -53,16 +44,9 @@ abstract class RippleComponent { /** Screen density used to adjust pixel-based constants. */ protected float mDensityScale; - /** - * If set, force all ripple animations to not run on RenderThread, even if it would be - * available. - */ - private final boolean mForceSoftware; - - public RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware) { + public RippleComponent(RippleDrawable owner, Rect bounds) { mOwner = owner; mBounds = bounds; - mForceSoftware = forceSoftware; } public void onBoundsChange() { @@ -92,89 +76,6 @@ abstract class RippleComponent { } /** - * Starts a ripple enter animation. - * - * @param fast whether the ripple should enter quickly - */ - public final void enter(boolean fast) { - cancel(); - - mSoftwareAnimator = createSoftwareEnter(fast); - - if (mSoftwareAnimator != null) { - mSoftwareAnimator.start(); - } - } - - /** - * Starts a ripple exit animation. - */ - public final void exit() { - cancel(); - - if (mHasDisplayListCanvas) { - // We don't have access to a canvas here, but we expect one on the - // next frame. We'll start the render thread animation then. - mHasPendingHardwareAnimator = true; - - // Request another frame. - invalidateSelf(); - } else { - mSoftwareAnimator = createSoftwareExit(); - mSoftwareAnimator.start(); - } - } - - /** - * Cancels all animations. Software animation values are left in the - * current state, while hardware animation values jump to the end state. - */ - public void cancel() { - cancelSoftwareAnimations(); - endHardwareAnimations(); - } - - /** - * Ends all animations, jumping values to the end state. - */ - public void end() { - endSoftwareAnimations(); - endHardwareAnimations(); - } - - /** - * Draws the ripple to the canvas, inheriting the paint's color and alpha - * properties. - * - * @param c the canvas to which the ripple should be drawn - * @param p the paint used to draw the ripple - * @return {@code true} if something was drawn, {@code false} otherwise - */ - public boolean draw(Canvas c, Paint p) { - final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated() - && c instanceof DisplayListCanvas; - if (mHasDisplayListCanvas != hasDisplayListCanvas) { - mHasDisplayListCanvas = hasDisplayListCanvas; - - if (!hasDisplayListCanvas) { - // We've switched from hardware to non-hardware mode. Panic. - endHardwareAnimations(); - } - } - - if (hasDisplayListCanvas) { - final DisplayListCanvas hw = (DisplayListCanvas) c; - startPendingAnimation(hw, p); - - if (mHardwareAnimator != null) { - return drawHardware(hw); - } - } - - return drawSoftware(c, p); - } - - /** * Populates {@code bounds} with the maximum drawing bounds of the ripple * relative to its center. The resulting bounds should be translated into * parent drawable coordinates before use. @@ -186,77 +87,10 @@ abstract class RippleComponent { bounds.set(-r, -r, r, r); } - /** - * Starts the pending hardware animation, if available. - * - * @param hw hardware canvas on which the animation should draw - * @param p paint whose properties the hardware canvas should use - */ - private void startPendingAnimation(DisplayListCanvas hw, Paint p) { - if (mHasPendingHardwareAnimator) { - mHasPendingHardwareAnimator = false; - - mHardwareAnimator = createHardwareExit(new Paint(p)); - mHardwareAnimator.start(hw); - - // Preemptively jump the software values to the end state now that - // the hardware exit has read whatever values it needs. - jumpValuesToExit(); - } - } - - /** - * Cancels any current software animations, leaving the values in their - * current state. - */ - private void cancelSoftwareAnimations() { - if (mSoftwareAnimator != null) { - mSoftwareAnimator.cancel(); - mSoftwareAnimator = null; - } - } - - /** - * Ends any current software animations, jumping the values to their end - * state. - */ - private void endSoftwareAnimations() { - if (mSoftwareAnimator != null) { - mSoftwareAnimator.end(); - mSoftwareAnimator = null; - } - } - - /** - * Ends any pending or current hardware animations. - * <p> - * Hardware animations can't synchronize values back to the software - * thread, so there is no "cancel" equivalent. - */ - private void endHardwareAnimations() { - if (mHardwareAnimator != null) { - mHardwareAnimator.end(); - mHardwareAnimator = null; - } - - if (mHasPendingHardwareAnimator) { - mHasPendingHardwareAnimator = false; - - // Manually jump values to their exited state. Normally we'd do that - // later when starting the hardware exit, but we're aborting early. - jumpValuesToExit(); - } - } - protected final void invalidateSelf() { mOwner.invalidateSelf(false); } - protected final boolean isHardwareAnimating() { - return mHardwareAnimator != null && mHardwareAnimator.isRunning() - || mHasPendingHardwareAnimator; - } - protected final void onHotspotBoundsChanged() { if (!mHasMaxRadius) { final float halfWidth = mBounds.width() / 2.0f; @@ -276,76 +110,4 @@ abstract class RippleComponent { protected void onTargetRadiusChanged(float targetRadius) { // Stub. } - - protected abstract Animator createSoftwareEnter(boolean fast); - - protected abstract Animator createSoftwareExit(); - - protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p); - - protected abstract boolean drawHardware(DisplayListCanvas c); - - protected abstract boolean drawSoftware(Canvas c, Paint p); - - /** - * Called when the hardware exit is cancelled. Jumps software values to end - * state to ensure that software and hardware values are synchronized. - */ - protected abstract void jumpValuesToExit(); - - public static class RenderNodeAnimatorSet { - private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>(); - - public void add(RenderNodeAnimator anim) { - mAnimators.add(anim); - } - - public void clear() { - mAnimators.clear(); - } - - public void start(DisplayListCanvas target) { - if (target == null) { - throw new IllegalArgumentException("Hardware canvas must be non-null"); - } - - final ArrayList<RenderNodeAnimator> animators = mAnimators; - final int N = animators.size(); - for (int i = 0; i < N; i++) { - final RenderNodeAnimator anim = animators.get(i); - anim.setTarget(target); - anim.start(); - } - } - - public void cancel() { - final ArrayList<RenderNodeAnimator> animators = mAnimators; - final int N = animators.size(); - for (int i = 0; i < N; i++) { - final RenderNodeAnimator anim = animators.get(i); - anim.cancel(); - } - } - - public void end() { - final ArrayList<RenderNodeAnimator> animators = mAnimators; - final int N = animators.size(); - for (int i = 0; i < N; i++) { - final RenderNodeAnimator anim = animators.get(i); - anim.end(); - } - } - - public boolean isRunning() { - final ArrayList<RenderNodeAnimator> animators = mAnimators; - final int N = animators.size(); - for (int i = 0; i < N; i++) { - final RenderNodeAnimator anim = animators.get(i); - if (anim.isRunning()) { - return true; - } - } - return false; - } - } } diff --git a/android/graphics/drawable/RippleDrawable.java b/android/graphics/drawable/RippleDrawable.java index 1727eca5..8b185f2b 100644 --- a/android/graphics/drawable/RippleDrawable.java +++ b/android/graphics/drawable/RippleDrawable.java @@ -16,11 +16,6 @@ package android.graphics.drawable; -import com.android.internal.R; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import android.annotation.NonNull; import android.annotation.Nullable; import android.content.pm.ActivityInfo.Config; @@ -42,6 +37,11 @@ import android.graphics.Rect; import android.graphics.Shader; import android.util.AttributeSet; +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; import java.util.Arrays; @@ -135,9 +135,6 @@ public class RippleDrawable extends LayerDrawable { private PorterDuffColorFilter mMaskColorFilter; private boolean mHasValidMask; - /** Whether we expect to draw a background when visible. */ - private boolean mBackgroundActive; - /** The current ripple. May be actively animating or pending entry. */ private RippleForeground mRipple; @@ -217,7 +214,7 @@ public class RippleDrawable extends LayerDrawable { } if (mBackground != null) { - mBackground.end(); + mBackground.jumpToFinal(); } cancelExitingRipples(); @@ -266,9 +263,9 @@ public class RippleDrawable extends LayerDrawable { } } - setRippleActive(focused || (enabled && pressed)); + setRippleActive(enabled && pressed); - setBackgroundActive(hovered, hovered); + setBackgroundActive(hovered, focused); return changed; } @@ -283,14 +280,13 @@ public class RippleDrawable extends LayerDrawable { } } - private void setBackgroundActive(boolean active, boolean focused) { - if (mBackgroundActive != active) { - mBackgroundActive = active; - if (active) { - tryBackgroundEnter(focused); - } else { - tryBackgroundExit(); - } + private void setBackgroundActive(boolean hovered, boolean focused) { + if (mBackground == null && (hovered || focused)) { + mBackground = new RippleBackground(this, mHotspotBounds, isBounded()); + mBackground.setup(mState.mMaxRadius, mDensity); + } + if (mBackground != null) { + mBackground.setState(focused, hovered, true); } } @@ -327,10 +323,6 @@ public class RippleDrawable extends LayerDrawable { tryRippleEnter(); } - if (mBackgroundActive) { - tryBackgroundEnter(false); - } - // Skip animations, just show the correct final states. jumpToCurrentState(); } @@ -546,26 +538,6 @@ public class RippleDrawable extends LayerDrawable { } /** - * Creates an active hotspot at the specified location. - */ - private void tryBackgroundEnter(boolean focused) { - if (mBackground == null) { - final boolean isBounded = isBounded(); - mBackground = new RippleBackground(this, mHotspotBounds, isBounded, mForceSoftware); - } - - mBackground.setup(mState.mMaxRadius, mDensity); - mBackground.enter(focused); - } - - private void tryBackgroundExit() { - if (mBackground != null) { - // Don't null out the background, we need it to draw! - mBackground.exit(); - } - } - - /** * Attempts to start an enter animation for the active hotspot. Fails if * there are too many animating ripples. */ @@ -593,7 +565,7 @@ public class RippleDrawable extends LayerDrawable { } mRipple.setup(mState.mMaxRadius, mDensity); - mRipple.enter(false); + mRipple.enter(); } /** @@ -623,9 +595,7 @@ public class RippleDrawable extends LayerDrawable { } if (mBackground != null) { - mBackground.end(); - mBackground = null; - mBackgroundActive = false; + mBackground.setState(false, false, false); } cancelExitingRipples(); @@ -858,6 +828,40 @@ public class RippleDrawable extends LayerDrawable { final float y = mHotspotBounds.exactCenterY(); canvas.translate(x, y); + final Paint p = getRipplePaint(); + + if (background != null && background.isVisible()) { + background.draw(canvas, p); + } + + if (count > 0) { + final RippleForeground[] ripples = mExitingRipples; + for (int i = 0; i < count; i++) { + ripples[i].draw(canvas, p); + } + } + + if (active != null) { + active.draw(canvas, p); + } + + canvas.translate(-x, -y); + } + + private void drawMask(Canvas canvas) { + mMask.draw(canvas); + } + + Paint getRipplePaint() { + if (mRipplePaint == null) { + mRipplePaint = new Paint(); + mRipplePaint.setAntiAlias(true); + mRipplePaint.setStyle(Paint.Style.FILL); + } + + final float x = mHotspotBounds.exactCenterX(); + final float y = mHotspotBounds.exactCenterY(); + updateMaskShaderIfNeeded(); // Position the shader to account for canvas translation. @@ -871,7 +875,7 @@ public class RippleDrawable extends LayerDrawable { // half so that the ripple and background together yield full alpha. final int color = mState.mColor.getColorForState(getState(), Color.BLACK); final int halfAlpha = (Color.alpha(color) / 2) << 24; - final Paint p = getRipplePaint(); + final Paint p = mRipplePaint; if (mMaskColorFilter != null) { // The ripple timing depends on the paint's alpha value, so we need @@ -890,35 +894,7 @@ public class RippleDrawable extends LayerDrawable { p.setShader(null); } - if (background != null && background.isVisible()) { - background.draw(canvas, p); - } - - if (count > 0) { - final RippleForeground[] ripples = mExitingRipples; - for (int i = 0; i < count; i++) { - ripples[i].draw(canvas, p); - } - } - - if (active != null) { - active.draw(canvas, p); - } - - canvas.translate(-x, -y); - } - - private void drawMask(Canvas canvas) { - mMask.draw(canvas); - } - - private Paint getRipplePaint() { - if (mRipplePaint == null) { - mRipplePaint = new Paint(); - mRipplePaint.setAntiAlias(true); - mRipplePaint.setStyle(Paint.Style.FILL); - } - return mRipplePaint; + return p; } @Override diff --git a/android/graphics/drawable/RippleForeground.java b/android/graphics/drawable/RippleForeground.java index a675eaf8..0b5020cb 100644 --- a/android/graphics/drawable/RippleForeground.java +++ b/android/graphics/drawable/RippleForeground.java @@ -18,7 +18,6 @@ package android.graphics.drawable; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Canvas; @@ -29,8 +28,11 @@ import android.util.FloatProperty; import android.util.MathUtils; import android.view.DisplayListCanvas; import android.view.RenderNodeAnimator; +import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; +import java.util.ArrayList; + /** * Draws a ripple foreground. */ @@ -40,7 +42,7 @@ class RippleForeground extends RippleComponent { 400f, 1.4f, 0); // Pixel-based accelerations and velocities. - private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024; + private static final float WAVE_TOUCH_DOWN_ACCELERATION = 2048; private static final float WAVE_OPACITY_DECAY_VELOCITY = 3; // Bounded ripple animation properties. @@ -49,8 +51,9 @@ class RippleForeground extends RippleComponent { private static final int BOUNDED_OPACITY_EXIT_DURATION = 400; private static final float MAX_BOUNDED_RADIUS = 350; - private static final int RIPPLE_ENTER_DELAY = 80; - private static final int OPACITY_ENTER_DURATION_FAST = 120; + private static final int OPACITY_ENTER_DURATION = 75; + private static final int OPACITY_EXIT_DURATION = 150; + private static final int OPACITY_HOLD_DURATION = OPACITY_ENTER_DURATION + 150; // Parent-relative values for starting position. private float mStartingX; @@ -72,7 +75,7 @@ class RippleForeground extends RippleComponent { private float mBoundedRadius = 0; // Software rendering properties. - private float mOpacity = 1; + private float mOpacity = 0; // Values used to tween between the start and end positions. private float mTweenRadius = 0; @@ -82,6 +85,22 @@ class RippleForeground extends RippleComponent { /** Whether this ripple has finished its exit animation. */ private boolean mHasFinishedExit; + /** Whether we can use hardware acceleration for the exit animation. */ + private boolean mUsingProperties; + + private long mEnterStartedAtMillis; + + private ArrayList<RenderNodeAnimator> mPendingHwAnimators = new ArrayList<>(); + private ArrayList<RenderNodeAnimator> mRunningHwAnimators = new ArrayList<>(); + + private ArrayList<Animator> mRunningSwAnimators = new ArrayList<>(); + + /** + * If set, force all ripple animations to not run on RenderThread, even if it would be + * available. + */ + private final boolean mForceSoftware; + /** * If we have a bound, don't start from 0. Start from 60% of the max out of width and height. */ @@ -89,8 +108,9 @@ class RippleForeground extends RippleComponent { public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, boolean isBounded, boolean forceSoftware) { - super(owner, bounds, forceSoftware); + super(owner, bounds); + mForceSoftware = forceSoftware; mStartingX = startingX; mStartingY = startingY; @@ -109,10 +129,7 @@ class RippleForeground extends RippleComponent { clampStartingPosition(); } - @Override - protected boolean drawSoftware(Canvas c, Paint p) { - boolean hasContent = false; - + private void drawSoftware(Canvas c, Paint p) { final int origAlpha = p.getAlpha(); final int alpha = (int) (origAlpha * mOpacity + 0.5f); final float radius = getCurrentRadius(); @@ -122,16 +139,51 @@ class RippleForeground extends RippleComponent { p.setAlpha(alpha); c.drawCircle(x, y, radius, p); p.setAlpha(origAlpha); - hasContent = true; } + } - return hasContent; + private void startPending(DisplayListCanvas c) { + if (!mPendingHwAnimators.isEmpty()) { + for (int i = 0; i < mPendingHwAnimators.size(); i++) { + RenderNodeAnimator animator = mPendingHwAnimators.get(i); + animator.setTarget(c); + animator.start(); + mRunningHwAnimators.add(animator); + } + mPendingHwAnimators.clear(); + } } - @Override - protected boolean drawHardware(DisplayListCanvas c) { - c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); - return true; + private void pruneHwFinished() { + if (!mRunningHwAnimators.isEmpty()) { + for (int i = mRunningHwAnimators.size() - 1; i >= 0; i--) { + if (!mRunningHwAnimators.get(i).isRunning()) { + mRunningHwAnimators.remove(i); + } + } + } + } + + private void pruneSwFinished() { + if (!mRunningSwAnimators.isEmpty()) { + for (int i = mRunningSwAnimators.size() - 1; i >= 0; i--) { + if (!mRunningSwAnimators.get(i).isRunning()) { + mRunningSwAnimators.remove(i); + } + } + } + } + + private void drawHardware(DisplayListCanvas c, Paint p) { + startPending(c); + pruneHwFinished(); + if (mPropPaint != null) { + mUsingProperties = true; + c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); + } else { + mUsingProperties = false; + drawSoftware(c, p); + } } /** @@ -162,141 +214,152 @@ class RippleForeground extends RippleComponent { return mHasFinishedExit; } - @Override - protected Animator createSoftwareEnter(boolean fast) { + private long computeFadeOutDelay() { + long timeSinceEnter = AnimationUtils.currentAnimationTimeMillis() - mEnterStartedAtMillis; + if (timeSinceEnter > 0 && timeSinceEnter < OPACITY_HOLD_DURATION) { + return OPACITY_HOLD_DURATION - timeSinceEnter; + } + return 0; + } + + private void startSoftwareEnter() { + for (int i = 0; i < mRunningSwAnimators.size(); i++) { + mRunningSwAnimators.get(i).cancel(); + } + mRunningSwAnimators.clear(); + final int duration = getRadiusDuration(); final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); - tweenRadius.setAutoCancel(true); tweenRadius.setDuration(duration); tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); - tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY); + tweenRadius.start(); + mRunningSwAnimators.add(tweenRadius); final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); - tweenOrigin.setAutoCancel(true); tweenOrigin.setDuration(duration); tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); - tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY); + tweenOrigin.start(); + mRunningSwAnimators.add(tweenOrigin); final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); - opacity.setAutoCancel(true); - opacity.setDuration(OPACITY_ENTER_DURATION_FAST); + opacity.setDuration(OPACITY_ENTER_DURATION); opacity.setInterpolator(LINEAR_INTERPOLATOR); - - final AnimatorSet set = new AnimatorSet(); - set.play(tweenOrigin).with(tweenRadius).with(opacity); - - return set; + opacity.start(); + mRunningSwAnimators.add(opacity); } - private float getCurrentX() { - return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX); - } - - private float getCurrentY() { - return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); - } - - private int getRadiusDuration() { - final float remainingRadius = mTargetRadius - getCurrentRadius(); - return (int) (1000 * Math.sqrt(remainingRadius / WAVE_TOUCH_DOWN_ACCELERATION * - mDensityScale) + 0.5); - } - - private float getCurrentRadius() { - return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius); - } - - private int getOpacityExitDuration() { - return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); - } - - @Override - protected Animator createSoftwareExit() { - final int radiusDuration; - final int originDuration; - final int opacityDuration; - - radiusDuration = getRadiusDuration(); - originDuration = radiusDuration; - opacityDuration = getOpacityExitDuration(); - - final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); - tweenRadius.setAutoCancel(true); - tweenRadius.setDuration(radiusDuration); - tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); - - final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); - tweenOrigin.setAutoCancel(true); - tweenOrigin.setDuration(originDuration); - tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); - + private void startSoftwareExit() { final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); - opacity.setAutoCancel(true); - opacity.setDuration(opacityDuration); + opacity.setDuration(OPACITY_EXIT_DURATION); opacity.setInterpolator(LINEAR_INTERPOLATOR); - - final AnimatorSet set = new AnimatorSet(); - set.play(tweenOrigin).with(tweenRadius).with(opacity); - set.addListener(mAnimationListener); - - return set; + opacity.addListener(mAnimationListener); + opacity.setStartDelay(computeFadeOutDelay()); + opacity.start(); + mRunningSwAnimators.add(opacity); } - @Override - protected RenderNodeAnimatorSet createHardwareExit(Paint p) { - final int radiusDuration; - final int originDuration; - final int opacityDuration; - - radiusDuration = getRadiusDuration(); - originDuration = radiusDuration; - opacityDuration = getOpacityExitDuration(); + private void startHardwareEnter() { + if (mForceSoftware) { return; } + mPropX = CanvasProperty.createFloat(getCurrentX()); + mPropY = CanvasProperty.createFloat(getCurrentY()); + mPropRadius = CanvasProperty.createFloat(getCurrentRadius()); + final Paint paint = mOwner.getRipplePaint(); + mPropPaint = CanvasProperty.createPaint(paint); - final float startX = getCurrentX(); - final float startY = getCurrentY(); - final float startRadius = getCurrentRadius(); - - p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f)); - - mPropPaint = CanvasProperty.createPaint(p); - mPropRadius = CanvasProperty.createFloat(startRadius); - mPropX = CanvasProperty.createFloat(startX); - mPropY = CanvasProperty.createFloat(startY); + final int radiusDuration = getRadiusDuration(); final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); radius.setDuration(radiusDuration); radius.setInterpolator(DECELERATE_INTERPOLATOR); + mPendingHwAnimators.add(radius); final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX); - x.setDuration(originDuration); + x.setDuration(radiusDuration); x.setInterpolator(DECELERATE_INTERPOLATOR); + mPendingHwAnimators.add(x); final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY); - y.setDuration(originDuration); + y.setDuration(radiusDuration); y.setInterpolator(DECELERATE_INTERPOLATOR); + mPendingHwAnimators.add(y); + + final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, + RenderNodeAnimator.PAINT_ALPHA, paint.getAlpha()); + opacity.setDuration(OPACITY_ENTER_DURATION); + opacity.setInterpolator(LINEAR_INTERPOLATOR); + opacity.setStartValue(0); + mPendingHwAnimators.add(opacity); + + invalidateSelf(); + } + + private void startHardwareExit() { + // Only run a hardware exit if we had a hardware enter to continue from + if (mForceSoftware || mPropPaint == null) return; final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0); - opacity.setDuration(opacityDuration); + opacity.setDuration(OPACITY_EXIT_DURATION); opacity.setInterpolator(LINEAR_INTERPOLATOR); opacity.addListener(mAnimationListener); + opacity.setStartDelay(computeFadeOutDelay()); + mPendingHwAnimators.add(opacity); + invalidateSelf(); + } + + /** + * Starts a ripple enter animation. + */ + public final void enter() { + mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis(); + startSoftwareEnter(); + startHardwareEnter(); + } - final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); - set.add(radius); - set.add(opacity); - set.add(x); - set.add(y); + /** + * Starts a ripple exit animation. + */ + public final void exit() { + startSoftwareExit(); + startHardwareExit(); + } - return set; + private float getCurrentX() { + return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX); } - @Override - protected void jumpValuesToExit() { - mOpacity = 0; - mTweenX = 1; - mTweenY = 1; - mTweenRadius = 1; + private float getCurrentY() { + return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); + } + + private int getRadiusDuration() { + final float remainingRadius = mTargetRadius - getCurrentRadius(); + return (int) (1000 * Math.sqrt(remainingRadius / WAVE_TOUCH_DOWN_ACCELERATION * + mDensityScale) + 0.5); + } + + private float getCurrentRadius() { + return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius); + } + + /** + * Draws the ripple to the canvas, inheriting the paint's color and alpha + * properties. + * + * @param c the canvas to which the ripple should be drawn + * @param p the paint used to draw the ripple + */ + public void draw(Canvas c, Paint p) { + final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof DisplayListCanvas; + + pruneSwFinished(); + if (hasDisplayListCanvas) { + final DisplayListCanvas hw = (DisplayListCanvas) c; + drawHardware(hw, p); + } else { + drawSoftware(c, p); + } } /** @@ -319,10 +382,39 @@ class RippleForeground extends RippleComponent { } } + /** + * Ends all animations, jumping values to the end state. + */ + public void end() { + for (int i = 0; i < mRunningSwAnimators.size(); i++) { + mRunningSwAnimators.get(i).end(); + } + mRunningSwAnimators.clear(); + for (int i = 0; i < mRunningHwAnimators.size(); i++) { + mRunningHwAnimators.get(i).end(); + } + mRunningHwAnimators.clear(); + } + + private void onAnimationPropertyChanged() { + if (!mUsingProperties) { + invalidateSelf(); + } + } + private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { mHasFinishedExit = true; + pruneHwFinished(); + pruneSwFinished(); + + if (mRunningHwAnimators.isEmpty()) { + mPropPaint = null; + mPropRadius = null; + mPropX = null; + mPropY = null; + } } }; @@ -361,7 +453,7 @@ class RippleForeground extends RippleComponent { @Override public void setValue(RippleForeground object, float value) { object.mTweenRadius = value; - object.invalidateSelf(); + object.onAnimationPropertyChanged(); } @Override @@ -375,18 +467,18 @@ class RippleForeground extends RippleComponent { */ private static final FloatProperty<RippleForeground> TWEEN_ORIGIN = new FloatProperty<RippleForeground>("tweenOrigin") { - @Override - public void setValue(RippleForeground object, float value) { - object.mTweenX = value; - object.mTweenY = value; - object.invalidateSelf(); - } + @Override + public void setValue(RippleForeground object, float value) { + object.mTweenX = value; + object.mTweenY = value; + object.onAnimationPropertyChanged(); + } - @Override - public Float get(RippleForeground object) { - return object.mTweenX; - } - }; + @Override + public Float get(RippleForeground object) { + return object.mTweenX; + } + }; /** * Property for animating opacity between 0 and its target value. @@ -396,7 +488,7 @@ class RippleForeground extends RippleComponent { @Override public void setValue(RippleForeground object, float value) { object.mOpacity = value; - object.invalidateSelf(); + object.onAnimationPropertyChanged(); } @Override diff --git a/android/graphics/drawable/VectorDrawable.java b/android/graphics/drawable/VectorDrawable.java index ceac3253..7b2e21a4 100644 --- a/android/graphics/drawable/VectorDrawable.java +++ b/android/graphics/drawable/VectorDrawable.java @@ -213,12 +213,79 @@ import dalvik.system.VMRuntime; * </vector> * </pre> * </li> - * <li>And here is an example of linear gradient color, which is supported in SDK 24+. + * <h4>Gradient support</h4> + * We support 3 types of gradients: {@link android.graphics.LinearGradient}, + * {@link android.graphics.RadialGradient}, or {@link android.graphics.SweepGradient}. + * <p/> + * And we support all of 3 types of tile modes {@link android.graphics.Shader.TileMode}: + * CLAMP, REPEAT, MIRROR. + * <p/> + * All of the attributes are listed in {@link android.R.styleable#GradientColor}. + * Note that different attributes are relevant for different types of gradient. + * <table border="2" align="center" cellpadding="5"> + * <thead> + * <tr> + * <th>LinearGradient</th> + * <th>RadialGradient</th> + * <th>SweepGradient</th> + * </tr> + * </thead> + * <tr> + * <td>startColor </td> + * <td>startColor</td> + * <td>startColor</td> + * </tr> + * <tr> + * <td>centerColor</td> + * <td>centerColor</td> + * <td>centerColor</td> + * </tr> + * <tr> + * <td>endColor</td> + * <td>endColor</td> + * <td>endColor</td> + * </tr> + * <tr> + * <td>type</td> + * <td>type</td> + * <td>type</td> + * </tr> + * <tr> + * <td>tileMode</td> + * <td>tileMode</td> + * <td>tileMode</td> + * </tr> + * <tr> + * <td>startX</td> + * <td>centerX</td> + * <td>centerX</td> + * </tr> + * <tr> + * <td>startY</td> + * <td>centerY</td> + * <td>centerY</td> + * </tr> + * <tr> + * <td>endX</td> + * <td>gradientRadius</td> + * <td></td> + * </tr> + * <tr> + * <td>endY</td> + * <td></td> + * <td></td> + * </tr> + * </table> + * <p/> + * Also note that if any color item {@link android.R.styleable#GradientColorItem} is defined, then + * startColor, centerColor and endColor will be ignored. + * <p/> * See more details in {@link android.R.styleable#GradientColor} and * {@link android.R.styleable#GradientColorItem}. + * <p/> + * Here is a simple example that defines a linear gradient. * <pre> * <gradient xmlns:android="http://schemas.android.com/apk/res/android" - * android:angle="90" * android:startColor="?android:attr/colorPrimary" * android:endColor="?android:attr/colorControlActivated" * android:centerColor="#f00" @@ -229,7 +296,18 @@ import dalvik.system.VMRuntime; * android:type="linear"> * </gradient> * </pre> - * </li> + * And here is a simple example that defines a radial gradient using color items. + * <pre> + * <gradient xmlns:android="http://schemas.android.com/apk/res/android" + * android:centerX="300" + * android:centerY="300" + * android:gradientRadius="100" + * android:type="radial"> + * <item android:offset="0.1" android:color="#0ff"/> + * <item android:offset="0.4" android:color="#fff"/> + * <item android:offset="0.9" android:color="#ff0"/> + * </gradient> + * </pre> * */ diff --git a/android/hardware/camera2/CameraCaptureSession.java b/android/hardware/camera2/CameraCaptureSession.java index da771e48..ff69bd89 100644 --- a/android/hardware/camera2/CameraCaptureSession.java +++ b/android/hardware/camera2/CameraCaptureSession.java @@ -249,7 +249,7 @@ public abstract class CameraCaptureSession implements AutoCloseable { * <p>This function can also be called in case where multiple surfaces share the same * OutputConfiguration, and one of the surfaces becomes available after the {@link * CameraCaptureSession} is created. In that case, the application must first create the - * OutputConfiguration with the available Surface, then enable furture surface sharing via + * OutputConfiguration with the available Surface, then enable further surface sharing via * {@link OutputConfiguration#enableSurfaceSharing}, before creating the CameraCaptureSession. * After the CameraCaptureSession is created, and once the extra Surface becomes available, the * application must then call {@link OutputConfiguration#addSurface} before finalizing the @@ -645,6 +645,44 @@ public abstract class CameraCaptureSession implements AutoCloseable { public abstract Surface getInputSurface(); /** + * Update {@link OutputConfiguration} after configuration finalization see + * {@link #finalizeOutputConfigurations}. + * + * <p>Any {@link OutputConfiguration} that has been modified via calls to + * {@link OutputConfiguration#addSurface} or {@link OutputConfiguration#removeSurface} must be + * updated. After the update call returns without throwing exceptions any newly added surfaces + * can be referenced in subsequent capture requests.</p> + * + * <p>Surfaces that get removed must not be part of any active repeating or single/burst + * request or have any pending results. Consider updating any repeating requests first via + * {@link #setRepeatingRequest} or {@link #setRepeatingBurst} and then wait for the last frame + * number when the sequence completes {@link CaptureCallback#onCaptureSequenceCompleted} + * before calling updateOutputConfiguration to remove a previously active Surface.</p> + * + * <p>Surfaces that get added must not be part of any other registered + * {@link OutputConfiguration}.</p> + * + * @param config Modified output configuration. + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error. + * @throws IllegalArgumentException if an attempt was made to add a {@link Surface} already + * in use by another buffer-producing API, such as MediaCodec or + * a different camera device or {@link OutputConfiguration}; or + * new surfaces are not compatible (see + * {@link OutputConfiguration#enableSurfaceSharing}); or a + * {@link Surface} that was removed from the modified + * {@link OutputConfiguration} still has pending requests. + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. + */ + public void updateOutputConfiguration(OutputConfiguration config) + throws CameraAccessException { + throw new UnsupportedOperationException("Subclasses must override this method"); + } + + /** * Close this capture session asynchronously. * * <p>Closing a session frees up the target output Surfaces of the session for reuse with either diff --git a/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/android/hardware/camera2/impl/CameraCaptureSessionImpl.java index c7654c9e..374789c6 100644 --- a/android/hardware/camera2/impl/CameraCaptureSessionImpl.java +++ b/android/hardware/camera2/impl/CameraCaptureSessionImpl.java @@ -314,6 +314,20 @@ public class CameraCaptureSessionImpl extends CameraCaptureSession } @Override + public void updateOutputConfiguration(OutputConfiguration config) + throws CameraAccessException { + synchronized (mDeviceImpl.mInterfaceLock) { + checkNotClosed(); + + if (DEBUG) { + Log.v(TAG, mIdString + "updateOutputConfiguration"); + } + + mDeviceImpl.updateOutputConfiguration(config); + } + } + + @Override public boolean isReprocessable() { return mInput != null; } diff --git a/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java b/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java index fec7fd97..8c4dbfa5 100644 --- a/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java +++ b/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java @@ -235,6 +235,13 @@ public class CameraConstrainedHighSpeedCaptureSessionImpl } @Override + public void updateOutputConfiguration(OutputConfiguration config) + throws CameraAccessException { + throw new UnsupportedOperationException("Constrained high speed session doesn't support" + + " this method"); + } + + @Override public void close() { mSessionImpl.close(); } diff --git a/android/hardware/camera2/impl/CameraDeviceImpl.java b/android/hardware/camera2/impl/CameraDeviceImpl.java index bfeb14de..6787d84b 100644 --- a/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -764,6 +764,24 @@ public class CameraDeviceImpl extends CameraDevice } } + public void updateOutputConfiguration(OutputConfiguration config) + throws CameraAccessException { + synchronized(mInterfaceLock) { + int streamId = -1; + for (int i = 0; i < mConfiguredOutputs.size(); i++) { + if (config.getSurface() == mConfiguredOutputs.valueAt(i).getSurface()) { + streamId = mConfiguredOutputs.keyAt(i); + break; + } + } + if (streamId == -1) { + throw new IllegalArgumentException("Invalid output configuration"); + } + + mRemoteDevice.updateOutputConfiguration(streamId, config); + } + } + public void tearDown(Surface surface) throws CameraAccessException { if (surface == null) throw new IllegalArgumentException("Surface is null"); diff --git a/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java index 27087a2e..0978ff87 100644 --- a/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java +++ b/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java @@ -215,6 +215,16 @@ public class ICameraDeviceUserWrapper { } } + public void updateOutputConfiguration(int streamId, OutputConfiguration config) + throws CameraAccessException { + try { + mRemoteDevice.updateOutputConfiguration(streamId, config); + } catch (Throwable t) { + CameraManager.throwAsPublicException(t); + throw new UnsupportedOperationException("Unexpected exception", t); + } + } + public void finalizeOutputConfigurations(int streamId, OutputConfiguration deferredConfig) throws CameraAccessException { try { diff --git a/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/android/hardware/camera2/legacy/CameraDeviceUserShim.java index 49d4096e..119cca8d 100644 --- a/android/hardware/camera2/legacy/CameraDeviceUserShim.java +++ b/android/hardware/camera2/legacy/CameraDeviceUserShim.java @@ -646,6 +646,11 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { } @Override + public void updateOutputConfiguration(int streamId, OutputConfiguration config) { + // TODO: b/63912484 implement updateOutputConfiguration. + } + + @Override public void waitUntilIdle() throws RemoteException { if (DEBUG) { Log.d(TAG, "waitUntilIdle called."); diff --git a/android/hardware/camera2/params/OutputConfiguration.java b/android/hardware/camera2/params/OutputConfiguration.java index 2b317d67..7409671f 100644 --- a/android/hardware/camera2/params/OutputConfiguration.java +++ b/android/hardware/camera2/params/OutputConfiguration.java @@ -42,6 +42,53 @@ import static com.android.internal.util.Preconditions.*; * A class for describing camera output, which contains a {@link Surface} and its specific * configuration for creating capture session. * + * <p>There are several ways to instantiate, modify and use OutputConfigurations. The most common + * and recommended usage patterns are summarized in the following list:</p> + *<ul> + * <li>Passing a {@link Surface} to the constructor and using the OutputConfiguration instance as + * argument to {@link CameraDevice#createCaptureSessionByOutputConfigurations}. This is the most + * frequent usage and clients should consider it first before other more complicated alternatives. + * </li> + * + * <li>Passing only a surface source class as an argument to the constructor. This is usually + * followed by a call to create a capture session + * (see {@link CameraDevice#createCaptureSessionByOutputConfigurations} and a {@link Surface} add + * call {@link #addSurface} with a valid {@link Surface}. The sequence completes with + * {@link CameraCaptureSession#finalizeOutputConfigurations}. This is the deferred usage case which + * aims to enhance performance by allowing the resource-intensive capture session create call to + * execute in parallel with any {@link Surface} initialization, such as waiting for a + * {@link android.view.SurfaceView} to be ready as part of the UI initialization.</li> + * + * <li>The third and most complex usage pattern inlvolves surface sharing. Once instantiated an + * OutputConfiguration can be enabled for surface sharing via {@link #enableSurfaceSharing}. This + * must be done before creating a new capture session and enables calls to + * {@link CameraCaptureSession#updateOutputConfiguration}. An OutputConfiguration with enabled + * surface sharing can be modified via {@link #addSurface} or {@link #removeSurface}. The updates + * to this OutputConfiguration will only come into effect after + * {@link CameraCaptureSession#updateOutputConfiguration} returns without throwing exceptions. + * Such updates can be done as long as the session is active. Clients should always consider the + * additional requirements and limitations placed on the output surfaces (for more details see + * {@link #enableSurfaceSharing}, {@link #addSurface}, {@link #removeSurface}, + * {@link CameraCaptureSession#updateOutputConfiguration}). A trade-off exists between additional + * complexity and flexibility. If exercised correctly surface sharing can switch between different + * output surfaces without interrupting any ongoing repeating capture requests. This saves time and + * can significantly improve the user experience.</li> + * + * <li>Surface sharing can be used in combination with deferred surfaces. The rules from both cases + * are combined and clients must call {@link #enableSurfaceSharing} before creating a capture + * session. Attach and/or remove output surfaces via {@link #addSurface}/{@link #removeSurface} and + * finalize the configuration using {@link CameraCaptureSession#finalizeOutputConfigurations}. + * {@link CameraCaptureSession#updateOutputConfiguration} can be called after the configuration + * finalize method returns without exceptions.</li> + * + * </ul> + * + * <p>Please note that surface sharing is currently only enabled for outputs that use the + * {@link ImageFormat#PRIVATE} format. This includes surface sources like + * {@link android.view.SurfaceView}, {@link android.media.MediaRecorder}, + * {@link android.graphics.SurfaceTexture} and {@link android.media.ImageReader}, configured using + * the aforementioned format.</p> + * * @see CameraDevice#createCaptureSessionByOutputConfigurations * */ @@ -123,7 +170,7 @@ public final class OutputConfiguration implements Parcelable { * {@link OutputConfiguration#addSurface} should not exceed this value.</p> * */ - private static final int MAX_SURFACES_COUNT = 2; + private static final int MAX_SURFACES_COUNT = 4; /** * Create a new {@link OutputConfiguration} instance with a {@link Surface}, @@ -280,7 +327,10 @@ public final class OutputConfiguration implements Parcelable { * <p>For advanced use cases, a camera application may require more streams than the combination * guaranteed by {@link CameraDevice#createCaptureSession}. In this case, more than one * compatible surface can be attached to an OutputConfiguration so that they map to one - * camera stream, and the outputs share memory buffers when possible. </p> + * camera stream, and the outputs share memory buffers when possible. Due to buffer sharing + * clients should be careful when adding surface outputs that modify their input data. If such + * case exists, camera clients should have an additional mechanism to synchronize read and write + * access between individual consumers.</p> * * <p>Two surfaces are compatible in the below cases:</p> * @@ -301,9 +351,9 @@ public final class OutputConfiguration implements Parcelable { * CameraDevice#createCaptureSessionByOutputConfigurations}. Calling this function after {@link * CameraDevice#createCaptureSessionByOutputConfigurations} has no effect.</p> * - * <p>Up to 2 surfaces can be shared for an OutputConfiguration. The supported surfaces for - * sharing must be of type SurfaceTexture, SurfaceView, MediaRecorder, MediaCodec, or - * implementation defined ImageReader.</p> + * <p>Up to {@link #getMaxSharedSurfaceCount} surfaces can be shared for an OutputConfiguration. + * The supported surfaces for sharing must be of type SurfaceTexture, SurfaceView, + * MediaRecorder, MediaCodec, or implementation defined ImageReader.</p> */ public void enableSurfaceSharing() { mIsShared = true; @@ -329,8 +379,10 @@ public final class OutputConfiguration implements Parcelable { * <p> This function can be called before or after {@link * CameraDevice#createCaptureSessionByOutputConfigurations}. If it's called after, * the application must finalize the capture session with - * {@link CameraCaptureSession#finalizeOutputConfigurations}. - * </p> + * {@link CameraCaptureSession#finalizeOutputConfigurations}. It is possible to call this method + * after the output configurations have been finalized only in cases of enabled surface sharing + * see {@link #enableSurfaceSharing}. The modified output configuration must be updated with + * {@link CameraCaptureSession#updateOutputConfiguration}.</p> * * <p> If the OutputConfiguration was constructed with a deferred surface by {@link * OutputConfiguration#OutputConfiguration(Size, Class)}, the added surface must be obtained @@ -388,6 +440,31 @@ public final class OutputConfiguration implements Parcelable { } /** + * Remove a surface from this OutputConfiguration. + * + * <p> Surfaces added via calls to {@link #addSurface} can also be removed from the + * OutputConfiguration. The only notable exception is the surface associated with + * the OutputConfigration see {@link #getSurface} which was passed as part of the constructor + * or was added first in the deferred case + * {@link OutputConfiguration#OutputConfiguration(Size, Class)}.</p> + * + * @param surface The surface to be removed. + * + * @throws IllegalArgumentException If the surface is associated with this OutputConfiguration + * (see {@link #getSurface}) or the surface didn't get added + * with {@link #addSurface}. + */ + public void removeSurface(@NonNull Surface surface) { + if (getSurface() == surface) { + throw new IllegalArgumentException( + "Cannot remove surface associated with this output configuration"); + } + if (!mSurfaces.remove(surface)) { + throw new IllegalArgumentException("Surface is not part of this output configuration"); + } + } + + /** * Create a new {@link OutputConfiguration} instance with another {@link OutputConfiguration} * instance. * @@ -447,6 +524,17 @@ public final class OutputConfiguration implements Parcelable { } /** + * Get the maximum supported shared {@link Surface} count. + * + * @return the maximum number of surfaces that can be added per each OutputConfiguration. + * + * @see #enableSurfaceSharing + */ + public static int getMaxSharedSurfaceCount() { + return MAX_SURFACES_COUNT; + } + + /** * Get the {@link Surface} associated with this {@link OutputConfiguration}. * * If more than one surface is associated with this {@link OutputConfiguration}, return the diff --git a/android/hardware/display/BrightnessChangeEvent.java b/android/hardware/display/BrightnessChangeEvent.java new file mode 100644 index 00000000..fe24e32e --- /dev/null +++ b/android/hardware/display/BrightnessChangeEvent.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017 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 android.hardware.display; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Data about a brightness settings change. + * TODO make this SystemAPI + * @hide + */ +public final class BrightnessChangeEvent implements Parcelable { + /** Brightness in nits */ + public int brightness; + + /** Timestamp of the change {@see System.currentTimeMillis()} */ + public long timeStamp; + + /** Package name of focused activity when brightness was changed. */ + public String packageName; + + /** User id of of the user running when brightness was changed. + * @hide */ + public int userId; + + /** Lux values of recent sensor data */ + public float[] luxValues; + + /** Timestamps of the lux sensor readings {@see System.currentTimeMillis()} */ + public long[] luxTimestamps; + + /** Most recent battery level when brightness was changed or Float.NaN */ + public float batteryLevel; + + /** Color filter active to provide night mode */ + public boolean nightMode; + + /** If night mode color filter is active this will be the temperature in kelvin */ + public int colorTemperature; + + /** Brightness level before slider adjustment */ + public int lastBrightness; + + public BrightnessChangeEvent() { + } + + private BrightnessChangeEvent(Parcel source) { + brightness = source.readInt(); + timeStamp = source.readLong(); + packageName = source.readString(); + userId = source.readInt(); + luxValues = source.createFloatArray(); + luxTimestamps = source.createLongArray(); + batteryLevel = source.readFloat(); + nightMode = source.readBoolean(); + colorTemperature = source.readInt(); + lastBrightness = source.readInt(); + } + + public static final Creator<BrightnessChangeEvent> CREATOR = + new Creator<BrightnessChangeEvent>() { + public BrightnessChangeEvent createFromParcel(Parcel source) { + return new BrightnessChangeEvent(source); + } + public BrightnessChangeEvent[] newArray(int size) { + return new BrightnessChangeEvent[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(brightness); + dest.writeLong(timeStamp); + dest.writeString(packageName); + dest.writeInt(userId); + dest.writeFloatArray(luxValues); + dest.writeLongArray(luxTimestamps); + dest.writeFloat(batteryLevel); + dest.writeBoolean(nightMode); + dest.writeInt(colorTemperature); + dest.writeInt(lastBrightness); + } +} diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java index b2af44ec..ef77d6e6 100644 --- a/android/hardware/display/DisplayManager.java +++ b/android/hardware/display/DisplayManager.java @@ -30,6 +30,7 @@ import android.view.Surface; import android.view.WindowManagerPolicy; import java.util.ArrayList; +import java.util.List; /** * Manages the properties of attached displays. @@ -615,6 +616,21 @@ public final class DisplayManager { } /** + * Fetch {@link BrightnessChangeEvent}s. + * @hide until we make it a system api. + */ + public List<BrightnessChangeEvent> getBrightnessEvents() { + return mGlobal.getBrightnessEvents(); + } + + /** + * @hide STOPSHIP - remove when adaptive brightness accepts curves. + */ + public void setBrightness(int brightness) { + mGlobal.setBrightness(brightness); + } + + /** * Listens for changes in available display devices. */ public interface DisplayListener { diff --git a/android/hardware/display/DisplayManagerGlobal.java b/android/hardware/display/DisplayManagerGlobal.java index a8a4eb67..d93d0e4e 100644 --- a/android/hardware/display/DisplayManagerGlobal.java +++ b/android/hardware/display/DisplayManagerGlobal.java @@ -17,6 +17,7 @@ package android.hardware.display; import android.content.Context; +import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.graphics.Point; import android.hardware.display.DisplayManager.DisplayListener; @@ -37,6 +38,8 @@ import android.view.DisplayInfo; import android.view.Surface; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Manager communication with the display manager service on behalf of @@ -456,6 +459,33 @@ public final class DisplayManagerGlobal { } } + /** + * Retrieves brightness change events. + */ + public List<BrightnessChangeEvent> getBrightnessEvents() { + try { + ParceledListSlice<BrightnessChangeEvent> events = mDm.getBrightnessEvents(); + if (events == null) { + return Collections.emptyList(); + } + return events.getList(); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** + * Set brightness but don't add a BrightnessChangeEvent + * STOPSHIP remove when adaptive brightness accepts curves. + */ + public void setBrightness(int brightness) { + try { + mDm.setBrightness(brightness); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub { @Override public void onDisplayEvent(int displayId, int event) { diff --git a/android/hardware/location/ContextHubManager.java b/android/hardware/location/ContextHubManager.java index 7cbb436c..24117278 100644 --- a/android/hardware/location/ContextHubManager.java +++ b/android/hardware/location/ContextHubManager.java @@ -271,6 +271,59 @@ public final class ContextHubManager { throw new UnsupportedOperationException("TODO: Implement this"); } + /* + * Helper function to generate a stub for a non-query transaction callback. + * + * @param transaction the transaction to unblock when complete + * + * @return the callback + * + * @hide + */ + private IContextHubTransactionCallback createTransactionCallback( + ContextHubTransaction<Void> transaction) { + return new IContextHubTransactionCallback.Stub() { + @Override + public void onQueryResponse(int result, List<NanoAppState> nanoappList) { + Log.e(TAG, "Received a query callback on a non-query request"); + transaction.setResponse(new ContextHubTransaction.Response<Void>( + ContextHubTransaction.TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE, null)); + } + + @Override + public void onTransactionComplete(int result) { + transaction.setResponse(new ContextHubTransaction.Response<Void>(result, null)); + } + }; + } + + /* + * Helper function to generate a stub for a query transaction callback. + * + * @param transaction the transaction to unblock when complete + * + * @return the callback + * + * @hide + */ + private IContextHubTransactionCallback createQueryCallback( + ContextHubTransaction<List<NanoAppState>> transaction) { + return new IContextHubTransactionCallback.Stub() { + @Override + public void onQueryResponse(int result, List<NanoAppState> nanoappList) { + transaction.setResponse(new ContextHubTransaction.Response<List<NanoAppState>>( + result, nanoappList)); + } + + @Override + public void onTransactionComplete(int result) { + Log.e(TAG, "Received a non-query callback on a query request"); + transaction.setResponse(new ContextHubTransaction.Response<List<NanoAppState>>( + ContextHubTransaction.TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE, null)); + } + }; + } + /** * Loads a nanoapp at the specified Context Hub. * @@ -411,7 +464,7 @@ public final class ContextHubManager { * * @param callback the notification callback to register * @param hubInfo the hub to attach this client to - * @param handler the handler to invoke the callback, if null uses the current thread Looper + * @param handler the handler to invoke the callback, if null uses the main thread's Looper * * @return the registered client object * diff --git a/android/hardware/location/ContextHubTransaction.java b/android/hardware/location/ContextHubTransaction.java index 4877d38b..a8569ef4 100644 --- a/android/hardware/location/ContextHubTransaction.java +++ b/android/hardware/location/ContextHubTransaction.java @@ -16,11 +16,16 @@ package android.hardware.location; import android.annotation.IntDef; +import android.annotation.NonNull; import android.os.Handler; +import android.os.Looper; +import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * A class describing a request sent to the Context Hub Service. @@ -29,17 +34,15 @@ import java.util.concurrent.TimeUnit; * through the ContextHubManager APIs. The caller can either retrieve the result * synchronously through a blocking call ({@link #waitForResponse(long, TimeUnit)}) or * asynchronously through a user-defined callback - * ({@link #onComplete(ContextHubTransaction.Callback<T>, Handler)}). - * - * A transaction can be invalidated if the caller of the transaction is no longer active - * and the reference to this object is lost, or if timeout period has passed in - * {@link #waitForResponse(long, TimeUnit)}. + * ({@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)}). * * @param <T> the type of the contents in the transaction response * * @hide */ public class ContextHubTransaction<T> { + private static final String TAG = "ContextHubTransaction"; + /** * Constants describing the type of a transaction through the Context Hub Service. */ @@ -68,7 +71,8 @@ public class ContextHubTransaction<T> { TRANSACTION_FAILED_UNINITIALIZED, TRANSACTION_FAILED_PENDING, TRANSACTION_FAILED_AT_HUB, - TRANSACTION_FAILED_TIMEOUT}) + TRANSACTION_FAILED_TIMEOUT, + TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE}) public @interface Result {} public static final int TRANSACTION_SUCCESS = 0; /** @@ -95,6 +99,10 @@ public class ContextHubTransaction<T> { * Failure mode when the transaction has timed out. */ public static final int TRANSACTION_FAILED_TIMEOUT = 6; + /** + * Failure mode when the transaction has failed internally at the service. + */ + public static final int TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE = 7; /** * A class describing the response for a ContextHubTransaction. @@ -146,11 +154,6 @@ public class ContextHubTransaction<T> { } /* - * The unique identifier representing the transaction. - */ - private int mTransactionId; - - /* * The type of the transaction. */ @Type @@ -171,8 +174,17 @@ public class ContextHubTransaction<T> { */ private ContextHubTransaction.Callback<T> mCallback = null; - ContextHubTransaction(int id, @Type int type) { - mTransactionId = id; + /* + * Synchronization latch used to block on response. + */ + private final CountDownLatch mDoneSignal = new CountDownLatch(1); + + /* + * true if the response has been set throught setResponse, false otherwise. + */ + private boolean mIsResponseSet = false; + + ContextHubTransaction(@Type int type) { mTransactionType = type; } @@ -191,17 +203,26 @@ public class ContextHubTransaction<T> { * for the transaction represented by this object by the Context Hub, or a * specified timeout period has elapsed. * - * If the specified timeout has passed, the transaction represented by this object - * is invalidated by the Context Hub Service (resulting in a timeout failure in the - * response). + * If the specified timeout has passed, a TimeoutException will be thrown and the caller may + * retry the invocation of this method at a later time. * * @param timeout the timeout duration * @param unit the unit of the timeout * * @return the transaction response + * + * @throws InterruptedException if the current thread is interrupted while waiting for response + * @throws TimeoutException if the timeout period has passed */ - public ContextHubTransaction.Response<T> waitForResponse(long timeout, TimeUnit unit) { - throw new UnsupportedOperationException("TODO: Implement this"); + public ContextHubTransaction.Response<T> waitForResponse( + long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { + boolean success = mDoneSignal.await(timeout, unit); + + if (!success) { + throw new TimeoutException("Timed out while waiting for transaction"); + } + + return mResponse; } /** @@ -215,15 +236,100 @@ public class ContextHubTransaction<T> { * will be immediately posted by the handler. If the transaction has been invalidated, * the callback will never be invoked. * + * A transaction can be invalidated if the process owning the transaction is no longer active + * and the reference to this object is lost. + * + * This method or {@link #setCallbackOnCompletecan(ContextHubTransaction.Callback)} can only be + * invoked once, or an IllegalStateException will be thrown. + * * @param callback the callback to be invoked upon completion * @param handler the handler to post the callback + * + * @throws IllegalStateException if this method is called multiple times + * @throws NullPointerException if the callback or handler is null */ - public void onComplete(ContextHubTransaction.Callback<T> callback, Handler handler) { - throw new UnsupportedOperationException("TODO: Implement this"); + public void setCallbackOnComplete( + @NonNull ContextHubTransaction.Callback<T> callback, @NonNull Handler handler) { + synchronized (this) { + if (callback == null) { + throw new NullPointerException("Callback cannot be null"); + } + if (handler == null) { + throw new NullPointerException("Handler cannot be null"); + } + if (mCallback != null) { + throw new IllegalStateException( + "Cannot set ContextHubTransaction callback multiple times"); + } + + mCallback = callback; + mHandler = handler; + + if (mDoneSignal.getCount() == 0) { + boolean callbackPosted = mHandler.post(() -> { + mCallback.onComplete(this, mResponse); + }); + + if (!callbackPosted) { + Log.e(TAG, "Failed to post callback to Handler"); + } + } + } } - private void setResponse(ContextHubTransaction.Response<T> response) { - mResponse = response; - throw new UnsupportedOperationException("TODO: Unblock waitForResponse"); + /** + * Sets a callback to be invoked when the transaction completes. + * + * Equivalent to {@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)} + * with the handler being that of the main thread's Looper. + * + * This method or {@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)} + * can only be invoked once, or an IllegalStateException will be thrown. + * + * @param callback the callback to be invoked upon completion + * + * @throws IllegalStateException if this method is called multiple times + * @throws NullPointerException if the callback is null + */ + public void setCallbackOnComplete(@NonNull ContextHubTransaction.Callback<T> callback) { + setCallbackOnComplete(callback, new Handler(Looper.getMainLooper())); + } + + /** + * Sets the response of the transaction. + * + * This method should only be invoked by ContextHubManager as a result of a callback from + * the Context Hub Service indicating the response from a transaction. This method should not be + * invoked more than once. + * + * @param response the response to set + * + * @throws IllegalStateException if this method is invoked multiple times + * @throws NullPointerException if the response is null + */ + void setResponse(ContextHubTransaction.Response<T> response) { + synchronized (this) { + if (response == null) { + throw new NullPointerException("Response cannot be null"); + } + if (mIsResponseSet) { + throw new IllegalStateException( + "Cannot set response of ContextHubTransaction multiple times"); + } + + mResponse = response; + mIsResponseSet = true; + + mDoneSignal.countDown(); + if (mCallback != null) { + boolean callbackPosted = mHandler.post(() -> { + mCallback.onComplete(this, mResponse); + }); + + if (!callbackPosted) { + Log.e(TAG, "Failed to post callback to Handler"); + } + } + } } } diff --git a/android/hardware/location/NanoAppBinary.java b/android/hardware/location/NanoAppBinary.java index 54542277..934e9e48 100644 --- a/android/hardware/location/NanoAppBinary.java +++ b/android/hardware/location/NanoAppBinary.java @@ -22,6 +22,7 @@ import android.util.Log; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.Arrays; /** * @hide @@ -57,7 +58,7 @@ public final class NanoAppBinary implements Parcelable { private static final int EXPECTED_HEADER_VERSION = 1; /* - * The magic value expected in the header. + * The magic value expected in the header as defined in context_hub.h. */ private static final int EXPECTED_MAGIC_VALUE = (((int) 'N' << 0) | ((int) 'A' << 8) | ((int) 'N' << 16) | ((int) 'O' << 24)); @@ -67,6 +68,17 @@ public final class NanoAppBinary implements Parcelable { */ private static final ByteOrder HEADER_ORDER = ByteOrder.LITTLE_ENDIAN; + /* + * The size of the header in bytes as defined in context_hub.h. + */ + private static final int HEADER_SIZE_BYTES = 40; + + /* + * The bit fields for mFlags as defined in context_hub.h. + */ + private static final int NANOAPP_SIGNED_FLAG_BIT = 0x1; + private static final int NANOAPP_ENCRYPTED_FLAG_BIT = 0x2; + public NanoAppBinary(byte[] appBinary) { mNanoAppBinary = appBinary; parseBinaryHeader(); @@ -111,11 +123,26 @@ public final class NanoAppBinary implements Parcelable { /** * @return the app binary byte array */ - public byte[] getNanoAppBinary() { + public byte[] getBinary() { return mNanoAppBinary; } /** + * @return the app binary byte array without the leading header + * + * @throws IndexOutOfBoundsException if the nanoapp binary size is smaller than the header size + * @throws NullPointerException if the nanoapp binary is null + */ + public byte[] getBinaryNoHeader() { + if (mNanoAppBinary.length < HEADER_SIZE_BYTES) { + throw new IndexOutOfBoundsException("NanoAppBinary binary byte size (" + + mNanoAppBinary.length + ") is less than header size (" + HEADER_SIZE_BYTES + ")"); + } + + return Arrays.copyOfRange(mNanoAppBinary, HEADER_SIZE_BYTES, mNanoAppBinary.length); + } + + /** * @return {@code true} if the header is valid, {@code false} otherwise */ public boolean hasValidHeader() { @@ -164,6 +191,31 @@ public final class NanoAppBinary implements Parcelable { return mTargetChreApiMinorVersion; } + /** + * Returns the flags for the nanoapp as defined in context_hub.h. + * + * This method is meant to be used by the Context Hub Service. + * + * @return the flags for the nanoapp + */ + public int getFlags() { + return mFlags; + } + + /** + * @return {@code true} if the nanoapp binary is signed, {@code false} otherwise + */ + public boolean isSigned() { + return (mFlags & NANOAPP_SIGNED_FLAG_BIT) != 0; + } + + /** + * @return {@code true} if the nanoapp binary is encrypted, {@code false} otherwise + */ + public boolean isEncrypted() { + return (mFlags & NANOAPP_ENCRYPTED_FLAG_BIT) != 0; + } + private NanoAppBinary(Parcel in) { int binaryLength = in.readInt(); mNanoAppBinary = new byte[binaryLength]; diff --git a/android/hardware/sidekick/SidekickInternal.java b/android/hardware/sidekick/SidekickInternal.java new file mode 100644 index 00000000..fe3550b2 --- /dev/null +++ b/android/hardware/sidekick/SidekickInternal.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 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 android.hardware.sidekick; + + +/** + * Sidekick local system service interface. + * + * @hide Only for use within the system server, and maybe by Clockwork Home. + */ +public abstract class SidekickInternal { + + /** + * Tell Sidekick to reset back to newly-powered-on state. + * + * @return true on success (Sidekick is reset), false if Sidekick is not + * available (failed or not present). Either way, upon return Sidekick is + * guaranteed not to be controlling the display. + */ + public abstract boolean reset(); + + /** + * Tell Sidekick it can start controlling the display. + * + * SidekickServer may choose not to actually control the display, if it's been told + * via other channels to leave the previous image on the display (same as SUSPEND in + * a non-Sidekick system). + * + * @param displayState - one of Display.STATE_DOZE_SUSPEND, Display.STATE_ON_SUSPEND + * @return true on success, false on failure (no sidekick available) + */ + public abstract boolean startDisplayControl(int displayState); + + /** + * Tell Sidekick it must stop controlling the display. + * + * No return code because this must always succeed - after return, Sidekick + * is guaranteed to not be controlling the display. + */ + public abstract void endDisplayControl(); + +} diff --git a/android/hardware/usb/AccessoryFilter.java b/android/hardware/usb/AccessoryFilter.java new file mode 100644 index 00000000..d9b7c5be --- /dev/null +++ b/android/hardware/usb/AccessoryFilter.java @@ -0,0 +1,145 @@ +/* + * Copyright 2017 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 android.hardware.usb; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Objects; + +/** + * This class is used to describe a USB accessory. + * When used in HashMaps all values must be specified, + * but wildcards can be used for any of the fields in + * the package meta-data. + * + * @hide + */ +public class AccessoryFilter { + // USB accessory manufacturer (or null for unspecified) + public final String mManufacturer; + // USB accessory model (or null for unspecified) + public final String mModel; + // USB accessory version (or null for unspecified) + public final String mVersion; + + public AccessoryFilter(String manufacturer, String model, String version) { + mManufacturer = manufacturer; + mModel = model; + mVersion = version; + } + + public AccessoryFilter(UsbAccessory accessory) { + mManufacturer = accessory.getManufacturer(); + mModel = accessory.getModel(); + mVersion = accessory.getVersion(); + } + + public static AccessoryFilter read(XmlPullParser parser) + throws XmlPullParserException, IOException { + String manufacturer = null; + String model = null; + String version = null; + + int count = parser.getAttributeCount(); + for (int i = 0; i < count; i++) { + String name = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + + if ("manufacturer".equals(name)) { + manufacturer = value; + } else if ("model".equals(name)) { + model = value; + } else if ("version".equals(name)) { + version = value; + } + } + return new AccessoryFilter(manufacturer, model, version); + } + + public void write(XmlSerializer serializer)throws IOException { + serializer.startTag(null, "usb-accessory"); + if (mManufacturer != null) { + serializer.attribute(null, "manufacturer", mManufacturer); + } + if (mModel != null) { + serializer.attribute(null, "model", mModel); + } + if (mVersion != null) { + serializer.attribute(null, "version", mVersion); + } + serializer.endTag(null, "usb-accessory"); + } + + public boolean matches(UsbAccessory acc) { + if (mManufacturer != null && !acc.getManufacturer().equals(mManufacturer)) return false; + if (mModel != null && !acc.getModel().equals(mModel)) return false; + return !(mVersion != null && !acc.getVersion().equals(mVersion)); + } + + /** + * Is the accessories described {@code accessory} covered by this filter? + * + * @param accessory A filter describing the accessory + * + * @return {@code true} iff this the filter covers the accessory + */ + public boolean contains(AccessoryFilter accessory) { + if (mManufacturer != null && !Objects.equals(accessory.mManufacturer, mManufacturer)) { + return false; + } + if (mModel != null && !Objects.equals(accessory.mModel, mModel)) return false; + return !(mVersion != null && !Objects.equals(accessory.mVersion, mVersion)); + } + + @Override + public boolean equals(Object obj) { + // can't compare if we have wildcard strings + if (mManufacturer == null || mModel == null || mVersion == null) { + return false; + } + if (obj instanceof AccessoryFilter) { + AccessoryFilter filter = (AccessoryFilter)obj; + return (mManufacturer.equals(filter.mManufacturer) && + mModel.equals(filter.mModel) && + mVersion.equals(filter.mVersion)); + } + if (obj instanceof UsbAccessory) { + UsbAccessory accessory = (UsbAccessory)obj; + return (mManufacturer.equals(accessory.getManufacturer()) && + mModel.equals(accessory.getModel()) && + mVersion.equals(accessory.getVersion())); + } + return false; + } + + @Override + public int hashCode() { + return ((mManufacturer == null ? 0 : mManufacturer.hashCode()) ^ + (mModel == null ? 0 : mModel.hashCode()) ^ + (mVersion == null ? 0 : mVersion.hashCode())); + } + + @Override + public String toString() { + return "AccessoryFilter[mManufacturer=\"" + mManufacturer + + "\", mModel=\"" + mModel + + "\", mVersion=\"" + mVersion + "\"]"; + } +} diff --git a/android/hardware/usb/DeviceFilter.java b/android/hardware/usb/DeviceFilter.java new file mode 100644 index 00000000..439c6297 --- /dev/null +++ b/android/hardware/usb/DeviceFilter.java @@ -0,0 +1,313 @@ +/* + * Copyright 2017 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 android.hardware.usb; + +import android.util.Slog; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Objects; + +/** + * This class is used to describe a USB device. + * When used in HashMaps all values must be specified, + * but wildcards can be used for any of the fields in + * the package meta-data. + * + * @hide + */ +public class DeviceFilter { + private static final String TAG = DeviceFilter.class.getSimpleName(); + + // USB Vendor ID (or -1 for unspecified) + public final int mVendorId; + // USB Product ID (or -1 for unspecified) + public final int mProductId; + // USB device or interface class (or -1 for unspecified) + public final int mClass; + // USB device subclass (or -1 for unspecified) + public final int mSubclass; + // USB device protocol (or -1 for unspecified) + public final int mProtocol; + // USB device manufacturer name string (or null for unspecified) + public final String mManufacturerName; + // USB device product name string (or null for unspecified) + public final String mProductName; + // USB device serial number string (or null for unspecified) + public final String mSerialNumber; + + public DeviceFilter(int vid, int pid, int clasz, int subclass, int protocol, + String manufacturer, String product, String serialnum) { + mVendorId = vid; + mProductId = pid; + mClass = clasz; + mSubclass = subclass; + mProtocol = protocol; + mManufacturerName = manufacturer; + mProductName = product; + mSerialNumber = serialnum; + } + + public DeviceFilter(UsbDevice device) { + mVendorId = device.getVendorId(); + mProductId = device.getProductId(); + mClass = device.getDeviceClass(); + mSubclass = device.getDeviceSubclass(); + mProtocol = device.getDeviceProtocol(); + mManufacturerName = device.getManufacturerName(); + mProductName = device.getProductName(); + mSerialNumber = device.getSerialNumber(); + } + + public static DeviceFilter read(XmlPullParser parser) + throws XmlPullParserException, IOException { + int vendorId = -1; + int productId = -1; + int deviceClass = -1; + int deviceSubclass = -1; + int deviceProtocol = -1; + String manufacturerName = null; + String productName = null; + String serialNumber = null; + + int count = parser.getAttributeCount(); + for (int i = 0; i < count; i++) { + String name = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + // Attribute values are ints or strings + if ("manufacturer-name".equals(name)) { + manufacturerName = value; + } else if ("product-name".equals(name)) { + productName = value; + } else if ("serial-number".equals(name)) { + serialNumber = value; + } else { + int intValue; + int radix = 10; + if (value != null && value.length() > 2 && value.charAt(0) == '0' && + (value.charAt(1) == 'x' || value.charAt(1) == 'X')) { + // allow hex values starting with 0x or 0X + radix = 16; + value = value.substring(2); + } + try { + intValue = Integer.parseInt(value, radix); + } catch (NumberFormatException e) { + Slog.e(TAG, "invalid number for field " + name, e); + continue; + } + if ("vendor-id".equals(name)) { + vendorId = intValue; + } else if ("product-id".equals(name)) { + productId = intValue; + } else if ("class".equals(name)) { + deviceClass = intValue; + } else if ("subclass".equals(name)) { + deviceSubclass = intValue; + } else if ("protocol".equals(name)) { + deviceProtocol = intValue; + } + } + } + return new DeviceFilter(vendorId, productId, + deviceClass, deviceSubclass, deviceProtocol, + manufacturerName, productName, serialNumber); + } + + public void write(XmlSerializer serializer) throws IOException { + serializer.startTag(null, "usb-device"); + if (mVendorId != -1) { + serializer.attribute(null, "vendor-id", Integer.toString(mVendorId)); + } + if (mProductId != -1) { + serializer.attribute(null, "product-id", Integer.toString(mProductId)); + } + if (mClass != -1) { + serializer.attribute(null, "class", Integer.toString(mClass)); + } + if (mSubclass != -1) { + serializer.attribute(null, "subclass", Integer.toString(mSubclass)); + } + if (mProtocol != -1) { + serializer.attribute(null, "protocol", Integer.toString(mProtocol)); + } + if (mManufacturerName != null) { + serializer.attribute(null, "manufacturer-name", mManufacturerName); + } + if (mProductName != null) { + serializer.attribute(null, "product-name", mProductName); + } + if (mSerialNumber != null) { + serializer.attribute(null, "serial-number", mSerialNumber); + } + serializer.endTag(null, "usb-device"); + } + + private boolean matches(int clasz, int subclass, int protocol) { + return ((mClass == -1 || clasz == mClass) && + (mSubclass == -1 || subclass == mSubclass) && + (mProtocol == -1 || protocol == mProtocol)); + } + + public boolean matches(UsbDevice device) { + if (mVendorId != -1 && device.getVendorId() != mVendorId) return false; + if (mProductId != -1 && device.getProductId() != mProductId) return false; + if (mManufacturerName != null && device.getManufacturerName() == null) return false; + if (mProductName != null && device.getProductName() == null) return false; + if (mSerialNumber != null && device.getSerialNumber() == null) return false; + if (mManufacturerName != null && device.getManufacturerName() != null && + !mManufacturerName.equals(device.getManufacturerName())) return false; + if (mProductName != null && device.getProductName() != null && + !mProductName.equals(device.getProductName())) return false; + if (mSerialNumber != null && device.getSerialNumber() != null && + !mSerialNumber.equals(device.getSerialNumber())) return false; + + // check device class/subclass/protocol + if (matches(device.getDeviceClass(), device.getDeviceSubclass(), + device.getDeviceProtocol())) return true; + + // if device doesn't match, check the interfaces + int count = device.getInterfaceCount(); + for (int i = 0; i < count; i++) { + UsbInterface intf = device.getInterface(i); + if (matches(intf.getInterfaceClass(), intf.getInterfaceSubclass(), + intf.getInterfaceProtocol())) return true; + } + + return false; + } + + /** + * If the device described by {@code device} covered by this filter? + * + * @param device The device + * + * @return {@code true} iff this filter covers the {@code device} + */ + public boolean contains(DeviceFilter device) { + // -1 and null means "match anything" + + if (mVendorId != -1 && device.mVendorId != mVendorId) return false; + if (mProductId != -1 && device.mProductId != mProductId) return false; + if (mManufacturerName != null && !Objects.equals(mManufacturerName, + device.mManufacturerName)) { + return false; + } + if (mProductName != null && !Objects.equals(mProductName, device.mProductName)) { + return false; + } + if (mSerialNumber != null + && !Objects.equals(mSerialNumber, device.mSerialNumber)) { + return false; + } + + // check device class/subclass/protocol + return matches(device.mClass, device.mSubclass, device.mProtocol); + } + + @Override + public boolean equals(Object obj) { + // can't compare if we have wildcard strings + if (mVendorId == -1 || mProductId == -1 || + mClass == -1 || mSubclass == -1 || mProtocol == -1) { + return false; + } + if (obj instanceof DeviceFilter) { + DeviceFilter filter = (DeviceFilter)obj; + + if (filter.mVendorId != mVendorId || + filter.mProductId != mProductId || + filter.mClass != mClass || + filter.mSubclass != mSubclass || + filter.mProtocol != mProtocol) { + return(false); + } + if ((filter.mManufacturerName != null && + mManufacturerName == null) || + (filter.mManufacturerName == null && + mManufacturerName != null) || + (filter.mProductName != null && + mProductName == null) || + (filter.mProductName == null && + mProductName != null) || + (filter.mSerialNumber != null && + mSerialNumber == null) || + (filter.mSerialNumber == null && + mSerialNumber != null)) { + return(false); + } + if ((filter.mManufacturerName != null && + mManufacturerName != null && + !mManufacturerName.equals(filter.mManufacturerName)) || + (filter.mProductName != null && + mProductName != null && + !mProductName.equals(filter.mProductName)) || + (filter.mSerialNumber != null && + mSerialNumber != null && + !mSerialNumber.equals(filter.mSerialNumber))) { + return false; + } + return true; + } + if (obj instanceof UsbDevice) { + UsbDevice device = (UsbDevice)obj; + if (device.getVendorId() != mVendorId || + device.getProductId() != mProductId || + device.getDeviceClass() != mClass || + device.getDeviceSubclass() != mSubclass || + device.getDeviceProtocol() != mProtocol) { + return(false); + } + if ((mManufacturerName != null && device.getManufacturerName() == null) || + (mManufacturerName == null && device.getManufacturerName() != null) || + (mProductName != null && device.getProductName() == null) || + (mProductName == null && device.getProductName() != null) || + (mSerialNumber != null && device.getSerialNumber() == null) || + (mSerialNumber == null && device.getSerialNumber() != null)) { + return(false); + } + if ((device.getManufacturerName() != null && + !mManufacturerName.equals(device.getManufacturerName())) || + (device.getProductName() != null && + !mProductName.equals(device.getProductName())) || + (device.getSerialNumber() != null && + !mSerialNumber.equals(device.getSerialNumber()))) { + return false; + } + return true; + } + return false; + } + + @Override + public int hashCode() { + return (((mVendorId << 16) | mProductId) ^ + ((mClass << 16) | (mSubclass << 8) | mProtocol)); + } + + @Override + public String toString() { + return "DeviceFilter[mVendorId=" + mVendorId + ",mProductId=" + mProductId + + ",mClass=" + mClass + ",mSubclass=" + mSubclass + + ",mProtocol=" + mProtocol + ",mManufacturerName=" + mManufacturerName + + ",mProductName=" + mProductName + ",mSerialNumber=" + mSerialNumber + + "]"; + } +} diff --git a/android/hardware/usb/UsbManager.java b/android/hardware/usb/UsbManager.java index 996824d4..6ce96698 100644 --- a/android/hardware/usb/UsbManager.java +++ b/android/hardware/usb/UsbManager.java @@ -102,7 +102,7 @@ public class UsbManager { "android.hardware.usb.action.USB_PORT_CHANGED"; /** - * Activity intent sent when a USB device is attached. + * Activity intent sent when user attaches a USB device. * * This intent is sent when a USB device is attached to the USB bus when in host mode. * <ul> @@ -128,7 +128,7 @@ public class UsbManager { "android.hardware.usb.action.USB_DEVICE_DETACHED"; /** - * Activity intent sent when a USB accessory is attached. + * Activity intent sent when user attaches a USB accessory. * * <ul> * <li> {@link #EXTRA_ACCESSORY} containing the {@link android.hardware.usb.UsbAccessory} diff --git a/android/location/Location.java b/android/location/Location.java index e7f903e8..c9d2f7f8 100644 --- a/android/location/Location.java +++ b/android/location/Location.java @@ -821,7 +821,7 @@ public class Location implements Parcelable { * considered 1 standard deviation. * * <p>For example, if {@link #getAltitude()} returns 150, and - * {@link #getVerticalAccuracyMeters()} ()} returns 20 then there is a 68% probability + * {@link #getVerticalAccuracyMeters()} returns 20 then there is a 68% probability * of the true altitude being between 130 and 170 meters. * * <p>If this location does not have a vertical accuracy, then 0.0 is returned. @@ -933,7 +933,7 @@ public class Location implements Parcelable { * considered 1 standard deviation. * * <p>For example, if {@link #getBearing()} returns 60, and - * {@link #getBearingAccuracyDegrees()} ()} returns 10, then there is a 68% probability of the + * {@link #getBearingAccuracyDegrees()} returns 10, then there is a 68% probability of the * true bearing being between 50 and 70 degrees. * * <p>If this location does not have a bearing accuracy, then 0.0 is returned. diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java index 20405d3b..7afe267f 100644 --- a/android/media/AudioAttributes.java +++ b/android/media/AudioAttributes.java @@ -244,6 +244,7 @@ public final class AudioAttributes implements Parcelable { SUPPRESSIBLE_USAGES.put(USAGE_GAME, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); } /** diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java index dab7632a..58976ca0 100644 --- a/android/media/AudioManager.java +++ b/android/media/AudioManager.java @@ -43,17 +43,16 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; -import android.os.SystemClock; import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.util.ArrayMap; import android.util.Log; -import android.util.Pair; +import android.util.Slog; import android.view.KeyEvent; import java.util.ArrayList; -import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -1966,9 +1965,28 @@ public class AudioManager { */ private boolean querySoundEffectsEnabled(int user) { return Settings.System.getIntForUser(getContext().getContentResolver(), - Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0; + Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0 + && !areSystemSoundsZenModeBlocked(getContext()); } + private boolean areSystemSoundsZenModeBlocked(Context context) { + int zenMode = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.ZEN_MODE, 0); + + switch (zenMode) { + case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: + case Settings.Global.ZEN_MODE_ALARMS: + return true; + case Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + final NotificationManager noMan = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + return (noMan.getNotificationPolicy().priorityCategories + & NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER) == 0; + case Settings.Global.ZEN_MODE_OFF: + default: + return false; + } + } /** * Load Sound effects. diff --git a/android/media/BufferingParams.java b/android/media/BufferingParams.java index 681271b1..521e8975 100644 --- a/android/media/BufferingParams.java +++ b/android/media/BufferingParams.java @@ -26,170 +26,68 @@ import java.lang.annotation.RetentionPolicy; /** * Structure for source buffering management params. * - * Used by {@link MediaPlayer#getDefaultBufferingParams()}, - * {@link MediaPlayer#getBufferingParams()} and + * Used by {@link MediaPlayer#getBufferingParams()} and * {@link MediaPlayer#setBufferingParams(BufferingParams)} * to control source buffering behavior. * * <p>There are two stages of source buffering in {@link MediaPlayer}: initial buffering * (when {@link MediaPlayer} is being prepared) and rebuffering (when {@link MediaPlayer} - * is playing back source). {@link BufferingParams} includes mode and corresponding - * watermarks for each stage of source buffering. The watermarks could be either size - * based (in milliseconds), or time based (in kilobytes) or both, depending on the mode. + * is playing back source). {@link BufferingParams} includes corresponding marks for each + * stage of source buffering. The marks are time based (in milliseconds). * - * <p>There are 4 buffering modes: {@link #BUFFERING_MODE_NONE}, - * {@link #BUFFERING_MODE_TIME_ONLY}, {@link #BUFFERING_MODE_SIZE_ONLY} and - * {@link #BUFFERING_MODE_TIME_THEN_SIZE}. - * {@link MediaPlayer} source component has default buffering modes which can be queried - * by calling {@link MediaPlayer#getDefaultBufferingParams()}. - * Users should always use those default modes or their downsized version when trying to - * change buffering params. For example, {@link #BUFFERING_MODE_TIME_THEN_SIZE} can be - * downsized to {@link #BUFFERING_MODE_NONE}, {@link #BUFFERING_MODE_TIME_ONLY} or - * {@link #BUFFERING_MODE_SIZE_ONLY}. But {@link #BUFFERING_MODE_TIME_ONLY} can not be - * downsized to {@link #BUFFERING_MODE_SIZE_ONLY}. + * <p>{@link MediaPlayer} source component has default marks which can be queried by + * calling {@link MediaPlayer#getBufferingParams()} before any change is made by + * {@link MediaPlayer#setBufferingParams()}. * <ul> - * <li><strong>initial buffering stage:</strong> has one watermark which is used when - * {@link MediaPlayer} is being prepared. When cached data amount exceeds this watermark, - * {@link MediaPlayer} is prepared.</li> - * <li><strong>rebuffering stage:</strong> has two watermarks, low and high, which are - * used when {@link MediaPlayer} is playing back content. + * <li><strong>initial buffering:</strong> initialMarkMs is used when + * {@link MediaPlayer} is being prepared. When cached data amount exceeds this mark + * {@link MediaPlayer} is prepared. </li> + * <li><strong>rebuffering during playback:</strong> resumePlaybackMarkMs is used when + * {@link MediaPlayer} is playing back content. * <ul> - * <li> When cached data amount exceeds high watermark, {@link MediaPlayer} will pause - * buffering. Buffering will resume when cache runs below some limit which could be low - * watermark or some intermediate value decided by the source component.</li> - * <li> When cached data amount runs below low watermark, {@link MediaPlayer} will paused - * playback. Playback will resume when cached data amount exceeds high watermark - * or reaches end of stream.</li> - * </ul> + * <li> {@link MediaPlayer} has internal mark, namely pausePlaybackMarkMs, to decide when + * to pause playback if cached data amount runs low. This internal mark varies based on + * type of data source. </li> + * <li> When cached data amount exceeds resumePlaybackMarkMs, {@link MediaPlayer} will + * resume playback if it has been paused due to low cached data amount. The internal mark + * pausePlaybackMarkMs shall be less than resumePlaybackMarkMs. </li> + * <li> {@link MediaPlayer} has internal mark, namely pauseRebufferingMarkMs, to decide + * when to pause rebuffering. Apparently, this internal mark shall be no less than + * resumePlaybackMarkMs. </li> + * <li> {@link MediaPlayer} has internal mark, namely resumeRebufferingMarkMs, to decide + * when to resume buffering. This internal mark varies based on type of data source. This + * mark shall be larger than pausePlaybackMarkMs, and less than pauseRebufferingMarkMs. + * </li> + * </ul> </li> * </ul> * <p>Users should use {@link Builder} to change {@link BufferingParams}. * @hide */ public final class BufferingParams implements Parcelable { - /** - * This mode indicates that source buffering is not supported. - */ - public static final int BUFFERING_MODE_NONE = 0; - /** - * This mode indicates that only time based source buffering is supported. This means - * the watermark(s) are time based. - */ - public static final int BUFFERING_MODE_TIME_ONLY = 1; - /** - * This mode indicates that only size based source buffering is supported. This means - * the watermark(s) are size based. - */ - public static final int BUFFERING_MODE_SIZE_ONLY = 2; - /** - * This mode indicates that both time and size based source buffering are supported, - * and time based calculation precedes size based. Size based calculation will be used - * only when time information is not available from the source. - */ - public static final int BUFFERING_MODE_TIME_THEN_SIZE = 3; - - /** @hide */ - @IntDef( - value = { - BUFFERING_MODE_NONE, - BUFFERING_MODE_TIME_ONLY, - BUFFERING_MODE_SIZE_ONLY, - BUFFERING_MODE_TIME_THEN_SIZE, - } - ) - @Retention(RetentionPolicy.SOURCE) - public @interface BufferingMode {} - - private static final int BUFFERING_NO_WATERMARK = -1; + private static final int BUFFERING_NO_MARK = -1; // params - private int mInitialBufferingMode = BUFFERING_MODE_NONE; - private int mRebufferingMode = BUFFERING_MODE_NONE; - - private int mInitialWatermarkMs = BUFFERING_NO_WATERMARK; - private int mInitialWatermarkKB = BUFFERING_NO_WATERMARK; + private int mInitialMarkMs = BUFFERING_NO_MARK; - private int mRebufferingWatermarkLowMs = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkHighMs = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkLowKB = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkHighKB = BUFFERING_NO_WATERMARK; + private int mResumePlaybackMarkMs = BUFFERING_NO_MARK; private BufferingParams() { } /** - * Return the initial buffering mode used when {@link MediaPlayer} is being prepared. - * @return one of the values that can be set in {@link Builder#setInitialBufferingMode(int)} - */ - public int getInitialBufferingMode() { - return mInitialBufferingMode; - } - - /** - * Return the rebuffering mode used when {@link MediaPlayer} is playing back source. - * @return one of the values that can be set in {@link Builder#setRebufferingMode(int)} - */ - public int getRebufferingMode() { - return mRebufferingMode; - } - - /** - * Return the time based initial buffering watermark in milliseconds. - * It is meaningful only when initial buffering mode obatined from - * {@link #getInitialBufferingMode()} is time based. - * @return time based initial buffering watermark in milliseconds - */ - public int getInitialBufferingWatermarkMs() { - return mInitialWatermarkMs; - } - - /** - * Return the size based initial buffering watermark in kilobytes. - * It is meaningful only when initial buffering mode obatined from - * {@link #getInitialBufferingMode()} is size based. - * @return size based initial buffering watermark in kilobytes + * Return initial buffering mark in milliseconds. + * @return initial buffering mark in milliseconds */ - public int getInitialBufferingWatermarkKB() { - return mInitialWatermarkKB; + public int getInitialMarkMs() { + return mInitialMarkMs; } /** - * Return the time based low watermark in milliseconds for rebuffering. - * It is meaningful only when rebuffering mode obatined from - * {@link #getRebufferingMode()} is time based. - * @return time based low watermark for rebuffering in milliseconds + * Return the mark in milliseconds for resuming playback. + * @return the mark for resuming playback in milliseconds */ - public int getRebufferingWatermarkLowMs() { - return mRebufferingWatermarkLowMs; - } - - /** - * Return the time based high watermark in milliseconds for rebuffering. - * It is meaningful only when rebuffering mode obatined from - * {@link #getRebufferingMode()} is time based. - * @return time based high watermark for rebuffering in milliseconds - */ - public int getRebufferingWatermarkHighMs() { - return mRebufferingWatermarkHighMs; - } - - /** - * Return the size based low watermark in kilobytes for rebuffering. - * It is meaningful only when rebuffering mode obatined from - * {@link #getRebufferingMode()} is size based. - * @return size based low watermark for rebuffering in kilobytes - */ - public int getRebufferingWatermarkLowKB() { - return mRebufferingWatermarkLowKB; - } - - /** - * Return the size based high watermark in kilobytes for rebuffering. - * It is meaningful only when rebuffering mode obatined from - * {@link #getRebufferingMode()} is size based. - * @return size based high watermark for rebuffering in kilobytes - */ - public int getRebufferingWatermarkHighKB() { - return mRebufferingWatermarkHighKB; + public int getResumePlaybackMarkMs() { + return mResumePlaybackMarkMs; } /** @@ -200,27 +98,19 @@ public final class BufferingParams implements Parcelable { * <pre class="prettyprint"> * BufferingParams myParams = mediaplayer.getDefaultBufferingParams(); * myParams = new BufferingParams.Builder(myParams) - * .setInitialBufferingWatermarkMs(10000) - * .build(); + * .setInitialMarkMs(10000) + * .setResumePlaybackMarkMs(15000) + * .build(); * mediaplayer.setBufferingParams(myParams); * </pre> */ public static class Builder { - private int mInitialBufferingMode = BUFFERING_MODE_NONE; - private int mRebufferingMode = BUFFERING_MODE_NONE; - - private int mInitialWatermarkMs = BUFFERING_NO_WATERMARK; - private int mInitialWatermarkKB = BUFFERING_NO_WATERMARK; - - private int mRebufferingWatermarkLowMs = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkHighMs = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkLowKB = BUFFERING_NO_WATERMARK; - private int mRebufferingWatermarkHighKB = BUFFERING_NO_WATERMARK; + private int mInitialMarkMs = BUFFERING_NO_MARK; + private int mResumePlaybackMarkMs = BUFFERING_NO_MARK; /** * Constructs a new Builder with the defaults. - * By default, both initial buffering mode and rebuffering mode are - * {@link BufferingParams#BUFFERING_MODE_NONE}, and all watermarks are -1. + * By default, all marks are -1. */ public Builder() { } @@ -231,16 +121,8 @@ public final class BufferingParams implements Parcelable { * in the new Builder. */ public Builder(BufferingParams bp) { - mInitialBufferingMode = bp.mInitialBufferingMode; - mRebufferingMode = bp.mRebufferingMode; - - mInitialWatermarkMs = bp.mInitialWatermarkMs; - mInitialWatermarkKB = bp.mInitialWatermarkKB; - - mRebufferingWatermarkLowMs = bp.mRebufferingWatermarkLowMs; - mRebufferingWatermarkHighMs = bp.mRebufferingWatermarkHighMs; - mRebufferingWatermarkLowKB = bp.mRebufferingWatermarkLowKB; - mRebufferingWatermarkHighKB = bp.mRebufferingWatermarkHighKB; + mInitialMarkMs = bp.mInitialMarkMs; + mResumePlaybackMarkMs = bp.mResumePlaybackMarkMs; } /** @@ -250,179 +132,37 @@ public final class BufferingParams implements Parcelable { * @return a new {@link BufferingParams} object */ public BufferingParams build() { - if (isTimeBasedMode(mRebufferingMode) - && mRebufferingWatermarkLowMs > mRebufferingWatermarkHighMs) { - throw new IllegalStateException("Illegal watermark:" - + mRebufferingWatermarkLowMs + " : " + mRebufferingWatermarkHighMs); - } - if (isSizeBasedMode(mRebufferingMode) - && mRebufferingWatermarkLowKB > mRebufferingWatermarkHighKB) { - throw new IllegalStateException("Illegal watermark:" - + mRebufferingWatermarkLowKB + " : " + mRebufferingWatermarkHighKB); - } - BufferingParams bp = new BufferingParams(); - bp.mInitialBufferingMode = mInitialBufferingMode; - bp.mRebufferingMode = mRebufferingMode; - - bp.mInitialWatermarkMs = mInitialWatermarkMs; - bp.mInitialWatermarkKB = mInitialWatermarkKB; + bp.mInitialMarkMs = mInitialMarkMs; + bp.mResumePlaybackMarkMs = mResumePlaybackMarkMs; - bp.mRebufferingWatermarkLowMs = mRebufferingWatermarkLowMs; - bp.mRebufferingWatermarkHighMs = mRebufferingWatermarkHighMs; - bp.mRebufferingWatermarkLowKB = mRebufferingWatermarkLowKB; - bp.mRebufferingWatermarkHighKB = mRebufferingWatermarkHighKB; return bp; } - private boolean isTimeBasedMode(int mode) { - return (mode == BUFFERING_MODE_TIME_ONLY || mode == BUFFERING_MODE_TIME_THEN_SIZE); - } - - private boolean isSizeBasedMode(int mode) { - return (mode == BUFFERING_MODE_SIZE_ONLY || mode == BUFFERING_MODE_TIME_THEN_SIZE); - } - /** - * Sets the initial buffering mode. - * @param mode one of {@link BufferingParams#BUFFERING_MODE_NONE}, - * {@link BufferingParams#BUFFERING_MODE_TIME_ONLY}, - * {@link BufferingParams#BUFFERING_MODE_SIZE_ONLY}, - * {@link BufferingParams#BUFFERING_MODE_TIME_THEN_SIZE}, + * Sets the time based mark in milliseconds for initial buffering. + * @param markMs time based mark in milliseconds * @return the same Builder instance. */ - public Builder setInitialBufferingMode(@BufferingMode int mode) { - switch (mode) { - case BUFFERING_MODE_NONE: - case BUFFERING_MODE_TIME_ONLY: - case BUFFERING_MODE_SIZE_ONLY: - case BUFFERING_MODE_TIME_THEN_SIZE: - mInitialBufferingMode = mode; - break; - default: - throw new IllegalArgumentException("Illegal buffering mode " + mode); - } + public Builder setInitialMarkMs(int markMs) { + mInitialMarkMs = markMs; return this; } /** - * Sets the rebuffering mode. - * @param mode one of {@link BufferingParams#BUFFERING_MODE_NONE}, - * {@link BufferingParams#BUFFERING_MODE_TIME_ONLY}, - * {@link BufferingParams#BUFFERING_MODE_SIZE_ONLY}, - * {@link BufferingParams#BUFFERING_MODE_TIME_THEN_SIZE}, + * Sets the time based mark in milliseconds for resuming playback. + * @param markMs time based mark in milliseconds for resuming playback * @return the same Builder instance. */ - public Builder setRebufferingMode(@BufferingMode int mode) { - switch (mode) { - case BUFFERING_MODE_NONE: - case BUFFERING_MODE_TIME_ONLY: - case BUFFERING_MODE_SIZE_ONLY: - case BUFFERING_MODE_TIME_THEN_SIZE: - mRebufferingMode = mode; - break; - default: - throw new IllegalArgumentException("Illegal buffering mode " + mode); - } - return this; - } - - /** - * Sets the time based watermark in milliseconds for initial buffering. - * @param watermarkMs time based watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setInitialBufferingWatermarkMs(int watermarkMs) { - mInitialWatermarkMs = watermarkMs; - return this; - } - - /** - * Sets the size based watermark in kilobytes for initial buffering. - * @param watermarkKB size based watermark in kilobytes - * @return the same Builder instance. - */ - public Builder setInitialBufferingWatermarkKB(int watermarkKB) { - mInitialWatermarkKB = watermarkKB; - return this; - } - - /** - * Sets the time based low watermark in milliseconds for rebuffering. - * @param watermarkMs time based low watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarkLowMs(int watermarkMs) { - mRebufferingWatermarkLowMs = watermarkMs; - return this; - } - - /** - * Sets the time based high watermark in milliseconds for rebuffering. - * @param watermarkMs time based high watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarkHighMs(int watermarkMs) { - mRebufferingWatermarkHighMs = watermarkMs; - return this; - } - - /** - * Sets the size based low watermark in milliseconds for rebuffering. - * @param watermarkKB size based low watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarkLowKB(int watermarkKB) { - mRebufferingWatermarkLowKB = watermarkKB; - return this; - } - - /** - * Sets the size based high watermark in milliseconds for rebuffering. - * @param watermarkKB size based high watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarkHighKB(int watermarkKB) { - mRebufferingWatermarkHighKB = watermarkKB; - return this; - } - - /** - * Sets the time based low and high watermarks in milliseconds for rebuffering. - * @param lowWatermarkMs time based low watermark in milliseconds - * @param highWatermarkMs time based high watermark in milliseconds - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarksMs(int lowWatermarkMs, int highWatermarkMs) { - mRebufferingWatermarkLowMs = lowWatermarkMs; - mRebufferingWatermarkHighMs = highWatermarkMs; - return this; - } - - /** - * Sets the size based low and high watermarks in kilobytes for rebuffering. - * @param lowWatermarkKB size based low watermark in kilobytes - * @param highWatermarkKB size based high watermark in kilobytes - * @return the same Builder instance. - */ - public Builder setRebufferingWatermarksKB(int lowWatermarkKB, int highWatermarkKB) { - mRebufferingWatermarkLowKB = lowWatermarkKB; - mRebufferingWatermarkHighKB = highWatermarkKB; + public Builder setResumePlaybackMarkMs(int markMs) { + mResumePlaybackMarkMs = markMs; return this; } } private BufferingParams(Parcel in) { - mInitialBufferingMode = in.readInt(); - mRebufferingMode = in.readInt(); - - mInitialWatermarkMs = in.readInt(); - mInitialWatermarkKB = in.readInt(); - - mRebufferingWatermarkLowMs = in.readInt(); - mRebufferingWatermarkHighMs = in.readInt(); - mRebufferingWatermarkLowKB = in.readInt(); - mRebufferingWatermarkHighKB = in.readInt(); + mInitialMarkMs = in.readInt(); + mResumePlaybackMarkMs = in.readInt(); } public static final Parcelable.Creator<BufferingParams> CREATOR = @@ -446,15 +186,7 @@ public final class BufferingParams implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mInitialBufferingMode); - dest.writeInt(mRebufferingMode); - - dest.writeInt(mInitialWatermarkMs); - dest.writeInt(mInitialWatermarkKB); - - dest.writeInt(mRebufferingWatermarkLowMs); - dest.writeInt(mRebufferingWatermarkHighMs); - dest.writeInt(mRebufferingWatermarkLowKB); - dest.writeInt(mRebufferingWatermarkHighKB); + dest.writeInt(mInitialMarkMs); + dest.writeInt(mResumePlaybackMarkMs); } } diff --git a/android/media/ExifInterface.java b/android/media/ExifInterface.java index ba41a7bd..91754162 100644 --- a/android/media/ExifInterface.java +++ b/android/media/ExifInterface.java @@ -2564,51 +2564,66 @@ public class ExifInterface { }); } + String hasImage = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE); String hasVideo = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); - final String METADATA_HAS_VIDEO_VALUE_YES = "yes"; - if (METADATA_HAS_VIDEO_VALUE_YES.equals(hasVideo)) { - String width = retriever.extractMetadata( + String width = null; + String height = null; + String rotation = null; + final String METADATA_VALUE_YES = "yes"; + // If the file has both image and video, prefer image info over video info. + // App querying ExifInterface is most likely using the bitmap path which + // picks the image first. + if (METADATA_VALUE_YES.equals(hasImage)) { + width = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH); + height = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT); + rotation = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION); + } else if (METADATA_VALUE_YES.equals(hasVideo)) { + width = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); - String height = retriever.extractMetadata( + height = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + rotation = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + } - if (width != null) { - mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, - ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder)); - } - - if (height != null) { - mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, - ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder)); - } + if (width != null) { + mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, + ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder)); + } - String rotation = retriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); - if (rotation != null) { - int orientation = ExifInterface.ORIENTATION_NORMAL; + if (height != null) { + mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, + ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder)); + } - // all rotation angles in CW - switch (Integer.parseInt(rotation)) { - case 90: - orientation = ExifInterface.ORIENTATION_ROTATE_90; - break; - case 180: - orientation = ExifInterface.ORIENTATION_ROTATE_180; - break; - case 270: - orientation = ExifInterface.ORIENTATION_ROTATE_270; - break; - } + if (rotation != null) { + int orientation = ExifInterface.ORIENTATION_NORMAL; - mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION, - ExifAttribute.createUShort(orientation, mExifByteOrder)); + // all rotation angles in CW + switch (Integer.parseInt(rotation)) { + case 90: + orientation = ExifInterface.ORIENTATION_ROTATE_90; + break; + case 180: + orientation = ExifInterface.ORIENTATION_ROTATE_180; + break; + case 270: + orientation = ExifInterface.ORIENTATION_ROTATE_270; + break; } - if (DEBUG) { - Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation); - } + mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION, + ExifAttribute.createUShort(orientation, mExifByteOrder)); + } + + if (DEBUG) { + Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation); } } finally { retriever.release(); diff --git a/android/media/MediaDrm.java b/android/media/MediaDrm.java index 88b1c5ff..1feea890 100644 --- a/android/media/MediaDrm.java +++ b/android/media/MediaDrm.java @@ -994,7 +994,6 @@ public final class MediaDrm { * {@link #PROPERTY_VENDOR}, {@link #PROPERTY_VERSION}, * {@link #PROPERTY_DESCRIPTION}, {@link #PROPERTY_ALGORITHMS} */ - /* FIXME this throws IllegalStateException for invalid property names */ @NonNull public native String getPropertyString(@NonNull @StringProperty String propertyName); @@ -1002,7 +1001,6 @@ public final class MediaDrm { * Byte array property name: the device unique identifier is established during * device provisioning and provides a means of uniquely identifying each device. */ - /* FIXME this throws IllegalStateException for invalid property names */ public static final String PROPERTY_DEVICE_UNIQUE_ID = "deviceUniqueId"; /** @hide */ diff --git a/android/media/MediaFormat.java b/android/media/MediaFormat.java index ed5f7d84..c475e122 100644 --- a/android/media/MediaFormat.java +++ b/android/media/MediaFormat.java @@ -96,6 +96,19 @@ import java.util.Map; * <tr><td>{@link #KEY_MIME}</td><td>String</td><td>The type of the format.</td></tr> * <tr><td>{@link #KEY_LANGUAGE}</td><td>String</td><td>The language of the content.</td></tr> * </table> + * + * Image formats have the following keys: + * <table> + * <tr><td>{@link #KEY_MIME}</td><td>String</td><td>The type of the format.</td></tr> + * <tr><td>{@link #KEY_WIDTH}</td><td>Integer</td><td></td></tr> + * <tr><td>{@link #KEY_HEIGHT}</td><td>Integer</td><td></td></tr> + * <tr><td>{@link #KEY_COLOR_FORMAT}</td><td>Integer</td><td>set by the user + * for encoders, readable in the output format of decoders</b></td></tr> + * <tr><td>{@link #KEY_GRID_WIDTH}</td><td>Integer</td><td>required if the image has grid</td></tr> + * <tr><td>{@link #KEY_GRID_HEIGHT}</td><td>Integer</td><td>required if the image has grid</td></tr> + * <tr><td>{@link #KEY_GRID_ROWS}</td><td>Integer</td><td>required if the image has grid</td></tr> + * <tr><td>{@link #KEY_GRID_COLS}</td><td>Integer</td><td>required if the image has grid</td></tr> + * </table> */ public final class MediaFormat { public static final String MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8"; @@ -126,6 +139,35 @@ public final class MediaFormat { public static final String MIMETYPE_AUDIO_SCRAMBLED = "audio/scrambled"; /** + * MIME type for HEIF still image data encoded in HEVC. + * + * To decode such an image, {@link MediaCodec} decoder for + * {@ #MIMETYPE_VIDEO_HEVC} shall be used. The client needs to form + * the correct {@link #MediaFormat} based on additional information in + * the track format, and send it to {@link MediaCodec#configure}. + * + * The track's MediaFormat will come with {@link #KEY_WIDTH} and + * {@link #KEY_HEIGHT} keys, which describes the width and height + * of the image. If the image doesn't contain grid (i.e. none of + * {@link #KEY_GRID_WIDTH}, {@link #KEY_GRID_HEIGHT}, + * {@link #KEY_GRID_ROWS}, {@link #KEY_GRID_COLS} are present}), the + * track will contain a single sample of coded data for the entire image, + * and the image width and height should be used to set up the decoder. + * + * If the image does come with grid, each sample from the track will + * contain one tile in the grid, of which the size is described by + * {@link #KEY_GRID_WIDTH} and {@link #KEY_GRID_HEIGHT}. This size + * (instead of {@link #KEY_WIDTH} and {@link #KEY_HEIGHT}) should be + * used to set up the decoder. The track contains {@link #KEY_GRID_ROWS} + * by {@link #KEY_GRID_COLS} samples in row-major, top-row first, + * left-to-right order. The output image should be reconstructed by + * first tiling the decoding results of the tiles in the correct order, + * then trimming (before rotation is applied) on the bottom and right + * side, if the tiled area is larger than the image width and height. + */ + public static final String MIMETYPE_IMAGE_ANDROID_HEIC = "image/vnd.android.heic"; + + /** * MIME type for WebVTT subtitle data. */ public static final String MIMETYPE_TEXT_VTT = "text/vtt"; @@ -232,6 +274,54 @@ public final class MediaFormat { public static final String KEY_FRAME_RATE = "frame-rate"; /** + * A key describing the grid width of the content in a {@link #MIMETYPE_IMAGE_ANDROID_HEIC} + * track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_GRID_HEIGHT + * @see #KEY_GRID_ROWS + * @see #KEY_GRID_COLS + */ + public static final String KEY_GRID_WIDTH = "grid-width"; + + /** + * A key describing the grid height of the content in a {@link #MIMETYPE_IMAGE_ANDROID_HEIC} + * track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_GRID_WIDTH + * @see #KEY_GRID_ROWS + * @see #KEY_GRID_COLS + */ + public static final String KEY_GRID_HEIGHT = "grid-height"; + + /** + * A key describing the number of grid rows in the content in a + * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_GRID_WIDTH + * @see #KEY_GRID_HEIGHT + * @see #KEY_GRID_COLS + */ + public static final String KEY_GRID_ROWS = "grid-rows"; + + /** + * A key describing the number of grid columns in the content in a + * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer. + * + * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks. + * + * @see #KEY_GRID_WIDTH + * @see #KEY_GRID_HEIGHT + * @see #KEY_GRID_ROWS + */ + public static final String KEY_GRID_COLS = "grid-cols"; + + /** * A key describing the raw audio sample encoding/format. * * <p>The associated value is an integer, using one of the diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java index 760cc49b..0b864018 100644 --- a/android/media/MediaMetadataRetriever.java +++ b/android/media/MediaMetadataRetriever.java @@ -47,7 +47,7 @@ public class MediaMetadataRetriever // The field below is accessed by native methods @SuppressWarnings("unused") private long mNativeContext; - + private static final int EMBEDDED_PICTURE_TYPE_ANY = 0xFFFF; public MediaMetadataRetriever() { @@ -58,7 +58,7 @@ public class MediaMetadataRetriever * Sets the data source (file pathname) to use. Call this * method before the rest of the methods in this class. This method may be * time-consuming. - * + * * @param path The path of the input media file. * @throws IllegalArgumentException If the path is invalid. */ @@ -113,7 +113,7 @@ public class MediaMetadataRetriever * responsibility to close the file descriptor. It is safe to do so as soon * as this call returns. Call this method before the rest of the methods in * this class. This method may be time-consuming. - * + * * @param fd the FileDescriptor for the file you want to play * @param offset the offset into the file where the data to be played starts, * in bytes. It must be non-negative @@ -123,13 +123,13 @@ public class MediaMetadataRetriever */ public native void setDataSource(FileDescriptor fd, long offset, long length) throws IllegalArgumentException; - + /** * Sets the data source (FileDescriptor) to use. It is the caller's * responsibility to close the file descriptor. It is safe to do so as soon * as this call returns. Call this method before the rest of the methods in * this class. This method may be time-consuming. - * + * * @param fd the FileDescriptor for the file you want to play * @throws IllegalArgumentException if the FileDescriptor is invalid */ @@ -138,11 +138,11 @@ public class MediaMetadataRetriever // intentionally less than LONG_MAX setDataSource(fd, 0, 0x7ffffffffffffffL); } - + /** - * Sets the data source as a content Uri. Call this method before + * Sets the data source as a content Uri. Call this method before * the rest of the methods in this class. This method may be time-consuming. - * + * * @param context the Context to use when resolving the Uri * @param uri the Content URI of the data you want to play * @throws IllegalArgumentException if the Uri is invalid @@ -154,7 +154,7 @@ public class MediaMetadataRetriever if (uri == null) { throw new IllegalArgumentException(); } - + String scheme = uri.getScheme(); if(scheme == null || scheme.equals("file")) { setDataSource(uri.getPath()); @@ -213,12 +213,12 @@ public class MediaMetadataRetriever /** * Call this method after setDataSource(). This method retrieves the * meta data value associated with the keyCode. - * + * * The keyCode currently supported is listed below as METADATA_XXX * constants. With any other value, it returns a null pointer. - * + * * @param keyCode One of the constants listed below at the end of the class. - * @return The meta data value associate with the given keyCode on success; + * @return The meta data value associate with the given keyCode on success; * null on failure. */ public native String extractMetadata(int keyCode); @@ -357,6 +357,109 @@ public class MediaMetadataRetriever private native Bitmap _getFrameAtTime(long timeUs, int option, int width, int height); /** + * This method retrieves a video frame by its index. It should only be called + * after {@link #setDataSource}. + * + * @param frameIndex 0-based index of the video frame. The frame index must be that of + * a valid frame. The total number of frames available for retrieval can be queried + * via the {@link #METADATA_KEY_VIDEO_FRAME_COUNT} key. + * + * @throws IllegalStateException if the container doesn't contain video or image sequences. + * @throws IllegalArgumentException if the requested frame index does not exist. + * + * @return A Bitmap containing the requested video frame, or null if the retrieval fails. + * + * @see #getFramesAtIndex(int, int) + */ + public Bitmap getFrameAtIndex(int frameIndex) { + Bitmap[] bitmaps = getFramesAtIndex(frameIndex, 1); + if (bitmaps == null || bitmaps.length < 1) { + return null; + } + return bitmaps[0]; + } + + /** + * This method retrieves a consecutive set of video frames starting at the + * specified index. It should only be called after {@link #setDataSource}. + * + * If the caller intends to retrieve more than one consecutive video frames, + * this method is preferred over {@link #getFrameAtIndex(int)} for efficiency. + * + * @param frameIndex 0-based index of the first video frame to retrieve. The frame index + * must be that of a valid frame. The total number of frames available for retrieval + * can be queried via the {@link #METADATA_KEY_VIDEO_FRAME_COUNT} key. + * @param numFrames number of consecutive video frames to retrieve. Must be a positive + * value. The stream must contain at least numFrames frames starting at frameIndex. + * + * @throws IllegalStateException if the container doesn't contain video or image sequences. + * @throws IllegalArgumentException if the frameIndex or numFrames is invalid, or the + * stream doesn't contain at least numFrames starting at frameIndex. + + * @return An array of Bitmaps containing the requested video frames. The returned + * array could contain less frames than requested if the retrieval fails. + * + * @see #getFrameAtIndex(int) + */ + public Bitmap[] getFramesAtIndex(int frameIndex, int numFrames) { + if (!"yes".equals(extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO))) { + throw new IllegalStateException("Does not contail video or image sequences"); + } + int frameCount = Integer.parseInt( + extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT)); + if (frameIndex < 0 || numFrames < 1 + || frameIndex >= frameCount + || frameIndex > frameCount - numFrames) { + throw new IllegalArgumentException("Invalid frameIndex or numFrames: " + + frameIndex + ", " + numFrames); + } + return _getFrameAtIndex(frameIndex, numFrames); + } + private native Bitmap[] _getFrameAtIndex(int frameIndex, int numFrames); + + /** + * This method retrieves a still image by its index. It should only be called + * after {@link #setDataSource}. + * + * @param imageIndex 0-based index of the image, with negative value indicating + * the primary image. + * @throws IllegalStateException if the container doesn't contain still images. + * @throws IllegalArgumentException if the requested image does not exist. + * + * @return the requested still image, or null if the image cannot be retrieved. + * + * @see #getPrimaryImage + */ + public Bitmap getImageAtIndex(int imageIndex) { + if (!"yes".equals(extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE))) { + throw new IllegalStateException("Does not contail still images"); + } + + String imageCount = extractMetadata(MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT); + if (imageIndex >= Integer.parseInt(imageCount)) { + throw new IllegalArgumentException("Invalid image index: " + imageCount); + } + + return _getImageAtIndex(imageIndex); + } + + /** + * This method retrieves the primary image of the media content. It should only + * be called after {@link #setDataSource}. + * + * @return the primary image, or null if it cannot be retrieved. + * + * @throws IllegalStateException if the container doesn't contain still images. + * + * @see #getImageAtIndex(int) + */ + public Bitmap getPrimaryImage() { + return getImageAtIndex(-1); + } + + private native Bitmap _getImageAtIndex(int imageIndex); + + /** * Call this method after setDataSource(). This method finds the optional * graphic or album/cover art associated associated with the data source. If * there are more than one pictures, (any) one of them is returned. @@ -572,5 +675,40 @@ public class MediaMetadataRetriever * number. */ public static final int METADATA_KEY_CAPTURE_FRAMERATE = 25; + /** + * If this key exists the media contains still image content. + */ + public static final int METADATA_KEY_HAS_IMAGE = 26; + /** + * If the media contains still images, this key retrieves the number + * of still images. + */ + public static final int METADATA_KEY_IMAGE_COUNT = 27; + /** + * If the media contains still images, this key retrieves the image + * index of the primary image. + */ + public static final int METADATA_KEY_IMAGE_PRIMARY = 28; + /** + * If the media contains still images, this key retrieves the width + * of the primary image. + */ + public static final int METADATA_KEY_IMAGE_WIDTH = 29; + /** + * If the media contains still images, this key retrieves the height + * of the primary image. + */ + public static final int METADATA_KEY_IMAGE_HEIGHT = 30; + /** + * If the media contains still images, this key retrieves the rotation + * of the primary image. + */ + public static final int METADATA_KEY_IMAGE_ROTATION = 31; + /** + * If the media contains video and this key exists, it retrieves the + * total number of frames in the video sequence. + */ + public static final int METADATA_KEY_VIDEO_FRAME_COUNT = 32; + // Add more here... } diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java index 62757e2e..649c091b 100644 --- a/android/media/MediaPlayer.java +++ b/android/media/MediaPlayer.java @@ -43,6 +43,7 @@ import android.system.Os; import android.system.OsConstants; import android.util.Log; import android.util.Pair; +import android.util.ArrayMap; import android.view.Surface; import android.view.SurfaceHolder; import android.widget.VideoView; @@ -58,6 +59,7 @@ import android.media.SubtitleData; import android.media.SubtitleTrack.RenderingWidget; import android.media.SyncParams; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import libcore.io.IoBridge; @@ -577,6 +579,7 @@ import java.util.Vector; public class MediaPlayer extends PlayerBase implements SubtitleController.Listener , VolumeAutomation + , AudioRouting { /** Constant to retrieve only the new metadata since the last @@ -1417,6 +1420,155 @@ public class MediaPlayer extends PlayerBase private native @Nullable VolumeShaper.State native_getVolumeShaperState(int id); + //-------------------------------------------------------------------------- + // Explicit Routing + //-------------------- + private AudioDeviceInfo mPreferredDevice = null; + + /** + * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route + * the output from this MediaPlayer. + * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source. + * If deviceInfo is null, default routing is restored. + * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and + * does not correspond to a valid audio device. + */ + @Override + public boolean setPreferredDevice(AudioDeviceInfo deviceInfo) { + if (deviceInfo != null && !deviceInfo.isSink()) { + return false; + } + int preferredDeviceId = deviceInfo != null ? deviceInfo.getId() : 0; + boolean status = native_setOutputDevice(preferredDeviceId); + if (status == true) { + synchronized (this) { + mPreferredDevice = deviceInfo; + } + } + return status; + } + + /** + * Returns the selected output specified by {@link #setPreferredDevice}. Note that this + * is not guaranteed to correspond to the actual device being used for playback. + */ + @Override + public AudioDeviceInfo getPreferredDevice() { + synchronized (this) { + return mPreferredDevice; + } + } + + /** + * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer + * Note: The query is only valid if the MediaPlayer is currently playing. + * If the player is not playing, the returned device can be null or correspond to previously + * selected device when the player was last active. + */ + @Override + public AudioDeviceInfo getRoutedDevice() { + int deviceId = native_getRoutedDeviceId(); + if (deviceId == 0) { + return null; + } + AudioDeviceInfo[] devices = + AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS); + for (int i = 0; i < devices.length; i++) { + if (devices[i].getId() == deviceId) { + return devices[i]; + } + } + return null; + } + + /* + * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler. + */ + private void enableNativeRoutingCallbacksLocked(boolean enabled) { + if (mRoutingChangeListeners.size() == 0) { + native_enableDeviceCallback(enabled); + } + } + + /** + * The list of AudioRouting.OnRoutingChangedListener interfaces added (with + * {@link #addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, Handler)} + * by an app to receive (re)routing notifications. + */ + @GuardedBy("mRoutingChangeListeners") + private ArrayMap<AudioRouting.OnRoutingChangedListener, + NativeRoutingEventHandlerDelegate> mRoutingChangeListeners = new ArrayMap<>(); + + /** + * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing + * changes on this MediaPlayer. + * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive + * notifications of rerouting events. + * @param handler Specifies the {@link Handler} object for the thread on which to execute + * the callback. If <code>null</code>, the handler on the main looper will be used. + */ + @Override + public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener, + Handler handler) { + synchronized (mRoutingChangeListeners) { + if (listener != null && !mRoutingChangeListeners.containsKey(listener)) { + enableNativeRoutingCallbacksLocked(true); + mRoutingChangeListeners.put( + listener, new NativeRoutingEventHandlerDelegate(this, listener, handler)); + } + } + } + + /** + * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added + * to receive rerouting notifications. + * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface + * to remove. + */ + @Override + public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener) { + synchronized (mRoutingChangeListeners) { + if (mRoutingChangeListeners.containsKey(listener)) { + mRoutingChangeListeners.remove(listener); + enableNativeRoutingCallbacksLocked(false); + } + } + } + + /** + * Helper class to handle the forwarding of native events to the appropriate listener + * (potentially) handled in a different thread + */ + private class NativeRoutingEventHandlerDelegate { + private MediaPlayer mMediaPlayer; + private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener; + private Handler mHandler; + + NativeRoutingEventHandlerDelegate(final MediaPlayer mediaPlayer, + final AudioRouting.OnRoutingChangedListener listener, Handler handler) { + mMediaPlayer = mediaPlayer; + mOnRoutingChangedListener = listener; + mHandler = handler != null ? handler : mEventHandler; + } + + void notifyClient() { + if (mHandler != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (mOnRoutingChangedListener != null) { + mOnRoutingChangedListener.onRoutingChanged(mMediaPlayer); + } + } + }); + } + } + } + + private native final boolean native_setOutputDevice(int deviceId); + private native final int native_getRoutedDeviceId(); + private native final void native_enableDeviceCallback(boolean enabled); + /** * Set the low-level power management behavior for this MediaPlayer. This * can be used when the MediaPlayer is not playing through a SurfaceHolder @@ -1546,21 +1698,9 @@ public class MediaPlayer extends PlayerBase public native boolean isPlaying(); /** - * Gets the default buffering management params. - * Calling it only after {@code setDataSource} has been called. - * Each type of data source might have different set of default params. - * - * @return the default buffering management params supported by the source component. - * @throws IllegalStateException if the internal player engine has not been - * initialized, or {@code setDataSource} has not been called. - * @hide - */ - @NonNull - public native BufferingParams getDefaultBufferingParams(); - - /** * Gets the current buffering management params used by the source component. * Calling it only after {@code setDataSource} has been called. + * Each type of data source might have different set of default params. * * @return the current buffering management params used by the source component. * @throws IllegalStateException if the internal player engine has not been @@ -1575,8 +1715,7 @@ public class MediaPlayer extends PlayerBase * The object sets its internal BufferingParams to the input, except that the input is * invalid or not supported. * Call it only after {@code setDataSource} has been called. - * Users should only use supported mode returned by {@link #getDefaultBufferingParams()} - * or its downsized version as described in {@link BufferingParams}. + * The input is a hint to MediaPlayer. * * @param params the buffering management params. * @@ -3176,6 +3315,7 @@ public class MediaPlayer extends PlayerBase private static final int MEDIA_SUBTITLE_DATA = 201; private static final int MEDIA_META_DATA = 202; private static final int MEDIA_DRM_INFO = 210; + private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000; private TimeProvider mTimeProvider; @@ -3414,6 +3554,16 @@ public class MediaPlayer extends PlayerBase case MEDIA_NOP: // interface test message - ignore break; + case MEDIA_AUDIO_ROUTING_CHANGED: + AudioManager.resetAudioPortGeneration(); + synchronized (mRoutingChangeListeners) { + for (NativeRoutingEventHandlerDelegate delegate + : mRoutingChangeListeners.values()) { + delegate.notifyClient(); + } + } + return; + default: Log.e(TAG, "Unknown message type " + msg.what); return; diff --git a/android/net/ConnectivityManager.java b/android/net/ConnectivityManager.java index d7ecc81f..8071e8b8 100644 --- a/android/net/ConnectivityManager.java +++ b/android/net/ConnectivityManager.java @@ -619,6 +619,35 @@ public class ConnectivityManager { */ public static final int NETID_UNSET = 0; + /** + * Private DNS Mode values. + * + * The "private_dns_mode" global setting stores a String value which is + * expected to be one of the following. + */ + + /** + * @hide + */ + public static final String PRIVATE_DNS_MODE_OFF = "off"; + /** + * @hide + */ + public static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic"; + /** + * @hide + */ + public static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname"; + /** + * The default Private DNS mode. + * + * This may change from release to release or may become dependent upon + * the capabilities of the underlying platform. + * + * @hide + */ + public static final String PRIVATE_DNS_DEFAULT_MODE = PRIVATE_DNS_MODE_OPPORTUNISTIC; + private final IConnectivityManager mService; /** * A kludge to facilitate static access where a Context pointer isn't available, like in the diff --git a/android/net/ConnectivityMetricsEvent.java b/android/net/ConnectivityMetricsEvent.java index 46bb3467..394ac428 100644 --- a/android/net/ConnectivityMetricsEvent.java +++ b/android/net/ConnectivityMetricsEvent.java @@ -18,6 +18,7 @@ package android.net; import android.os.Parcel; import android.os.Parcelable; + import com.android.internal.util.BitUtils; /** @@ -80,7 +81,7 @@ public final class ConnectivityMetricsEvent implements Parcelable { StringBuilder buffer = new StringBuilder("ConnectivityMetricsEvent("); buffer.append(String.format("%tT.%tL", timestamp, timestamp)); if (netId != 0) { - buffer.append(", ").append(netId); + buffer.append(", ").append("netId=").append(netId); } if (ifname != null) { buffer.append(", ").append(ifname); diff --git a/android/net/IpSecAlgorithm.java b/android/net/IpSecAlgorithm.java index 16b14523..64f8f39e 100644 --- a/android/net/IpSecAlgorithm.java +++ b/android/net/IpSecAlgorithm.java @@ -78,7 +78,11 @@ public final class IpSecAlgorithm implements Parcelable { /** * AES-GCM Authentication/Integrity + Encryption/Ciphering Algorithm. * - * <p>Valid lengths for this key are {128, 192, 256}. + * <p>Valid lengths for keying material are {160, 224, 288}. + * + * <p>As per RFC4106 (Section 8.1), keying material consists of a 128, 192, or 256 bit AES key + * followed by a 32-bit salt. RFC compliance requires that the salt must be unique per + * invocation with the same key. * * <p>Valid ICV (truncation) lengths are {64, 96, 128}. */ diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java index d7b32561..eccd5f47 100644 --- a/android/net/IpSecManager.java +++ b/android/net/IpSecManager.java @@ -136,7 +136,7 @@ public final class IpSecManager { } @Override - protected void finalize() { + protected void finalize() throws Throwable { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } diff --git a/android/net/LocalSocketImpl.java b/android/net/LocalSocketImpl.java index 05c8afb3..6e4a231b 100644 --- a/android/net/LocalSocketImpl.java +++ b/android/net/LocalSocketImpl.java @@ -16,18 +16,18 @@ package android.net; -import java.io.IOException; -import java.io.OutputStream; -import java.io.InputStream; -import java.io.FileDescriptor; -import java.net.SocketOptions; - import android.system.ErrnoException; +import android.system.Int32Ref; import android.system.Os; import android.system.OsConstants; import android.system.StructLinger; import android.system.StructTimeval; -import android.util.MutableInt; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.SocketOptions; /** * Socket implementation used for android.net.LocalSocket and @@ -62,7 +62,7 @@ class LocalSocketImpl FileDescriptor myFd = fd; if (myFd == null) throw new IOException("socket closed"); - MutableInt avail = new MutableInt(0); + Int32Ref avail = new Int32Ref(0); try { Os.ioctlInt(myFd, OsConstants.FIONREAD, avail); } catch (ErrnoException e) { @@ -167,7 +167,7 @@ class LocalSocketImpl if (myFd == null) throw new IOException("socket closed"); // Loop until the output buffer is empty. - MutableInt pending = new MutableInt(0); + Int32Ref pending = new Int32Ref(0); while (true) { try { // See linux/net/unix/af_unix.c diff --git a/android/net/MacAddress.java b/android/net/MacAddress.java new file mode 100644 index 00000000..f6a69bac --- /dev/null +++ b/android/net/MacAddress.java @@ -0,0 +1,274 @@ +/* + * Copyright 2017 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 android.net; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.BitUtils; + +import java.util.Arrays; +import java.util.Random; +import java.util.StringJoiner; + +/** + * Represents a mac address. + * + * @hide + */ +public final class MacAddress implements Parcelable { + + private static final int ETHER_ADDR_LEN = 6; + private static final byte[] ETHER_ADDR_BROADCAST = addr(0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + + /** The broadcast mac address. */ + public static final MacAddress BROADCAST_ADDRESS = new MacAddress(ETHER_ADDR_BROADCAST); + + /** The zero mac address. */ + public static final MacAddress ALL_ZEROS_ADDRESS = new MacAddress(0); + + /** Represents categories of mac addresses. */ + public enum MacAddressType { + UNICAST, + MULTICAST, + BROADCAST; + } + + private static final long VALID_LONG_MASK = BROADCAST_ADDRESS.mAddr; + private static final long LOCALLY_ASSIGNED_MASK = new MacAddress("2:0:0:0:0:0").mAddr; + private static final long MULTICAST_MASK = new MacAddress("1:0:0:0:0:0").mAddr; + private static final long OUI_MASK = new MacAddress("ff:ff:ff:0:0:0").mAddr; + private static final long NIC_MASK = new MacAddress("0:0:0:ff:ff:ff").mAddr; + private static final MacAddress BASE_ANDROID_MAC = new MacAddress("da:a1:19:0:0:0"); + + // Internal representation of the mac address as a single 8 byte long. + // The encoding scheme sets the two most significant bytes to 0. The 6 bytes of the + // mac address are encoded in the 6 least significant bytes of the long, where the first + // byte of the array is mapped to the 3rd highest logical byte of the long, the second + // byte of the array is mapped to the 4th highest logical byte of the long, and so on. + private final long mAddr; + + private MacAddress(long addr) { + mAddr = addr; + } + + /** Creates a MacAddress for the given byte representation. */ + public MacAddress(byte[] addr) { + this(longAddrFromByteAddr(addr)); + } + + /** Creates a MacAddress for the given string representation. */ + public MacAddress(String addr) { + this(longAddrFromByteAddr(byteAddrFromStringAddr(addr))); + } + + /** Returns the MacAddressType of this MacAddress. */ + public MacAddressType addressType() { + if (equals(BROADCAST_ADDRESS)) { + return MacAddressType.BROADCAST; + } + if (isMulticastAddress()) { + return MacAddressType.MULTICAST; + } + return MacAddressType.UNICAST; + } + + /** Returns true if this MacAddress corresponds to a multicast address. */ + public boolean isMulticastAddress() { + return (mAddr & MULTICAST_MASK) != 0; + } + + /** Returns true if this MacAddress corresponds to a locally assigned address. */ + public boolean isLocallyAssigned() { + return (mAddr & LOCALLY_ASSIGNED_MASK) != 0; + } + + /** Returns a byte array representation of this MacAddress. */ + public byte[] toByteArray() { + return byteAddrFromLongAddr(mAddr); + } + + @Override + public String toString() { + return stringAddrFromByteAddr(byteAddrFromLongAddr(mAddr)); + } + + @Override + public int hashCode() { + return (int) ((mAddr >> 32) ^ mAddr); + } + + @Override + public boolean equals(Object o) { + return (o instanceof MacAddress) && ((MacAddress) o).mAddr == mAddr; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mAddr); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<MacAddress> CREATOR = + new Parcelable.Creator<MacAddress>() { + public MacAddress createFromParcel(Parcel in) { + return new MacAddress(in.readLong()); + } + + public MacAddress[] newArray(int size) { + return new MacAddress[size]; + } + }; + + /** Return true if the given byte array is not null and has the length of a mac address. */ + public static boolean isMacAddress(byte[] addr) { + return addr != null && addr.length == ETHER_ADDR_LEN; + } + + /** + * Return the MacAddressType of the mac address represented by the given byte array, + * or null if the given byte array does not represent an mac address. + */ + public static MacAddressType macAddressType(byte[] addr) { + if (!isMacAddress(addr)) { + return null; + } + return new MacAddress(addr).addressType(); + } + + /** DOCME */ + public static byte[] byteAddrFromStringAddr(String addr) { + if (addr == null) { + throw new IllegalArgumentException("cannot convert the null String"); + } + String[] parts = addr.split(":"); + if (parts.length != ETHER_ADDR_LEN) { + throw new IllegalArgumentException(addr + " was not a valid MAC address"); + } + byte[] bytes = new byte[ETHER_ADDR_LEN]; + for (int i = 0; i < ETHER_ADDR_LEN; i++) { + int x = Integer.valueOf(parts[i], 16); + if (x < 0 || 0xff < x) { + throw new IllegalArgumentException(addr + "was not a valid MAC address"); + } + bytes[i] = (byte) x; + } + return bytes; + } + + /** DOCME */ + public static String stringAddrFromByteAddr(byte[] addr) { + if (!isMacAddress(addr)) { + return null; + } + StringJoiner j = new StringJoiner(":"); + for (byte b : addr) { + j.add(Integer.toHexString(BitUtils.uint8(b))); + } + return j.toString(); + } + + /** @hide */ + public static byte[] byteAddrFromLongAddr(long addr) { + byte[] bytes = new byte[ETHER_ADDR_LEN]; + int index = ETHER_ADDR_LEN; + while (index-- > 0) { + bytes[index] = (byte) addr; + addr = addr >> 8; + } + return bytes; + } + + /** @hide */ + public static long longAddrFromByteAddr(byte[] addr) { + if (!isMacAddress(addr)) { + throw new IllegalArgumentException( + Arrays.toString(addr) + " was not a valid MAC address"); + } + long longAddr = 0; + for (byte b : addr) { + longAddr = (longAddr << 8) + BitUtils.uint8(b); + } + return longAddr; + } + + /** @hide */ + public static long longAddrFromStringAddr(String addr) { + if (addr == null) { + throw new IllegalArgumentException("cannot convert the null String"); + } + String[] parts = addr.split(":"); + if (parts.length != ETHER_ADDR_LEN) { + throw new IllegalArgumentException(addr + " was not a valid MAC address"); + } + long longAddr = 0; + int index = ETHER_ADDR_LEN; + while (index-- > 0) { + int x = Integer.valueOf(parts[index], 16); + if (x < 0 || 0xff < x) { + throw new IllegalArgumentException(addr + "was not a valid MAC address"); + } + longAddr = x + (longAddr << 8); + } + return longAddr; + } + + /** @hide */ + public static String stringAddrFromLongAddr(long addr) { + addr = Long.reverseBytes(addr) >> 16; + StringJoiner j = new StringJoiner(":"); + for (int i = 0; i < ETHER_ADDR_LEN; i++) { + j.add(Integer.toHexString((byte) addr)); + addr = addr >> 8; + } + return j.toString(); + } + + /** + * Returns a randomely generated mac address with the Android OUI value "DA-A1-19". + * The locally assigned bit is always set to 1. + */ + public static MacAddress getRandomAddress() { + return getRandomAddress(BASE_ANDROID_MAC, new Random()); + } + + /** + * Returns a randomely generated mac address using the given Random object and the same + * OUI values as the given MacAddress. The locally assigned bit is always set to 1. + */ + public static MacAddress getRandomAddress(MacAddress base, Random r) { + long longAddr = (base.mAddr & OUI_MASK) | (NIC_MASK & r.nextLong()) | LOCALLY_ASSIGNED_MASK; + return new MacAddress(longAddr); + } + + // Convenience function for working around the lack of byte literals. + private static byte[] addr(int... in) { + if (in.length != ETHER_ADDR_LEN) { + throw new IllegalArgumentException(Arrays.toString(in) + + " was not an array with length equal to " + ETHER_ADDR_LEN); + } + byte[] out = new byte[ETHER_ADDR_LEN]; + for (int i = 0; i < ETHER_ADDR_LEN; i++) { + out[i] = (byte) in[i]; + } + return out; + } +} diff --git a/android/net/Network.java b/android/net/Network.java index 3c868c39..903b602b 100644 --- a/android/net/Network.java +++ b/android/net/Network.java @@ -16,14 +16,14 @@ package android.net; -import android.os.Parcelable; import android.os.Parcel; +import android.os.Parcelable; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; -import libcore.net.http.Dns; -import libcore.net.http.HttpURLConnectionFactory; +import com.android.okhttp.internalandroidapi.Dns; +import com.android.okhttp.internalandroidapi.HttpURLConnectionFactory; import java.io.FileDescriptor; import java.io.IOException; @@ -34,11 +34,12 @@ import java.net.MalformedURLException; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; -import java.net.UnknownHostException; import java.net.URL; import java.net.URLConnection; +import java.net.UnknownHostException; import java.util.Arrays; import java.util.concurrent.TimeUnit; + import javax.net.SocketFactory; /** diff --git a/android/net/NetworkCapabilities.java b/android/net/NetworkCapabilities.java index db12dd97..ee75fd44 100644 --- a/android/net/NetworkCapabilities.java +++ b/android/net/NetworkCapabilities.java @@ -17,6 +17,7 @@ package android.net; import android.annotation.IntDef; +import android.net.ConnectivityManager.NetworkCallback; import android.os.Parcel; import android.os.Parcelable; @@ -30,15 +31,24 @@ import java.util.Objects; import java.util.StringJoiner; /** - * This class represents the capabilities of a network. This is used both to specify - * needs to {@link ConnectivityManager} and when inspecting a network. - * - * Note that this replaces the old {@link ConnectivityManager#TYPE_MOBILE} method - * of network selection. Rather than indicate a need for Wi-Fi because an application - * needs high bandwidth and risk obsolescence when a new, fast network appears (like LTE), - * the application should specify it needs high bandwidth. Similarly if an application - * needs an unmetered network for a bulk transfer it can specify that rather than assuming - * all cellular based connections are metered and all Wi-Fi based connections are not. + * Representation of the capabilities of a network. This object serves two + * purposes: + * <ul> + * <li>An expression of the current capabilities of an active network, typically + * expressed through + * {@link NetworkCallback#onCapabilitiesChanged(Network, NetworkCapabilities)} + * or {@link ConnectivityManager#getNetworkCapabilities(Network)}. + * <li>An expression of the future capabilities of a desired network, typically + * expressed through {@link NetworkRequest}. + * </ul> + * <p> + * This replaces the old {@link ConnectivityManager#TYPE_MOBILE} method of + * network selection. Rather than indicate a need for Wi-Fi because an + * application needs high bandwidth and risk obsolescence when a new, fast + * network appears (like LTE), the application should specify it needs high + * bandwidth. Similarly if an application needs an unmetered network for a bulk + * transfer it can specify that rather than assuming all cellular based + * connections are metered and all Wi-Fi based connections are not. */ public final class NetworkCapabilities implements Parcelable { private static final String TAG = "NetworkCapabilities"; @@ -101,6 +111,7 @@ public final class NetworkCapabilities implements Parcelable { NET_CAPABILITY_NOT_VPN, NET_CAPABILITY_VALIDATED, NET_CAPABILITY_CAPTIVE_PORTAL, + NET_CAPABILITY_NOT_ROAMING, NET_CAPABILITY_FOREGROUND, }) public @interface NetCapability { } @@ -218,11 +229,16 @@ public final class NetworkCapabilities implements Parcelable { public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17; /** + * Indicates that this network is not roaming. + */ + public static final int NET_CAPABILITY_NOT_ROAMING = 18; + + /** * Indicates that this network is available for use by apps, and not a network that is being * kept up in the background to facilitate fast network switching. * @hide */ - public static final int NET_CAPABILITY_FOREGROUND = 18; + public static final int NET_CAPABILITY_FOREGROUND = 19; private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS; private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_FOREGROUND; @@ -237,6 +253,7 @@ public final class NetworkCapabilities implements Parcelable { (1 << NET_CAPABILITY_TRUSTED) | (1 << NET_CAPABILITY_VALIDATED) | (1 << NET_CAPABILITY_CAPTIVE_PORTAL) | + (1 << NET_CAPABILITY_NOT_ROAMING) | (1 << NET_CAPABILITY_FOREGROUND); /** @@ -316,6 +333,21 @@ public final class NetworkCapabilities implements Parcelable { } /** + * Sets (or clears) the given capability on this {@link NetworkCapabilities} + * instance. + * + * @hide + */ + public NetworkCapabilities setCapability(@NetCapability int capability, boolean value) { + if (value) { + addCapability(capability); + } else { + removeCapability(capability); + } + return this; + } + + /** * Gets all the capabilities set on this {@code NetworkCapability} instance. * * @return an array of capability values for this instance. @@ -326,6 +358,15 @@ public final class NetworkCapabilities implements Parcelable { } /** + * Sets all the capabilities set on this {@code NetworkCapability} instance. + * + * @hide + */ + public void setCapabilities(@NetCapability int[] capabilities) { + mNetworkCapabilities = BitUtils.packBits(capabilities); + } + + /** * Tests for the presence of a capabilitity on this instance. * * @param capability the capabilities to be tested for. @@ -515,6 +556,21 @@ public final class NetworkCapabilities implements Parcelable { } /** + * Sets (or clears) the given transport on this {@link NetworkCapabilities} + * instance. + * + * @hide + */ + public NetworkCapabilities setTransportType(@Transport int transportType, boolean value) { + if (value) { + addTransportType(transportType); + } else { + removeTransportType(transportType); + } + return this; + } + + /** * Gets all the transports set on this {@code NetworkCapability} instance. * * @return an array of transport type values for this instance. @@ -525,6 +581,15 @@ public final class NetworkCapabilities implements Parcelable { } /** + * Sets all the transports set on this {@code NetworkCapability} instance. + * + * @hide + */ + public void setTransportTypes(@Transport int[] transportTypes) { + mTransportTypes = BitUtils.packBits(transportTypes); + } + + /** * Tests for the presence of a transport on this instance. * * @param transportType the transport type to be tested for. @@ -549,12 +614,18 @@ public final class NetworkCapabilities implements Parcelable { } /** + * Value indicating that link bandwidth is unspecified. + * @hide + */ + public static final int LINK_BANDWIDTH_UNSPECIFIED = 0; + + /** * Passive link bandwidth. This is a rough guide of the expected peak bandwidth * for the first hop on the given transport. It is not measured, but may take into account * link parameters (Radio technology, allocated channels, etc). */ - private int mLinkUpBandwidthKbps; - private int mLinkDownBandwidthKbps; + private int mLinkUpBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED; + private int mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED; /** * Sets the upstream bandwidth for this network in Kbps. This always only refers to @@ -571,8 +642,9 @@ public final class NetworkCapabilities implements Parcelable { * @param upKbps the estimated first hop upstream (device to network) bandwidth. * @hide */ - public void setLinkUpstreamBandwidthKbps(int upKbps) { + public NetworkCapabilities setLinkUpstreamBandwidthKbps(int upKbps) { mLinkUpBandwidthKbps = upKbps; + return this; } /** @@ -600,8 +672,9 @@ public final class NetworkCapabilities implements Parcelable { * @param downKbps the estimated first hop downstream (network to device) bandwidth. * @hide */ - public void setLinkDownstreamBandwidthKbps(int downKbps) { + public NetworkCapabilities setLinkDownstreamBandwidthKbps(int downKbps) { mLinkDownBandwidthKbps = downKbps; + return this; } /** @@ -628,6 +701,20 @@ public final class NetworkCapabilities implements Parcelable { return (this.mLinkUpBandwidthKbps == nc.mLinkUpBandwidthKbps && this.mLinkDownBandwidthKbps == nc.mLinkDownBandwidthKbps); } + /** @hide */ + public static int minBandwidth(int a, int b) { + if (a == LINK_BANDWIDTH_UNSPECIFIED) { + return b; + } else if (b == LINK_BANDWIDTH_UNSPECIFIED) { + return a; + } else { + return Math.min(a, b); + } + } + /** @hide */ + public static int maxBandwidth(int a, int b) { + return Math.max(a, b); + } private NetworkSpecifier mNetworkSpecifier = null; @@ -708,8 +795,9 @@ public final class NetworkCapabilities implements Parcelable { * @param signalStrength the bearer-specific signal strength. * @hide */ - public void setSignalStrength(int signalStrength) { + public NetworkCapabilities setSignalStrength(int signalStrength) { mSignalStrength = signalStrength; + return this; } /** @@ -968,6 +1056,7 @@ public final class NetworkCapabilities implements Parcelable { case NET_CAPABILITY_NOT_VPN: return "NOT_VPN"; case NET_CAPABILITY_VALIDATED: return "VALIDATED"; case NET_CAPABILITY_CAPTIVE_PORTAL: return "CAPTIVE_PORTAL"; + case NET_CAPABILITY_NOT_ROAMING: return "NOT_ROAMING"; case NET_CAPABILITY_FOREGROUND: return "FOREGROUND"; default: return Integer.toString(capability); } diff --git a/android/net/NetworkIdentity.java b/android/net/NetworkIdentity.java index acd7b560..d3b35998 100644 --- a/android/net/NetworkIdentity.java +++ b/android/net/NetworkIdentity.java @@ -189,7 +189,8 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> { String subscriberId = null; String networkId = null; - boolean roaming = false; + boolean roaming = !state.networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING); boolean metered = !state.networkCapabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_NOT_METERED); @@ -203,7 +204,6 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> { } subscriberId = state.subscriberId; - roaming = state.networkInfo.isRoaming(); } else if (type == TYPE_WIFI) { if (state.networkId != null) { diff --git a/android/net/NetworkInfo.java b/android/net/NetworkInfo.java index 818aa211..e6ad89a2 100644 --- a/android/net/NetworkInfo.java +++ b/android/net/NetworkInfo.java @@ -305,11 +305,17 @@ public class NetworkInfo implements Parcelable { } /** - * Indicates whether the device is currently roaming on this network. - * When {@code true}, it suggests that use of data on this network - * may incur extra costs. + * Indicates whether the device is currently roaming on this network. When + * {@code true}, it suggests that use of data on this network may incur + * extra costs. + * * @return {@code true} if roaming is in effect, {@code false} otherwise. + * @deprecated Callers should switch to checking + * {@link NetworkCapabilities#NET_CAPABILITY_NOT_ROAMING} + * instead, since that handles more complex situations, such as + * VPNs. */ + @Deprecated public boolean isRoaming() { synchronized (this) { return mIsRoaming; @@ -318,6 +324,7 @@ public class NetworkInfo implements Parcelable { /** {@hide} */ @VisibleForTesting + @Deprecated public void setRoaming(boolean isRoaming) { synchronized (this) { mIsRoaming = isRoaming; diff --git a/android/net/NetworkWatchlistManager.java b/android/net/NetworkWatchlistManager.java new file mode 100644 index 00000000..42e43c8a --- /dev/null +++ b/android/net/NetworkWatchlistManager.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 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 android.net; + +import android.annotation.SystemService; +import android.content.Context; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import com.android.internal.net.INetworkWatchlistManager; +import com.android.internal.util.Preconditions; + +/** + * Class that manage network watchlist in system. + * @hide + */ +@SystemService(Context.NETWORK_WATCHLIST_SERVICE) +public class NetworkWatchlistManager { + + private static final String TAG = "NetworkWatchlistManager"; + private static final String SHARED_MEMORY_TAG = "NETWORK_WATCHLIST_SHARED_MEMORY"; + + private final Context mContext; + private final INetworkWatchlistManager mNetworkWatchlistManager; + + /** + * @hide + */ + public NetworkWatchlistManager(Context context, INetworkWatchlistManager manager) { + mContext = context; + mNetworkWatchlistManager = manager; + } + + /** + * @hide + */ + public NetworkWatchlistManager(Context context) { + mContext = Preconditions.checkNotNull(context, "missing context"); + mNetworkWatchlistManager = (INetworkWatchlistManager) + INetworkWatchlistManager.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_WATCHLIST_SERVICE)); + } + + /** + * Report network watchlist records if necessary. + * + * Watchlist report process will run summarize records into a single report, then the + * report will be processed by differential privacy framework and store it on disk. + * + * @hide + */ + public void reportWatchlistIfNecessary() { + try { + mNetworkWatchlistManager.reportWatchlistIfNecessary(); + } catch (RemoteException e) { + Log.e(TAG, "Cannot report records", e); + e.rethrowFromSystemServer(); + } + } +} diff --git a/android/net/SSLCertificateSocketFactory.java b/android/net/SSLCertificateSocketFactory.java index 0b1569ca..4817813c 100644 --- a/android/net/SSLCertificateSocketFactory.java +++ b/android/net/SSLCertificateSocketFactory.java @@ -63,7 +63,12 @@ import javax.net.ssl.X509TrustManager; * This implementation does check the server's certificate hostname, but only * for createSocket variants that specify a hostname. When using methods that * use {@link InetAddress} or which return an unconnected socket, you MUST - * verify the server's identity yourself to ensure a secure connection.</p> + * verify the server's identity yourself to ensure a secure connection. + * + * Refer to + * <a href="https://developer.android.com/training/articles/security-gms-provider.html"> + * Updating Your Security Provider to Protect Against SSL Exploits</a> + * for further information.</p> * * <p>One way to verify the server's identity is to use * {@link HttpsURLConnection#getDefaultHostnameVerifier()} to get a diff --git a/android/net/Uri.java b/android/net/Uri.java index d5377c71..9edcc0e9 100644 --- a/android/net/Uri.java +++ b/android/net/Uri.java @@ -1066,7 +1066,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { return null; } - int end = authority.indexOf('@'); + int end = authority.lastIndexOf('@'); return end == NOT_FOUND ? null : authority.substring(0, end); } @@ -1090,7 +1090,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { } // Parse out user info and then port. - int userInfoSeparator = authority.indexOf('@'); + int userInfoSeparator = authority.lastIndexOf('@'); int portSeparator = authority.indexOf(':', userInfoSeparator); String encodedHost = portSeparator == NOT_FOUND @@ -1116,7 +1116,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { // Make sure we look for the port separtor *after* the user info // separator. We have URLs with a ':' in the user info. - int userInfoSeparator = authority.indexOf('@'); + int userInfoSeparator = authority.lastIndexOf('@'); int portSeparator = authority.indexOf(':', userInfoSeparator); if (portSeparator == NOT_FOUND) { diff --git a/android/net/apf/ApfFilter.java b/android/net/apf/ApfFilter.java index 5c2b66f6..31a1abb3 100644 --- a/android/net/apf/ApfFilter.java +++ b/android/net/apf/ApfFilter.java @@ -86,6 +86,14 @@ import libcore.io.IoBridge; */ public class ApfFilter { + // Helper class for specifying functional filter parameters. + public static class ApfConfiguration { + public ApfCapabilities apfCapabilities; + public boolean multicastFilter; + public boolean ieee802_3Filter; + public int[] ethTypeBlackList; + } + // Enums describing the outcome of receiving an RA packet. private static enum ProcessRaResult { MATCH, // Received RA matched a known RA @@ -261,17 +269,16 @@ public class ApfFilter { private int mIPv4PrefixLength; @VisibleForTesting - ApfFilter(ApfCapabilities apfCapabilities, NetworkInterface networkInterface, - IpClient.Callback ipClientCallback, boolean multicastFilter, - boolean ieee802_3Filter, int[] ethTypeBlackList, IpConnectivityLog log) { - mApfCapabilities = apfCapabilities; + ApfFilter(ApfConfiguration config, NetworkInterface networkInterface, + IpClient.Callback ipClientCallback, IpConnectivityLog log) { + mApfCapabilities = config.apfCapabilities; mIpClientCallback = ipClientCallback; mNetworkInterface = networkInterface; - mMulticastFilter = multicastFilter; - mDrop802_3Frames = ieee802_3Filter; + mMulticastFilter = config.multicastFilter; + mDrop802_3Frames = config.ieee802_3Filter; // Now fill the black list from the passed array - mEthTypeBlackList = filterEthTypeBlackList(ethTypeBlackList); + mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList); mMetricsLog = log; @@ -1160,9 +1167,10 @@ public class ApfFilter { * Create an {@link ApfFilter} if {@code apfCapabilities} indicates support for packet * filtering using APF programs. */ - public static ApfFilter maybeCreate(ApfCapabilities apfCapabilities, - NetworkInterface networkInterface, IpClient.Callback ipClientCallback, - boolean multicastFilter, boolean ieee802_3Filter, int[] ethTypeBlackList) { + public static ApfFilter maybeCreate(ApfConfiguration config, + NetworkInterface networkInterface, IpClient.Callback ipClientCallback) { + if (config == null) return null; + ApfCapabilities apfCapabilities = config.apfCapabilities; if (apfCapabilities == null || networkInterface == null) return null; if (apfCapabilities.apfVersionSupported == 0) return null; if (apfCapabilities.maximumApfProgramSize < 512) { @@ -1178,8 +1186,7 @@ public class ApfFilter { Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported); return null; } - return new ApfFilter(apfCapabilities, networkInterface, ipClientCallback, - multicastFilter, ieee802_3Filter, ethTypeBlackList, new IpConnectivityLog()); + return new ApfFilter(config, networkInterface, ipClientCallback, new IpConnectivityLog()); } public synchronized void shutdown() { diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java index 2359fab4..70983c86 100644 --- a/android/net/ip/IpClient.java +++ b/android/net/ip/IpClient.java @@ -310,12 +310,12 @@ public class IpClient extends StateMachine { return this; } - public Builder withIPv6AddrGenModeEUI64() { + public Builder withRandomMacAddress() { mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_EUI64; return this; } - public Builder withIPv6AddrGenModeStablePrivacy() { + public Builder withStableMacAddress() { mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY; return this; } @@ -1429,15 +1429,15 @@ public class IpClient extends StateMachine { @Override public void enter() { + ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration(); + apfConfig.apfCapabilities = mConfiguration.mApfCapabilities; + apfConfig.multicastFilter = mMulticastFiltering; // Get the Configuration for ApfFilter from Context - final boolean filter802_3Frames = + apfConfig.ieee802_3Filter = mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames); - - final int[] ethTypeBlackList = mContext.getResources().getIntArray( - R.array.config_apfEthTypeBlackList); - - mApfFilter = ApfFilter.maybeCreate(mConfiguration.mApfCapabilities, mNetworkInterface, - mCallback, mMulticastFiltering, filter802_3Frames, ethTypeBlackList); + apfConfig.ethTypeBlackList = + mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList); + mApfFilter = ApfFilter.maybeCreate(apfConfig, mNetworkInterface, mCallback); // TODO: investigate the effects of any multicast filtering racing/interfering with the // rest of this IP configuration startup. if (mApfFilter == null) { diff --git a/android/net/ip/IpManager.java b/android/net/ip/IpManager.java index b12cb32c..38981453 100644 --- a/android/net/ip/IpManager.java +++ b/android/net/ip/IpManager.java @@ -88,16 +88,6 @@ public class IpManager extends IpClient { return this; } @Override - public Builder withIPv6AddrGenModeEUI64() { - super.withIPv6AddrGenModeEUI64(); - return this; - } - @Override - public Builder withIPv6AddrGenModeStablePrivacy() { - super.withIPv6AddrGenModeStablePrivacy(); - return this; - } - @Override public Builder withNetwork(Network network) { super.withNetwork(network); return this; diff --git a/android/net/metrics/ConnectStats.java b/android/net/metrics/ConnectStats.java index 2495cab1..b320b755 100644 --- a/android/net/metrics/ConnectStats.java +++ b/android/net/metrics/ConnectStats.java @@ -119,7 +119,8 @@ public class ConnectStats { @Override public String toString() { - StringBuilder builder = new StringBuilder("ConnectStats(").append(netId).append(", "); + StringBuilder builder = + new StringBuilder("ConnectStats(").append("netId=").append(netId).append(", "); for (int t : BitUtils.unpackBits(transports)) { builder.append(NetworkCapabilities.transportNameOf(t)).append(", "); } diff --git a/android/net/metrics/DefaultNetworkEvent.java b/android/net/metrics/DefaultNetworkEvent.java index eb61c153..8ff8e4f3 100644 --- a/android/net/metrics/DefaultNetworkEvent.java +++ b/android/net/metrics/DefaultNetworkEvent.java @@ -20,44 +20,72 @@ import static android.net.ConnectivityManager.NETID_UNSET; import android.net.NetworkCapabilities; +import com.android.internal.util.BitUtils; + +import java.util.StringJoiner; + /** * An event recorded by ConnectivityService when there is a change in the default network. * {@hide} */ public class DefaultNetworkEvent { - // The ID of the network that has become the new default or NETID_UNSET if none. + // The creation time in milliseconds of this DefaultNetworkEvent. + public final long creationTimeMs; + // The network ID of the network or NETID_UNSET if none. public int netId = NETID_UNSET; - // The list of transport types of the new default network, for example TRANSPORT_WIFI, as - // defined in NetworkCapabilities.java. - public int[] transportTypes = new int[0]; - // The ID of the network that was the default before or NETID_UNSET if none. - public int prevNetId = NETID_UNSET; - // Whether the previous network had IPv4/IPv6 connectivity. - public boolean prevIPv4; - public boolean prevIPv6; + // The list of transport types, as defined in NetworkCapabilities.java. + public int transports; + // The list of transport types of the last previous default network. + public int previousTransports; + // Whether the network has IPv4/IPv6 connectivity. + public boolean ipv4; + public boolean ipv6; + // The initial network score when this network became the default network. + public int initialScore; + // The initial network score when this network stopped being the default network. + public int finalScore; + // The total duration in milliseconds this network was the default network. + public long durationMs; + // The total duration in milliseconds this network was the default network and was validated. + public long validatedMs; + + public DefaultNetworkEvent(long timeMs) { + creationTimeMs = timeMs; + } + + /** Update the durationMs of this DefaultNetworkEvent for the given current time. */ + public void updateDuration(long timeMs) { + durationMs = timeMs - creationTimeMs; + } @Override public String toString() { - String prevNetwork = String.valueOf(prevNetId); - String newNetwork = String.valueOf(netId); - if (prevNetId != 0) { - prevNetwork += ":" + ipSupport(); + StringJoiner j = new StringJoiner(", ", "DefaultNetworkEvent(", ")"); + j.add("netId=" + netId); + for (int t : BitUtils.unpackBits(transports)) { + j.add(NetworkCapabilities.transportNameOf(t)); + } + j.add("ip=" + ipSupport()); + if (initialScore > 0) { + j.add("initial_score=" + initialScore); } - if (netId != 0) { - newNetwork += ":" + NetworkCapabilities.transportNamesOf(transportTypes); + if (finalScore > 0) { + j.add("final_score=" + finalScore); } - return String.format("DefaultNetworkEvent(%s -> %s)", prevNetwork, newNetwork); + j.add(String.format("duration=%.0fs", durationMs / 1000.0)); + j.add(String.format("validation=%4.1f%%", (validatedMs * 100.0) / durationMs)); + return j.toString(); } private String ipSupport() { - if (prevIPv4 && prevIPv6) { + if (ipv4 && ipv6) { return "IPv4v6"; } - if (prevIPv6) { + if (ipv6) { return "IPv6"; } - if (prevIPv4) { + if (ipv4) { return "IPv4"; } return "NONE"; diff --git a/android/net/metrics/DnsEvent.java b/android/net/metrics/DnsEvent.java index 81b098bb..5aa705b0 100644 --- a/android/net/metrics/DnsEvent.java +++ b/android/net/metrics/DnsEvent.java @@ -85,7 +85,8 @@ final public class DnsEvent { @Override public String toString() { - StringBuilder builder = new StringBuilder("DnsEvent(").append(netId).append(", "); + StringBuilder builder = + new StringBuilder("DnsEvent(").append("netId=").append(netId).append(", "); for (int t : BitUtils.unpackBits(transports)) { builder.append(NetworkCapabilities.transportNameOf(t)).append(", "); } diff --git a/android/net/metrics/NetworkEvent.java b/android/net/metrics/NetworkEvent.java index 4df3bf09..1999e78d 100644 --- a/android/net/metrics/NetworkEvent.java +++ b/android/net/metrics/NetworkEvent.java @@ -60,29 +60,25 @@ public final class NetworkEvent implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface EventType {} - public final int netId; public final @EventType int eventType; public final long durationMs; - public NetworkEvent(int netId, @EventType int eventType, long durationMs) { - this.netId = netId; + public NetworkEvent(@EventType int eventType, long durationMs) { this.eventType = eventType; this.durationMs = durationMs; } - public NetworkEvent(int netId, @EventType int eventType) { - this(netId, eventType, 0); + public NetworkEvent(@EventType int eventType) { + this(eventType, 0); } private NetworkEvent(Parcel in) { - netId = in.readInt(); eventType = in.readInt(); durationMs = in.readLong(); } @Override public void writeToParcel(Parcel out, int flags) { - out.writeInt(netId); out.writeInt(eventType); out.writeLong(durationMs); } @@ -105,8 +101,8 @@ public final class NetworkEvent implements Parcelable { @Override public String toString() { - return String.format("NetworkEvent(%d, %s, %dms)", - netId, Decoder.constants.get(eventType), durationMs); + return String.format("NetworkEvent(%s, %dms)", + Decoder.constants.get(eventType), durationMs); } final static class Decoder { diff --git a/android/net/metrics/WakeupEvent.java b/android/net/metrics/WakeupEvent.java index cbf3fc8c..8f1a5c42 100644 --- a/android/net/metrics/WakeupEvent.java +++ b/android/net/metrics/WakeupEvent.java @@ -16,6 +16,10 @@ package android.net.metrics; +import android.net.MacAddress; + +import java.util.StringJoiner; + /** * An event logged when NFLOG notifies userspace of a wakeup packet for * watched interfaces. @@ -23,12 +27,35 @@ package android.net.metrics; */ public class WakeupEvent { public String iface; - public long timestampMs; public int uid; + public int ethertype; + public byte[] dstHwAddr; + public String srcIp; + public String dstIp; + public int ipNextHeader; + public int srcPort; + public int dstPort; + public long timestampMs; @Override public String toString() { - return String.format("WakeupEvent(%tT.%tL, %s, uid: %d)", - timestampMs, timestampMs, iface, uid); + StringJoiner j = new StringJoiner(", ", "WakeupEvent(", ")"); + j.add(String.format("%tT.%tL", timestampMs, timestampMs)); + j.add(iface); + j.add("uid: " + Integer.toString(uid)); + j.add("eth=0x" + Integer.toHexString(ethertype)); + j.add("dstHw=" + MacAddress.stringAddrFromByteAddr(dstHwAddr)); + if (ipNextHeader > 0) { + j.add("ipNxtHdr=" + ipNextHeader); + j.add("srcIp=" + srcIp); + j.add("dstIp=" + dstIp); + if (srcPort > -1) { + j.add("srcPort=" + srcPort); + } + if (dstPort > -1) { + j.add("dstPort=" + dstPort); + } + } + return j.toString(); } } diff --git a/android/net/metrics/WakeupStats.java b/android/net/metrics/WakeupStats.java index 97e83f96..1ba97771 100644 --- a/android/net/metrics/WakeupStats.java +++ b/android/net/metrics/WakeupStats.java @@ -16,8 +16,12 @@ package android.net.metrics; +import android.net.MacAddress; import android.os.Process; import android.os.SystemClock; +import android.util.SparseIntArray; + +import java.util.StringJoiner; /** * An event logged per interface and that aggregates WakeupEvents for that interface. @@ -38,6 +42,13 @@ public class WakeupStats { public long noUidWakeups = 0; public long durationSec = 0; + public long l2UnicastCount = 0; + public long l2MulticastCount = 0; + public long l2BroadcastCount = 0; + + public final SparseIntArray ethertypes = new SparseIntArray(); + public final SparseIntArray ipNextHeaders = new SparseIntArray(); + public WakeupStats(String iface) { this.iface = iface; } @@ -68,20 +79,56 @@ public class WakeupStats { } break; } + + switch (MacAddress.macAddressType(ev.dstHwAddr)) { + case UNICAST: + l2UnicastCount++; + break; + case MULTICAST: + l2MulticastCount++; + break; + case BROADCAST: + l2BroadcastCount++; + break; + default: + break; + } + + increment(ethertypes, ev.ethertype); + if (ev.ipNextHeader >= 0) { + increment(ipNextHeaders, ev.ipNextHeader); + } } @Override public String toString() { updateDuration(); - return new StringBuilder() - .append("WakeupStats(").append(iface) - .append(", total: ").append(totalWakeups) - .append(", root: ").append(rootWakeups) - .append(", system: ").append(systemWakeups) - .append(", apps: ").append(applicationWakeups) - .append(", non-apps: ").append(nonApplicationWakeups) - .append(", no uid: ").append(noUidWakeups) - .append(", ").append(durationSec).append("s)") - .toString(); + StringJoiner j = new StringJoiner(", ", "WakeupStats(", ")"); + j.add(iface); + j.add("" + durationSec + "s"); + j.add("total: " + totalWakeups); + j.add("root: " + rootWakeups); + j.add("system: " + systemWakeups); + j.add("apps: " + applicationWakeups); + j.add("non-apps: " + nonApplicationWakeups); + j.add("no uid: " + noUidWakeups); + j.add(String.format("l2 unicast/multicast/broadcast: %d/%d/%d", + l2UnicastCount, l2MulticastCount, l2BroadcastCount)); + for (int i = 0; i < ethertypes.size(); i++) { + int eth = ethertypes.keyAt(i); + int count = ethertypes.valueAt(i); + j.add(String.format("ethertype 0x%x: %d", eth, count)); + } + for (int i = 0; i < ipNextHeaders.size(); i++) { + int proto = ipNextHeaders.keyAt(i); + int count = ipNextHeaders.valueAt(i); + j.add(String.format("ipNxtHdr %d: %d", proto, count)); + } + return j.toString(); + } + + private static void increment(SparseIntArray counters, int key) { + int newcount = counters.get(key, 0) + 1; + counters.put(key, newcount); } } diff --git a/android/net/wifi/WifiInfo.java b/android/net/wifi/WifiInfo.java index a367b231..bf8fed1c 100644 --- a/android/net/wifi/WifiInfo.java +++ b/android/net/wifi/WifiInfo.java @@ -348,6 +348,9 @@ public class WifiInfo implements Parcelable { * quotation marks. Otherwise, it is returned as a string of hex digits. The * SSID may be <unknown ssid> if there is no network currently connected, * or if the caller has insufficient permissions to access the SSID. + * + * Prior to {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}, this method + * always returned the SSID with no quotes around it. * @return the SSID */ public String getSSID() { diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java index c2959d5e..66fabf33 100644 --- a/android/net/wifi/WifiManager.java +++ b/android/net/wifi/WifiManager.java @@ -16,6 +16,7 @@ package android.net.wifi; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; @@ -1127,7 +1128,7 @@ public class WifiManager { */ private int addOrUpdateNetwork(WifiConfiguration config) { try { - return mService.addOrUpdateNetwork(config); + return mService.addOrUpdateNetwork(config, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1148,7 +1149,7 @@ public class WifiManager { */ public void addOrUpdatePasspointConfiguration(PasspointConfiguration config) { try { - if (!mService.addOrUpdatePasspointConfiguration(config)) { + if (!mService.addOrUpdatePasspointConfiguration(config, mContext.getOpPackageName())) { throw new IllegalArgumentException(); } } catch (RemoteException e) { @@ -1165,7 +1166,7 @@ public class WifiManager { */ public void removePasspointConfiguration(String fqdn) { try { - if (!mService.removePasspointConfiguration(fqdn)) { + if (!mService.removePasspointConfiguration(fqdn, mContext.getOpPackageName())) { throw new IllegalArgumentException(); } } catch (RemoteException e) { @@ -1251,7 +1252,7 @@ public class WifiManager { */ public boolean removeNetwork(int netId) { try { - return mService.removeNetwork(netId); + return mService.removeNetwork(netId, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1297,7 +1298,7 @@ public class WifiManager { boolean success; try { - success = mService.enableNetwork(netId, attemptConnect); + success = mService.enableNetwork(netId, attemptConnect, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1323,7 +1324,7 @@ public class WifiManager { */ public boolean disableNetwork(int netId) { try { - return mService.disableNetwork(netId); + return mService.disableNetwork(netId, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1336,7 +1337,7 @@ public class WifiManager { */ public boolean disconnect() { try { - mService.disconnect(); + mService.disconnect(mContext.getOpPackageName()); return true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1351,7 +1352,7 @@ public class WifiManager { */ public boolean reconnect() { try { - mService.reconnect(); + mService.reconnect(mContext.getOpPackageName()); return true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1366,7 +1367,7 @@ public class WifiManager { */ public boolean reassociate() { try { - mService.reassociate(); + mService.reassociate(mContext.getOpPackageName()); return true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1739,7 +1740,7 @@ public class WifiManager { @Deprecated public boolean saveConfiguration() { try { - return mService.saveConfiguration(); + return mService.saveConfiguration(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1748,13 +1749,12 @@ public class WifiManager { /** * Set the country code. * @param countryCode country code in ISO 3166 format. - * @param persist {@code true} if this needs to be remembered * * @hide */ - public void setCountryCode(String country, boolean persist) { + public void setCountryCode(@NonNull String country) { try { - mService.setCountryCode(country, persist); + mService.setCountryCode(country); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1803,18 +1803,14 @@ public class WifiManager { /** * Enable or disable Wi-Fi. - * - * Note: This method will return false if wifi cannot be enabled (e.g., an incompatible mode - * where the user has enabled tethering or Airplane Mode). - * - * Applications need to have the {@link android.Manifest.permission#CHANGE_WIFI_STATE} - * permission to toggle wifi. Callers without the permissions will trigger a - * {@link java.lang.SecurityException}. + * <p> + * Applications must have the {@link android.Manifest.permission#CHANGE_WIFI_STATE} + * permission to toggle wifi. * * @param enabled {@code true} to enable, {@code false} to disable. - * @return {@code true} if the operation succeeds (or if the existing state - * is the same as the requested state). False if wifi cannot be toggled on/off when the - * request is made. + * @return {@code false} if the request cannot be satisfied; {@code true} indicates that wifi is + * either already in the requested state, or in progress toward the requested state. + * @throws {@link java.lang.SecurityException} if the caller is missing required permissions. */ public boolean setWifiEnabled(boolean enabled) { try { @@ -2060,7 +2056,7 @@ public class WifiManager { } mLOHSCallbackProxy = null; try { - mService.stopLocalOnlyHotspot(); + mService.stopLocalOnlyHotspot(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2179,7 +2175,7 @@ public class WifiManager { @RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE) public boolean setWifiApConfiguration(WifiConfiguration wifiConfig) { try { - mService.setWifiApConfiguration(wifiConfig); + mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName()); return true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -2951,7 +2947,7 @@ public class WifiManager { public void disableEphemeralNetwork(String SSID) { if (SSID == null) throw new IllegalArgumentException("SSID cannot be null"); try { - mService.disableEphemeralNetwork(SSID); + mService.disableEphemeralNetwork(SSID, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2990,7 +2986,7 @@ public class WifiManager { */ public Messenger getWifiServiceMessenger() { try { - return mService.getWifiServiceMessenger(); + return mService.getWifiServiceMessenger(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -3441,6 +3437,7 @@ public class WifiManager { * Set wifi verbose log. Called from developer settings. * @hide */ + @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void enableVerboseLogging (int verbose) { try { mService.enableVerboseLogging(verbose); @@ -3519,7 +3516,7 @@ public class WifiManager { */ public void factoryReset() { try { - mService.factoryReset(); + mService.factoryReset(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -3546,7 +3543,7 @@ public class WifiManager { */ public boolean setEnableAutoJoinWhenAssociated(boolean enabled) { try { - return mService.setEnableAutoJoinWhenAssociated(enabled); + return mService.setEnableAutoJoinWhenAssociated(enabled, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/android/net/wifi/rtt/RangingResultCallback.java b/android/net/wifi/rtt/RangingResultCallback.java index 7405e82e..c8aea3c4 100644 --- a/android/net/wifi/rtt/RangingResultCallback.java +++ b/android/net/wifi/rtt/RangingResultCallback.java @@ -36,7 +36,7 @@ import java.util.List; */ public abstract class RangingResultCallback { /** @hide */ - @IntDef({STATUS_CODE_FAIL}) + @IntDef({STATUS_CODE_FAIL, STATUS_CODE_FAIL_RTT_NOT_AVAILABLE}) @Retention(RetentionPolicy.SOURCE) public @interface RangingOperationStatus { } @@ -47,6 +47,14 @@ public abstract class RangingResultCallback { public static final int STATUS_CODE_FAIL = 1; /** + * A failure code for the whole ranging request operation. Indicates that the request failed due + * to RTT not being available - e.g. Wi-Fi was disabled. Use the + * {@link WifiRttManager#isAvailable()} and {@link WifiRttManager#ACTION_WIFI_RTT_STATE_CHANGED} + * to track RTT availability. + */ + public static final int STATUS_CODE_FAIL_RTT_NOT_AVAILABLE = 2; + + /** * Called when a ranging operation failed in whole - i.e. no ranging operation to any of the * devices specified in the request was attempted. * diff --git a/android/net/wifi/rtt/WifiRttManager.java b/android/net/wifi/rtt/WifiRttManager.java index 435bb377..128d6c91 100644 --- a/android/net/wifi/rtt/WifiRttManager.java +++ b/android/net/wifi/rtt/WifiRttManager.java @@ -3,15 +3,19 @@ package android.net.wifi.rtt; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_WIFI_STATE; import static android.Manifest.permission.CHANGE_WIFI_STATE; +import static android.Manifest.permission.LOCATION_HARDWARE; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SdkConstant; +import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; import android.os.Binder; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; +import android.os.WorkSource; import android.util.Log; import java.util.List; @@ -22,11 +26,18 @@ import java.util.List; * <p> * The devices which can be ranged include: * <li>Access Points (APs) + * <li>Wi-Fi Aware peers * <p> * Ranging requests are triggered using * {@link #startRanging(RangingRequest, RangingResultCallback, Handler)}. Results (in case of * successful operation) are returned in the {@link RangingResultCallback#onRangingResults(List)} * callback. + * <p> + * Wi-Fi RTT may not be usable at some points, e.g. when Wi-Fi is disabled. To validate that + * the functionality is available use the {@link #isAvailable()} function. To track + * changes in RTT usability register for the {@link #ACTION_WIFI_RTT_STATE_CHANGED} + * broadcast. Note that this broadcast is not sticky - you should register for it and then + * check the above API to avoid a race condition. * * @hide RTT_API */ @@ -38,6 +49,18 @@ public class WifiRttManager { private final Context mContext; private final IWifiRttManager mService; + /** + * Broadcast intent action to indicate that the state of Wi-Fi RTT availability has changed. + * Use the {@link #isAvailable()} to query the current status. + * This broadcast is <b>not</b> sticky, use the {@link #isAvailable()} API after registering + * the broadcast to check the current state of Wi-Fi RTT. + * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered + * components will be launched. + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_WIFI_RTT_STATE_CHANGED = + "android.net.wifi.rtt.action.WIFI_RTT_STATE_CHANGED"; + /** @hide */ public WifiRttManager(Context context, IWifiRttManager service) { mContext = context; @@ -45,6 +68,22 @@ public class WifiRttManager { } /** + * Returns the current status of RTT API: whether or not RTT is available. To track + * changes in the state of RTT API register for the + * {@link #ACTION_WIFI_RTT_STATE_CHANGED} broadcast. + * + * @return A boolean indicating whether the app can use the RTT API at this time (true) or + * not (false). + */ + public boolean isAvailable() { + try { + return mService.isAvailable(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Initiate a request to range to a set of devices specified in the {@link RangingRequest}. * Results will be returned in the {@link RangingResultCallback} set of callbacks. * @@ -58,21 +97,63 @@ public class WifiRttManager { @RequiresPermission(allOf = {ACCESS_COARSE_LOCATION, CHANGE_WIFI_STATE, ACCESS_WIFI_STATE}) public void startRanging(RangingRequest request, RangingResultCallback callback, @Nullable Handler handler) { + startRanging(null, request, callback, handler); + } + + /** + * Initiate a request to range to a set of devices specified in the {@link RangingRequest}. + * Results will be returned in the {@link RangingResultCallback} set of callbacks. + * + * @param workSource A mechanism to specify an alternative work-source for the request. + * @param request A request specifying a set of devices whose distance measurements are + * requested. + * @param callback A callback for the result of the ranging request. + * @param handler The Handler on whose thread to execute the callbacks of the {@code + * callback} object. If a null is provided then the application's main thread + * will be used. + * + * @hide (@SystemApi) + */ + @RequiresPermission(allOf = {LOCATION_HARDWARE, ACCESS_COARSE_LOCATION, CHANGE_WIFI_STATE, + ACCESS_WIFI_STATE}) + public void startRanging(@Nullable WorkSource workSource, RangingRequest request, + RangingResultCallback callback, @Nullable Handler handler) { if (VDBG) { - Log.v(TAG, "startRanging: request=" + request + ", callback=" + callback + ", handler=" - + handler); + Log.v(TAG, "startRanging: workSource=" + workSource + ", request=" + request + + ", callback=" + callback + ", handler=" + handler); } Looper looper = (handler == null) ? Looper.getMainLooper() : handler.getLooper(); Binder binder = new Binder(); try { - mService.startRanging(binder, mContext.getOpPackageName(), request, + mService.startRanging(binder, mContext.getOpPackageName(), workSource, request, new RttCallbackProxy(looper, callback)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** + * Cancel all ranging requests for the specified work sources. The requests have been requested + * using {@link #startRanging(WorkSource, RangingRequest, RangingResultCallback, Handler)}. + * + * @param workSource The work-sources of the requesters. + * + * @hide (@SystemApi) + */ + @RequiresPermission(allOf = {LOCATION_HARDWARE}) + public void cancelRanging(WorkSource workSource) { + if (VDBG) { + Log.v(TAG, "cancelRanging: workSource=" + workSource); + } + + try { + mService.cancelRanging(workSource); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private static class RttCallbackProxy extends IRttCallback.Stub { private final Handler mHandler; private final RangingResultCallback mCallback; diff --git a/android/os/BatteryManager.java b/android/os/BatteryManager.java index f715f507..6e0f70c1 100644 --- a/android/os/BatteryManager.java +++ b/android/os/BatteryManager.java @@ -19,6 +19,7 @@ package android.os; import android.annotation.SystemService; import android.content.Context; import android.hardware.health.V1_0.Constants; + import com.android.internal.app.IBatteryStats; /** @@ -33,39 +34,39 @@ public class BatteryManager { * integer containing the current status constant. */ public static final String EXTRA_STATUS = "status"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * integer containing the current health constant. */ public static final String EXTRA_HEALTH = "health"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * boolean indicating whether a battery is present. */ public static final String EXTRA_PRESENT = "present"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * integer field containing the current battery level, from 0 to * {@link #EXTRA_SCALE}. */ public static final String EXTRA_LEVEL = "level"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * integer containing the maximum battery level. */ public static final String EXTRA_SCALE = "scale"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * integer containing the resource ID of a small status bar icon * indicating the current battery state. */ public static final String EXTRA_ICON_SMALL = "icon-small"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * integer indicating whether the device is plugged in to a power @@ -73,19 +74,19 @@ public class BatteryManager { * types of power sources. */ public static final String EXTRA_PLUGGED = "plugged"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * integer containing the current battery voltage level. */ public static final String EXTRA_VOLTAGE = "voltage"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * integer containing the current battery temperature. */ public static final String EXTRA_TEMPERATURE = "temperature"; - + /** * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: * String describing the technology of the current battery. @@ -216,6 +217,7 @@ public class BatteryManager { */ public static final int BATTERY_PROPERTY_STATUS = 6; + private final Context mContext; private final IBatteryStats mBatteryStats; private final IBatteryPropertiesRegistrar mBatteryPropertiesRegistrar; @@ -223,6 +225,7 @@ public class BatteryManager { * @removed Was previously made visible by accident. */ public BatteryManager() { + mContext = null; mBatteryStats = IBatteryStats.Stub.asInterface( ServiceManager.getService(BatteryStats.SERVICE_NAME)); mBatteryPropertiesRegistrar = IBatteryPropertiesRegistrar.Stub.asInterface( @@ -230,8 +233,10 @@ public class BatteryManager { } /** {@hide} */ - public BatteryManager(IBatteryStats batteryStats, + public BatteryManager(Context context, + IBatteryStats batteryStats, IBatteryPropertiesRegistrar batteryPropertiesRegistrar) { + mContext = context; mBatteryStats = batteryStats; mBatteryPropertiesRegistrar = batteryPropertiesRegistrar; } @@ -278,16 +283,23 @@ public class BatteryManager { } /** - * Return the value of a battery property of integer type. If the - * platform does not provide the property queried, this value will - * be Integer.MIN_VALUE. + * Return the value of a battery property of integer type. * * @param id identifier of the requested property * - * @return the property value, or Integer.MIN_VALUE if not supported. + * @return the property value. If the property is not supported or there is any other error, + * return (a) 0 if {@code targetSdkVersion < VERSION_CODES.P} or (b) Integer.MIN_VALUE + * if {@code targetSdkVersion >= VERSION_CODES.P}. */ public int getIntProperty(int id) { - return (int)queryProperty(id); + long value = queryProperty(id); + if (value == Long.MIN_VALUE && mContext != null + && mContext.getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.P) { + return Integer.MIN_VALUE; + } + + return (int) value; } /** diff --git a/android/os/BatteryStatsInternal.java b/android/os/BatteryStatsInternal.java new file mode 100644 index 00000000..b0436eb5 --- /dev/null +++ b/android/os/BatteryStatsInternal.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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 android.os; + +/** + * Battery stats local system service interface. This is used to pass internal data out of + * BatteryStatsImpl. + * + * @hide Only for use within Android OS. + */ +public abstract class BatteryStatsInternal { + /** + * Returns the wifi interfaces. + */ + public abstract String[] getWifiIfaces(); + + /** + * Returns the mobile data interfaces. + */ + public abstract String[] getMobileIfaces(); +} diff --git a/android/os/Binder.java b/android/os/Binder.java index 2bfb0138..b5bcd02c 100644 --- a/android/os/Binder.java +++ b/android/os/Binder.java @@ -193,6 +193,19 @@ public class Binder implements IBinder { } /** + * Reset the given interface back to the default blocking behavior, + * reverting any changes made by {@link #allowBlocking(IBinder)}. + * + * @hide + */ + public static IBinder defaultBlocking(IBinder binder) { + if (binder instanceof BinderProxy) { + ((BinderProxy) binder).mWarnOnBlocking = sWarnOnBlocking; + } + return binder; + } + + /** * Inherit the current {@link #allowBlocking(IBinder)} value from one given * interface to another. * diff --git a/android/os/Binder_Delegate.java b/android/os/Binder_Delegate.java new file mode 100644 index 00000000..03596dee --- /dev/null +++ b/android/os/Binder_Delegate.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 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 android.os; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import libcore.util.NativeAllocationRegistry_Delegate; + +/** + * Delegate overriding selected methods of android.os.Binder + * + * Through the layoutlib_create tool, selected methods of Binder have been replaced + * by calls to methods of the same name in this delegate class. + * + * + */ +public class Binder_Delegate { + + // ---- delegate manager ---- + private static final DelegateManager<Binder_Delegate> sManager = + new DelegateManager<>(Binder_Delegate.class); + private static long sFinalizer = -1; + + @LayoutlibDelegate + /*package*/ static long getNativeBBinderHolder() { + return sManager.addNewDelegate(new Binder_Delegate()); + } + + @LayoutlibDelegate + /*package*/ static long getNativeFinalizer() { + synchronized (Binder_Delegate.class) { + if (sFinalizer == -1) { + sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer( + sManager::removeJavaReferenceFor); + } + } + return sFinalizer; + } +} diff --git a/android/os/ConfigUpdate.java b/android/os/ConfigUpdate.java index 13968779..94a44ec3 100644 --- a/android/os/ConfigUpdate.java +++ b/android/os/ConfigUpdate.java @@ -68,13 +68,6 @@ public final class ConfigUpdate { = "android.intent.action.UPDATE_CT_LOGS"; /** - * Update system wide timezone data. - * @hide - */ - @SystemApi - public static final String ACTION_UPDATE_TZDATA = "android.intent.action.UPDATE_TZDATA"; - - /** * Update language detection model file. * @hide */ diff --git a/android/os/Debug.java b/android/os/Debug.java index 017c2134..2acf36fe 100644 --- a/android/os/Debug.java +++ b/android/os/Debug.java @@ -16,14 +16,16 @@ package android.os; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AppGlobals; import android.content.Context; import android.util.Log; import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.Preconditions; import com.android.internal.util.TypedProperties; -import dalvik.bytecode.OpcodeInfo; import dalvik.system.VMDebug; import org.apache.harmony.dalvik.ddmc.Chunk; @@ -48,8 +50,6 @@ import java.util.HashMap; import java.util.Map; - - /** * Provides various debugging methods for Android applications, including * tracing and allocation counts. @@ -1959,13 +1959,7 @@ public final class Debug */ @Deprecated public static class InstructionCount { - private static final int NUM_INSTR = - OpcodeInfo.MAXIMUM_PACKED_VALUE + 1; - - private int[] mCounts; - public InstructionCount() { - mCounts = new int[NUM_INSTR]; } /** @@ -1975,13 +1969,7 @@ public final class Debug * @return true if counting was started */ public boolean resetAndStart() { - try { - VMDebug.startInstructionCounting(); - VMDebug.resetInstructionCount(); - } catch (UnsupportedOperationException uoe) { - return false; - } - return true; + return false; } /** @@ -1989,13 +1977,7 @@ public final class Debug * counting process. */ public boolean collect() { - try { - VMDebug.stopInstructionCounting(); - VMDebug.getInstructionCount(mCounts); - } catch (UnsupportedOperationException uoe) { - return false; - } - return true; + return false; } /** @@ -2003,13 +1985,7 @@ public final class Debug * all threads). */ public int globalTotal() { - int count = 0; - - for (int i = 0; i < NUM_INSTR; i++) { - count += mCounts[i]; - } - - return count; + return 0; } /** @@ -2017,15 +1993,7 @@ public final class Debug * executed globally. */ public int globalMethodInvocations() { - int count = 0; - - for (int i = 0; i < NUM_INSTR; i++) { - if (OpcodeInfo.isInvoke(i)) { - count += mCounts[i]; - } - } - - return count; + return 0; } } @@ -2382,4 +2350,24 @@ public final class Debug public static String getCaller() { return getCaller(Thread.currentThread().getStackTrace(), 0); } + + /** + * Attach a library as a jvmti agent to the current runtime. + * + * @param library library containing the agent + * @param options options passed to the agent + * + * @throws IOException If the agent could not be attached + */ + public static void attachJvmtiAgent(@NonNull String library, @Nullable String options) + throws IOException { + Preconditions.checkNotNull(library); + Preconditions.checkArgument(!library.contains("=")); + + if (options == null) { + VMDebug.attachAgent(library); + } else { + VMDebug.attachAgent(library + "=" + options); + } + } } diff --git a/android/os/Environment.java b/android/os/Environment.java index 5b0e5bbc..f977c1de 100644 --- a/android/os/Environment.java +++ b/android/os/Environment.java @@ -836,7 +836,6 @@ public class Environment { * physically removed. */ public static boolean isExternalStorageRemovable() { - if (isStorageDisabled()) return false; final File externalDir = sCurrentUser.getExternalDirs()[0]; return isExternalStorageRemovable(externalDir); } @@ -875,7 +874,6 @@ public class Environment { * boolean) */ public static boolean isExternalStorageEmulated() { - if (isStorageDisabled()) return false; final File externalDir = sCurrentUser.getExternalDirs()[0]; return isExternalStorageEmulated(externalDir); } @@ -951,9 +949,6 @@ public class Environment { return cur; } - private static boolean isStorageDisabled() { - return SystemProperties.getBoolean("config.disable_storage", false); - } /** * If the given path exists on emulated external storage, return the diff --git a/android/os/FileUtils.java b/android/os/FileUtils.java index 56d6e0a6..7c53ec19 100644 --- a/android/os/FileUtils.java +++ b/android/os/FileUtils.java @@ -320,8 +320,17 @@ public class FileUtils { * is {@code filename}. */ public static void bytesToFile(String filename, byte[] content) throws IOException { - try (FileOutputStream fos = new FileOutputStream(filename)) { - fos.write(content); + if (filename.startsWith("/proc/")) { + final int oldMask = StrictMode.allowThreadDiskWritesMask(); + try (FileOutputStream fos = new FileOutputStream(filename)) { + fos.write(content); + } finally { + StrictMode.setThreadPolicyMask(oldMask); + } + } else { + try (FileOutputStream fos = new FileOutputStream(filename)) { + fos.write(content); + } } } diff --git a/android/os/HidlSupport.java b/android/os/HidlSupport.java index 7dec4d72..3544ea1e 100644 --- a/android/os/HidlSupport.java +++ b/android/os/HidlSupport.java @@ -156,4 +156,27 @@ public class HidlSupport { // Should not reach here. throw new UnsupportedOperationException(); } + + /** + * Test that two interfaces are equal. This is the Java equivalent to C++ + * interfacesEqual function. + * This essentially calls .equals on the internal binder objects (via Binder()). + * - If both interfaces are proxies, asBinder() returns a {@link HwRemoteBinder} + * object, and they are compared in {@link HwRemoteBinder#equals}. + * - If both interfaces are stubs, asBinder() returns the object itself. By default, + * auto-generated IFoo.Stub does not override equals(), but an implementation can + * optionally override it, and {@code interfacesEqual} will use it here. + */ + public static boolean interfacesEqual(IHwInterface lft, Object rgt) { + if (lft == rgt) { + return true; + } + if (lft == null || rgt == null) { + return false; + } + if (!(rgt instanceof IHwInterface)) { + return false; + } + return Objects.equals(lft.asBinder(), ((IHwInterface) rgt).asBinder()); + } } diff --git a/android/os/HwBinder.java b/android/os/HwBinder.java index 270e63f4..5e2a0815 100644 --- a/android/os/HwBinder.java +++ b/android/os/HwBinder.java @@ -16,10 +16,10 @@ package android.os; -import java.util.ArrayList; -import java.util.NoSuchElementException; import libcore.util.NativeAllocationRegistry; +import java.util.NoSuchElementException; + /** @hide */ public abstract class HwBinder implements IHwBinder { private static final String TAG = "HwBinder"; @@ -46,9 +46,16 @@ public abstract class HwBinder implements IHwBinder { public native final void registerService(String serviceName) throws RemoteException; - public static native final IHwBinder getService( + public static final IHwBinder getService( String iface, String serviceName) + throws RemoteException, NoSuchElementException { + return getService(iface, serviceName, false /* retry */); + } + public static native final IHwBinder getService( + String iface, + String serviceName, + boolean retry) throws RemoteException, NoSuchElementException; public static native final void configureRpcThreadpool( diff --git a/android/os/HwBlob.java b/android/os/HwBlob.java index 88226f0a..5e9b9ae3 100644 --- a/android/os/HwBlob.java +++ b/android/os/HwBlob.java @@ -43,6 +43,18 @@ public class HwBlob { public native final double getDouble(long offset); public native final String getString(long offset); + /** + The copyTo... methods copy the blob's data, starting from the given + byte offset, into the array. A total of "size" _elements_ are copied. + */ + public native final void copyToBoolArray(long offset, boolean[] array, int size); + public native final void copyToInt8Array(long offset, byte[] array, int size); + public native final void copyToInt16Array(long offset, short[] array, int size); + public native final void copyToInt32Array(long offset, int[] array, int size); + public native final void copyToInt64Array(long offset, long[] array, int size); + public native final void copyToFloatArray(long offset, float[] array, int size); + public native final void copyToDoubleArray(long offset, double[] array, int size); + public native final void putBool(long offset, boolean x); public native final void putInt8(long offset, byte x); public native final void putInt16(long offset, short x); @@ -52,6 +64,14 @@ public class HwBlob { public native final void putDouble(long offset, double x); public native final void putString(long offset, String x); + public native final void putBoolArray(long offset, boolean[] x); + public native final void putInt8Array(long offset, byte[] x); + public native final void putInt16Array(long offset, short[] x); + public native final void putInt32Array(long offset, int[] x); + public native final void putInt64Array(long offset, long[] x); + public native final void putFloatArray(long offset, float[] x); + public native final void putDoubleArray(long offset, double[] x); + public native final void putBlob(long offset, HwBlob blob); public native final long handle(); diff --git a/android/os/HwRemoteBinder.java b/android/os/HwRemoteBinder.java index 2f89ce62..a07e42c7 100644 --- a/android/os/HwRemoteBinder.java +++ b/android/os/HwRemoteBinder.java @@ -63,4 +63,9 @@ public class HwRemoteBinder implements IHwBinder { } private long mNativeContext; + + @Override + public final native boolean equals(Object other); + @Override + public final native int hashCode(); } diff --git a/android/os/LocaleList.java b/android/os/LocaleList.java index 2dc3bebb..ca9cbec9 100644 --- a/android/os/LocaleList.java +++ b/android/os/LocaleList.java @@ -295,7 +295,11 @@ public final class LocaleList implements Parcelable { return STRING_EN_XA.equals(locale) || STRING_AR_XB.equals(locale); } - private static boolean isPseudoLocale(Locale locale) { + /** + * Returns true if locale is a pseudo-locale, false otherwise. + * {@hide} + */ + public static boolean isPseudoLocale(Locale locale) { return LOCALE_EN_XA.equals(locale) || LOCALE_AR_XB.equals(locale); } diff --git a/android/os/Parcel.java b/android/os/Parcel.java index c2cf3967..10adb5a6 100644 --- a/android/os/Parcel.java +++ b/android/os/Parcel.java @@ -2020,8 +2020,6 @@ public final class Parcel { @Deprecated static native void closeFileDescriptor(FileDescriptor desc) throws IOException; - static native void clearFileDescriptor(FileDescriptor desc); - /** * Read a byte value from the parcel at the current dataPosition(). */ diff --git a/android/os/ParcelFileDescriptor.java b/android/os/ParcelFileDescriptor.java index 7f588adb..7556f092 100644 --- a/android/os/ParcelFileDescriptor.java +++ b/android/os/ParcelFileDescriptor.java @@ -683,7 +683,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { throw new IllegalStateException("Already closed"); } final int fd = getFd(); - Parcel.clearFileDescriptor(mFd); + mFd.setInt$(-1); writeCommStatusAndClose(Status.DETACHED, null); mClosed = true; mGuard.close(); diff --git a/android/os/PowerManager.java b/android/os/PowerManager.java index 7f4dee6e..dd4825ef 100644 --- a/android/os/PowerManager.java +++ b/android/os/PowerManager.java @@ -513,6 +513,53 @@ public final class PowerManager { */ public static final int SHUTDOWN_REASON_BATTERY_THERMAL = 6; + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ServiceType.GPS, + ServiceType.VIBRATION, + ServiceType.ANIMATION, + ServiceType.FULL_BACKUP, + ServiceType.KEYVALUE_BACKUP, + ServiceType.NETWORK_FIREWALL, + ServiceType.SCREEN_BRIGHTNESS, + ServiceType.SOUND, + ServiceType.BATTERY_STATS, + ServiceType.DATA_SAVER, + ServiceType.FORCE_ALL_APPS_STANDBY_JOBS, + ServiceType.FORCE_ALL_APPS_STANDBY_ALARMS, + ServiceType.OPTIONAL_SENSORS, + }) + public @interface ServiceType { + int NULL = 0; + int GPS = 1; + int VIBRATION = 2; + int ANIMATION = 3; + int FULL_BACKUP = 4; + int KEYVALUE_BACKUP = 5; + int NETWORK_FIREWALL = 6; + int SCREEN_BRIGHTNESS = 7; + int SOUND = 8; + int BATTERY_STATS = 9; + int DATA_SAVER = 10; + + /** + * Whether the job scheduler should force app standby on all apps on battery saver or not. + */ + int FORCE_ALL_APPS_STANDBY_JOBS = 11; + + /** + * Whether the alarm manager should force app standby on all apps on battery saver or not. + */ + int FORCE_ALL_APPS_STANDBY_ALARMS = 12; + + /** + * Whether to disable non-essential sensors. (e.g. edge sensors.) + */ + int OPTIONAL_SENSORS = 13; + } + final Context mContext; final IPowerManager mService; final Handler mHandler; @@ -1055,15 +1102,14 @@ public final class PowerManager { /** * Get data about the battery saver mode for a specific service - * @param serviceType unique key for the service, one of - * {@link com.android.server.power.BatterySaverPolicy.ServiceType} + * @param serviceType unique key for the service, one of {@link ServiceType} * @return Battery saver state data. * * @hide * @see com.android.server.power.BatterySaverPolicy * @see PowerSaveState */ - public PowerSaveState getPowerSaveState(int serviceType) { + public PowerSaveState getPowerSaveState(@ServiceType int serviceType) { try { return mService.getPowerSaveState(serviceType); } catch (RemoteException e) { diff --git a/android/os/PowerManagerInternal.java b/android/os/PowerManagerInternal.java index a01b8ed2..77ac2651 100644 --- a/android/os/PowerManagerInternal.java +++ b/android/os/PowerManagerInternal.java @@ -18,6 +18,8 @@ package android.os; import android.view.Display; +import java.util.function.Consumer; + /** * Power manager local system service interface. * @@ -125,6 +127,23 @@ public abstract class PowerManagerInternal { public abstract void registerLowPowerModeObserver(LowPowerModeListener listener); + /** + * Same as {@link #registerLowPowerModeObserver} but can take a lambda. + */ + public void registerLowPowerModeObserver(int serviceType, Consumer<PowerSaveState> listener) { + registerLowPowerModeObserver(new LowPowerModeListener() { + @Override + public int getServiceType() { + return serviceType; + } + + @Override + public void onLowPowerModeChanged(PowerSaveState state) { + listener.accept(state); + } + }); + } + public interface LowPowerModeListener { int getServiceType(); void onLowPowerModeChanged(PowerSaveState state); diff --git a/android/os/PowerSaveState.java b/android/os/PowerSaveState.java index 7058a1dc..de1128df 100644 --- a/android/os/PowerSaveState.java +++ b/android/os/PowerSaveState.java @@ -27,7 +27,7 @@ public class PowerSaveState implements Parcelable { /** * Whether we should enable battery saver for this service. * - * @see com.android.server.power.BatterySaverPolicy.ServiceType + * @see com.android.server.power.BatterySaverPolicy */ public final boolean batterySaverEnabled; /** diff --git a/android/os/RemoteCallbackList.java b/android/os/RemoteCallbackList.java index 2281fb6d..b9b9a18e 100644 --- a/android/os/RemoteCallbackList.java +++ b/android/os/RemoteCallbackList.java @@ -19,6 +19,7 @@ package android.os; import android.util.ArrayMap; import android.util.Slog; +import java.io.PrintWriter; import java.util.function.Consumer; /** @@ -399,6 +400,13 @@ public class RemoteCallbackList<E extends IInterface> { } } + /** @hide */ + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("callbacks: "); pw.println(mCallbacks.size()); + pw.print(prefix); pw.print("killed: "); pw.println(mKilled); + pw.print(prefix); pw.print("broadcasts count: "); pw.println(mBroadcastCount); + } + private void logExcessiveCallbacks() { final long size = mCallbacks.size(); final long TOO_MANY = 3000; diff --git a/android/os/ShellCallback.java b/android/os/ShellCallback.java index e7fe697f..ad9fbfbf 100644 --- a/android/os/ShellCallback.java +++ b/android/os/ShellCallback.java @@ -35,8 +35,9 @@ public class ShellCallback implements Parcelable { IShellCallback mShellCallback; class MyShellCallback extends IShellCallback.Stub { - public ParcelFileDescriptor openOutputFile(String path, String seLinuxContext) { - return onOpenOutputFile(path, seLinuxContext); + public ParcelFileDescriptor openFile(String path, String seLinuxContext, + String mode) { + return onOpenFile(path, seLinuxContext, mode); } } @@ -48,23 +49,27 @@ public class ShellCallback implements Parcelable { } /** - * Ask the shell to open a file for writing. This will truncate the file if it - * already exists. It will create the file if it doesn't exist. + * Ask the shell to open a file. If opening for writing, will truncate the file if it + * already exists and will create the file if it doesn't exist. * @param path Path of the file to be opened/created. * @param seLinuxContext Optional SELinux context that must be allowed to have * access to the file; if null, nothing is required. + * @param mode Mode to open file in: "r" for input/reading an existing file, + * "r+" for reading/writing an existing file, "w" for output/writing a new file (either + * creating or truncating an existing one), "w+" for reading/writing a new file (either + * creating or truncating an existing one). */ - public ParcelFileDescriptor openOutputFile(String path, String seLinuxContext) { - if (DEBUG) Log.d(TAG, "openOutputFile " + this + ": mLocal=" + mLocal + public ParcelFileDescriptor openFile(String path, String seLinuxContext, String mode) { + if (DEBUG) Log.d(TAG, "openFile " + this + " mode=" + mode + ": mLocal=" + mLocal + " mShellCallback=" + mShellCallback); if (mLocal) { - return onOpenOutputFile(path, seLinuxContext); + return onOpenFile(path, seLinuxContext, mode); } if (mShellCallback != null) { try { - return mShellCallback.openOutputFile(path, seLinuxContext); + return mShellCallback.openFile(path, seLinuxContext, mode); } catch (RemoteException e) { Log.w(TAG, "Failure opening " + path, e); } @@ -72,7 +77,7 @@ public class ShellCallback implements Parcelable { return null; } - public ParcelFileDescriptor onOpenOutputFile(String path, String seLinuxContext) { + public ParcelFileDescriptor onOpenFile(String path, String seLinuxContext, String mode) { return null; } diff --git a/android/os/ShellCommand.java b/android/os/ShellCommand.java index 6223235e..d75219fd 100644 --- a/android/os/ShellCommand.java +++ b/android/os/ShellCommand.java @@ -226,10 +226,10 @@ public abstract class ShellCommand { * Helper for just system services to ask the shell to open an output file. * @hide */ - public ParcelFileDescriptor openOutputFileForSystem(String path) { + public ParcelFileDescriptor openFileForSystem(String path, String mode) { try { - ParcelFileDescriptor pfd = getShellCallback().openOutputFile(path, - "u:r:system_server:s0"); + ParcelFileDescriptor pfd = getShellCallback().openFile(path, + "u:r:system_server:s0", mode); if (pfd != null) { return pfd; } diff --git a/android/os/StrictMode.java b/android/os/StrictMode.java index ee3e5bc9..f90604ab 100644 --- a/android/os/StrictMode.java +++ b/android/os/StrictMode.java @@ -16,6 +16,7 @@ package android.os; import android.animation.ValueAnimator; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.ActivityManager; @@ -25,8 +26,26 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; import android.net.TrafficStats; import android.net.Uri; +import android.os.strictmode.CleartextNetworkViolation; +import android.os.strictmode.ContentUriWithoutPermissionViolation; +import android.os.strictmode.CustomViolation; +import android.os.strictmode.DiskReadViolation; +import android.os.strictmode.DiskWriteViolation; +import android.os.strictmode.FileUriExposedViolation; +import android.os.strictmode.InstanceCountViolation; +import android.os.strictmode.IntentReceiverLeakedViolation; +import android.os.strictmode.LeakedClosableViolation; +import android.os.strictmode.NetworkViolation; +import android.os.strictmode.ResourceMismatchViolation; +import android.os.strictmode.ServiceConnectionLeakedViolation; +import android.os.strictmode.SqliteObjectLeakedViolation; +import android.os.strictmode.UnbufferedIoViolation; +import android.os.strictmode.UntaggedSocketViolation; +import android.os.strictmode.Violation; +import android.os.strictmode.WebViewMethodCalledOnWrongThreadViolation; import android.util.ArrayMap; import android.util.Log; import android.util.Printer; @@ -35,6 +54,7 @@ import android.util.Slog; import android.view.IWindowManager; import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.BackgroundThread; import com.android.internal.os.RuntimeInit; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.HexDump; @@ -53,6 +73,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; import java.util.HashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -140,6 +162,15 @@ public final class StrictMode { */ private static final String CLEARTEXT_PROPERTY = "persist.sys.strictmode.clear"; + /** + * Quick feature-flag that can be used to disable the defaults provided by {@link + * #initThreadDefaults(ApplicationInfo)} and {@link #initVmDefaults(ApplicationInfo)}. + */ + private static final boolean DISABLE = false; + + // Only apply VM penalties for the same violation at this interval. + private static final long MIN_VM_INTERVAL_MS = 1000; + // Only log a duplicate stack trace to the logs every second. private static final long MIN_LOG_INTERVAL_MS = 1000; @@ -156,36 +187,30 @@ public final class StrictMode { // Byte 1: Thread-policy /** @hide */ - @TestApi - public static final int DETECT_DISK_WRITE = 0x01; // for ThreadPolicy + @TestApi public static final int DETECT_DISK_WRITE = 0x01; // for ThreadPolicy /** @hide */ - @TestApi - public static final int DETECT_DISK_READ = 0x02; // for ThreadPolicy + @TestApi public static final int DETECT_DISK_READ = 0x02; // for ThreadPolicy /** @hide */ - @TestApi - public static final int DETECT_NETWORK = 0x04; // for ThreadPolicy + @TestApi public static final int DETECT_NETWORK = 0x04; // for ThreadPolicy /** * For StrictMode.noteSlowCall() * * @hide */ - @TestApi - public static final int DETECT_CUSTOM = 0x08; // for ThreadPolicy + @TestApi public static final int DETECT_CUSTOM = 0x08; // for ThreadPolicy /** * For StrictMode.noteResourceMismatch() * * @hide */ - @TestApi - public static final int DETECT_RESOURCE_MISMATCH = 0x10; // for ThreadPolicy + @TestApi public static final int DETECT_RESOURCE_MISMATCH = 0x10; // for ThreadPolicy /** @hide */ - @TestApi - public static final int DETECT_UNBUFFERED_IO = 0x20; // for ThreadPolicy + @TestApi public static final int DETECT_UNBUFFERED_IO = 0x20; // for ThreadPolicy private static final int ALL_THREAD_DETECT_BITS = DETECT_DISK_WRITE @@ -202,48 +227,40 @@ public final class StrictMode { * * @hide */ - @TestApi - public static final int DETECT_VM_CURSOR_LEAKS = 0x01 << 8; // for VmPolicy + @TestApi public static final int DETECT_VM_CURSOR_LEAKS = 0x01 << 8; // for VmPolicy /** * Note, a "VM_" bit, not thread. * * @hide */ - @TestApi - public static final int DETECT_VM_CLOSABLE_LEAKS = 0x02 << 8; // for VmPolicy + @TestApi public static final int DETECT_VM_CLOSABLE_LEAKS = 0x02 << 8; // for VmPolicy /** * Note, a "VM_" bit, not thread. * * @hide */ - @TestApi - public static final int DETECT_VM_ACTIVITY_LEAKS = 0x04 << 8; // for VmPolicy + @TestApi public static final int DETECT_VM_ACTIVITY_LEAKS = 0x04 << 8; // for VmPolicy /** @hide */ - @TestApi - public static final int DETECT_VM_INSTANCE_LEAKS = 0x08 << 8; // for VmPolicy + @TestApi public static final int DETECT_VM_INSTANCE_LEAKS = 0x08 << 8; // for VmPolicy /** @hide */ - @TestApi - public static final int DETECT_VM_REGISTRATION_LEAKS = 0x10 << 8; // for VmPolicy + @TestApi public static final int DETECT_VM_REGISTRATION_LEAKS = 0x10 << 8; // for VmPolicy /** @hide */ - @TestApi - public static final int DETECT_VM_FILE_URI_EXPOSURE = 0x20 << 8; // for VmPolicy + @TestApi public static final int DETECT_VM_FILE_URI_EXPOSURE = 0x20 << 8; // for VmPolicy /** @hide */ - @TestApi - public static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8; // for VmPolicy + @TestApi public static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8; // for VmPolicy /** @hide */ @TestApi public static final int DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION = 0x80 << 8; // for VmPolicy /** @hide */ - @TestApi - public static final int DETECT_VM_UNTAGGED_SOCKET = 0x80 << 24; // for VmPolicy + @TestApi public static final int DETECT_VM_UNTAGGED_SOCKET = 0x80 << 24; // for VmPolicy private static final int ALL_VM_DETECT_BITS = DETECT_VM_CURSOR_LEAKS @@ -354,15 +371,33 @@ public final class StrictMode { } else { msg = "StrictMode policy violation:"; } - if (info.hasStackTrace()) { - Log.d(TAG, msg + " " + info.getStackTrace()); - } else { - Log.d(TAG, msg + " missing stack trace!"); - } + Log.d(TAG, msg + " " + info.getStackTrace()); }; private static volatile ViolationLogger sLogger = LOGCAT_LOGGER; + private static final ThreadLocal<OnThreadViolationListener> sThreadViolationListener = + new ThreadLocal<>(); + private static final ThreadLocal<Executor> sThreadViolationExecutor = new ThreadLocal<>(); + + /** + * When #{@link ThreadPolicy.Builder#penaltyListener} is enabled, the listener is called on the + * provided executor when a Thread violation occurs. + */ + public interface OnThreadViolationListener { + /** Called on a thread policy violation. */ + void onThreadViolation(Violation v); + } + + /** + * When #{@link VmPolicy.Builder#penaltyListener} is enabled, the listener is called on the + * provided executor when a VM violation occurs. + */ + public interface OnVmViolationListener { + /** Called on a VM policy violation. */ + void onVmViolation(Violation v); + } + /** {@hide} */ @TestApi public static void setViolationLogger(ViolationLogger listener) { @@ -392,12 +427,16 @@ public final class StrictMode { */ public static final class ThreadPolicy { /** The default, lax policy which doesn't catch anything. */ - public static final ThreadPolicy LAX = new ThreadPolicy(0); + public static final ThreadPolicy LAX = new ThreadPolicy(0, null, null); final int mask; + final OnThreadViolationListener mListener; + final Executor mCallbackExecutor; - private ThreadPolicy(int mask) { + private ThreadPolicy(int mask, OnThreadViolationListener listener, Executor executor) { this.mask = mask; + mListener = listener; + mCallbackExecutor = executor; } @Override @@ -425,6 +464,8 @@ public final class StrictMode { */ public static final class Builder { private int mMask = 0; + private OnThreadViolationListener mListener; + private Executor mExecutor; /** * Create a Builder that detects nothing and has no violations. (but note that {@link @@ -590,6 +631,20 @@ public final class StrictMode { return enable(PENALTY_DROPBOX); } + /** + * Call #{@link OnThreadViolationListener#onThreadViolation(Violation)} on specified + * executor every violation. + */ + public Builder penaltyListener( + @NonNull OnThreadViolationListener listener, @NonNull Executor executor) { + if (executor == null) { + throw new NullPointerException("executor must not be null"); + } + mListener = listener; + mExecutor = executor; + return this; + } + private Builder enable(int bit) { mMask |= bit; return this; @@ -609,7 +664,8 @@ public final class StrictMode { public ThreadPolicy build() { // If there are detection bits set but no violation bits // set, enable simple logging. - if (mMask != 0 + if (mListener == null + && mMask != 0 && (mMask & (PENALTY_DEATH | PENALTY_LOG @@ -618,7 +674,7 @@ public final class StrictMode { == 0) { penaltyLog(); } - return new ThreadPolicy(mMask); + return new ThreadPolicy(mMask, mListener, mExecutor); } } } @@ -630,19 +686,27 @@ public final class StrictMode { */ public static final class VmPolicy { /** The default, lax policy which doesn't catch anything. */ - public static final VmPolicy LAX = new VmPolicy(0, EMPTY_CLASS_LIMIT_MAP); + public static final VmPolicy LAX = new VmPolicy(0, EMPTY_CLASS_LIMIT_MAP, null, null); final int mask; + final OnVmViolationListener mListener; + final Executor mCallbackExecutor; // Map from class to max number of allowed instances in memory. final HashMap<Class, Integer> classInstanceLimit; - private VmPolicy(int mask, HashMap<Class, Integer> classInstanceLimit) { + private VmPolicy( + int mask, + HashMap<Class, Integer> classInstanceLimit, + OnVmViolationListener listener, + Executor executor) { if (classInstanceLimit == null) { throw new NullPointerException("classInstanceLimit == null"); } this.mask = mask; this.classInstanceLimit = classInstanceLimit; + mListener = listener; + mCallbackExecutor = executor; } @Override @@ -670,6 +734,8 @@ public final class StrictMode { */ public static final class Builder { private int mMask; + private OnVmViolationListener mListener; + private Executor mExecutor; private HashMap<Class, Integer> mClassInstanceLimit; // null until needed private boolean mClassInstanceLimitNeedCow = false; // need copy-on-write @@ -683,6 +749,8 @@ public final class StrictMode { mMask = base.mask; mClassInstanceLimitNeedCow = true; mClassInstanceLimit = base.classInstanceLimit; + mListener = base.mListener; + mExecutor = base.mCallbackExecutor; } /** @@ -714,6 +782,11 @@ public final class StrictMode { return enable(DETECT_VM_ACTIVITY_LEAKS); } + /** @hide */ + public Builder permitActivityLeaks() { + return disable(DETECT_VM_ACTIVITY_LEAKS); + } + /** * Detect everything that's potentially suspect. * @@ -847,6 +920,11 @@ public final class StrictMode { return enable(DETECT_VM_UNTAGGED_SOCKET); } + /** @hide */ + public Builder permitUntaggedSockets() { + return disable(DETECT_VM_UNTAGGED_SOCKET); + } + /** * Crashes the whole process on violation. This penalty runs at the end of all enabled * penalties so you'll still get your logging or other violations before the process @@ -889,6 +967,19 @@ public final class StrictMode { return enable(PENALTY_DROPBOX); } + /** + * Call #{@link OnVmViolationListener#onVmViolation(Violation)} on every violation. + */ + public Builder penaltyListener( + @NonNull OnVmViolationListener listener, @NonNull Executor executor) { + if (executor == null) { + throw new NullPointerException("executor must not be null"); + } + mListener = listener; + mExecutor = executor; + return this; + } + private Builder enable(int bit) { mMask |= bit; return this; @@ -908,7 +999,8 @@ public final class StrictMode { public VmPolicy build() { // If there are detection bits set but no violation bits // set, enable simple logging. - if (mMask != 0 + if (mListener == null + && mMask != 0 && (mMask & (PENALTY_DEATH | PENALTY_LOG @@ -919,7 +1011,9 @@ public final class StrictMode { } return new VmPolicy( mMask, - mClassInstanceLimit != null ? mClassInstanceLimit : EMPTY_CLASS_LIMIT_MAP); + mClassInstanceLimit != null ? mClassInstanceLimit : EMPTY_CLASS_LIMIT_MAP, + mListener, + mExecutor); } } } @@ -952,9 +1046,12 @@ public final class StrictMode { */ public static void setThreadPolicy(final ThreadPolicy policy) { setThreadPolicyMask(policy.mask); + sThreadViolationListener.set(policy.mListener); + sThreadViolationExecutor.set(policy.mCallbackExecutor); } - private static void setThreadPolicyMask(final int policyMask) { + /** @hide */ + public static void setThreadPolicyMask(final int policyMask) { // In addition to the Java-level thread-local in Dalvik's // BlockGuard, we also need to keep a native thread-local in // Binder in order to propagate the value across Binder calls, @@ -991,55 +1088,6 @@ public final class StrictMode { CloseGuard.setEnabled(enabled); } - /** @hide */ - public static class StrictModeViolation extends BlockGuard.BlockGuardPolicyException { - public StrictModeViolation(int policyState, int policyViolated, String message) { - super(policyState, policyViolated, message); - } - } - - /** @hide */ - public static class StrictModeNetworkViolation extends StrictModeViolation { - public StrictModeNetworkViolation(int policyMask) { - super(policyMask, DETECT_NETWORK, null); - } - } - - /** @hide */ - private static class StrictModeDiskReadViolation extends StrictModeViolation { - public StrictModeDiskReadViolation(int policyMask) { - super(policyMask, DETECT_DISK_READ, null); - } - } - - /** @hide */ - private static class StrictModeDiskWriteViolation extends StrictModeViolation { - public StrictModeDiskWriteViolation(int policyMask) { - super(policyMask, DETECT_DISK_WRITE, null); - } - } - - /** @hide */ - private static class StrictModeCustomViolation extends StrictModeViolation { - public StrictModeCustomViolation(int policyMask, String name) { - super(policyMask, DETECT_CUSTOM, name); - } - } - - /** @hide */ - private static class StrictModeResourceMismatchViolation extends StrictModeViolation { - public StrictModeResourceMismatchViolation(int policyMask, Object tag) { - super(policyMask, DETECT_RESOURCE_MISMATCH, tag != null ? tag.toString() : null); - } - } - - /** @hide */ - private static class StrictModeUnbufferedIOViolation extends StrictModeViolation { - public StrictModeUnbufferedIOViolation(int policyMask) { - super(policyMask, DETECT_UNBUFFERED_IO, null); - } - } - /** * Returns the bitmask of the current thread's policy. * @@ -1056,7 +1104,10 @@ public final class StrictMode { // introduce VmPolicy cleanly) but this isn't particularly // optimal for users who might call this method often. This // should be in a thread-local and not allocate on each call. - return new ThreadPolicy(getThreadPolicyMask()); + return new ThreadPolicy( + getThreadPolicyMask(), + sThreadViolationListener.get(), + sThreadViolationExecutor.get()); } /** @@ -1069,12 +1120,20 @@ public final class StrictMode { * end of a block */ public static ThreadPolicy allowThreadDiskWrites() { + return new ThreadPolicy( + allowThreadDiskWritesMask(), + sThreadViolationListener.get(), + sThreadViolationExecutor.get()); + } + + /** @hide */ + public static int allowThreadDiskWritesMask() { int oldPolicyMask = getThreadPolicyMask(); int newPolicyMask = oldPolicyMask & ~(DETECT_DISK_WRITE | DETECT_DISK_READ); if (newPolicyMask != oldPolicyMask) { setThreadPolicyMask(newPolicyMask); } - return new ThreadPolicy(oldPolicyMask); + return oldPolicyMask; } /** @@ -1085,31 +1144,66 @@ public final class StrictMode { * @return the old policy, to be passed to setThreadPolicy to restore the policy. */ public static ThreadPolicy allowThreadDiskReads() { + return new ThreadPolicy( + allowThreadDiskReadsMask(), + sThreadViolationListener.get(), + sThreadViolationExecutor.get()); + } + + /** @hide */ + public static int allowThreadDiskReadsMask() { int oldPolicyMask = getThreadPolicyMask(); int newPolicyMask = oldPolicyMask & ~(DETECT_DISK_READ); if (newPolicyMask != oldPolicyMask) { setThreadPolicyMask(newPolicyMask); } - return new ThreadPolicy(oldPolicyMask); + return oldPolicyMask; } - // We don't want to flash the screen red in the system server - // process, nor do we want to modify all the call sites of - // conditionallyEnableDebugLogging() in the system server, - // so instead we use this to determine if we are the system server. - private static boolean amTheSystemServerProcess() { - // Fast path. Most apps don't have the system server's UID. - if (Process.myUid() != Process.SYSTEM_UID) { - return false; - } + private static ThreadPolicy allowThreadViolations() { + ThreadPolicy oldPolicy = getThreadPolicy(); + setThreadPolicyMask(0); + return oldPolicy; + } - // The settings app, though, has the system server's UID so - // look up our stack to see if we came from the system server. - Throwable stack = new Throwable(); - stack.fillInStackTrace(); - for (StackTraceElement ste : stack.getStackTrace()) { - String clsName = ste.getClassName(); - if (clsName != null && clsName.startsWith("com.android.server.")) { + private static VmPolicy allowVmViolations() { + VmPolicy oldPolicy = getVmPolicy(); + sVmPolicy = VmPolicy.LAX; + return oldPolicy; + } + + /** + * Determine if the given app is "bundled" as part of the system image. These bundled apps are + * developed in lock-step with the OS, and they aren't updated outside of an OTA, so we want to + * chase any {@link StrictMode} regressions by enabling detection when running on {@link + * Build#IS_USERDEBUG} or {@link Build#IS_ENG} builds. + * + * <p>Unbundled apps included in the system image are expected to detect and triage their own + * {@link StrictMode} issues separate from the OS release process, which is why we don't enable + * them here. + * + * @hide + */ + public static boolean isBundledSystemApp(ApplicationInfo ai) { + if (ai == null || ai.packageName == null) { + // Probably system server + return true; + } else if (ai.isSystemApp()) { + // Ignore unbundled apps living in the wrong namespace + if (ai.packageName.equals("com.android.vending") + || ai.packageName.equals("com.android.chrome")) { + return false; + } + + // Ignore bundled apps that are way too spammy + // STOPSHIP: burn this list down to zero + if (ai.packageName.equals("com.android.phone")) { + return false; + } + + if (ai.packageName.equals("android") + || ai.packageName.startsWith("android.") + || ai.packageName.startsWith("com.android.")) { return true; } } @@ -1117,81 +1211,81 @@ public final class StrictMode { } /** - * Enable DropBox logging for debug phone builds. + * Initialize default {@link ThreadPolicy} for the current thread. * * @hide */ - public static boolean conditionallyEnableDebugLogging() { - boolean doFlashes = - SystemProperties.getBoolean(VISUAL_PROPERTY, false) && !amTheSystemServerProcess(); - final boolean suppress = SystemProperties.getBoolean(DISABLE_PROPERTY, false); - - // For debug builds, log event loop stalls to dropbox for analysis. - // Similar logic also appears in ActivityThread.java for system apps. - if (!doFlashes && (Build.IS_USER || suppress)) { - setCloseGuardEnabled(false); - return false; + public static void initThreadDefaults(ApplicationInfo ai) { + final ThreadPolicy.Builder builder = new ThreadPolicy.Builder(); + final int targetSdkVersion = + (ai != null) ? ai.targetSdkVersion : Build.VERSION_CODES.CUR_DEVELOPMENT; + + // Starting in HC, we don't allow network usage on the main thread + if (targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) { + builder.detectNetwork(); + builder.penaltyDeathOnNetwork(); + } + + if (Build.IS_USER || DISABLE || SystemProperties.getBoolean(DISABLE_PROPERTY, false)) { + // Detect nothing extra + } else if (Build.IS_USERDEBUG) { + // Detect everything in bundled apps + if (isBundledSystemApp(ai)) { + builder.detectAll(); + builder.penaltyDropBox(); + if (SystemProperties.getBoolean(VISUAL_PROPERTY, false)) { + builder.penaltyFlashScreen(); + } + } + } else if (Build.IS_ENG) { + // Detect everything in bundled apps + if (isBundledSystemApp(ai)) { + builder.detectAll(); + builder.penaltyDropBox(); + builder.penaltyLog(); + builder.penaltyFlashScreen(); + } } - // Eng builds have flashes on all the time. The suppression property - // overrides this, so we force the behavior only after the short-circuit - // check above. - if (Build.IS_ENG) { - doFlashes = true; - } + setThreadPolicy(builder.build()); + } - // Thread policy controls BlockGuard. - int threadPolicyMask = - StrictMode.DETECT_DISK_WRITE - | StrictMode.DETECT_DISK_READ - | StrictMode.DETECT_NETWORK; + /** + * Initialize default {@link VmPolicy} for the current VM. + * + * @hide + */ + public static void initVmDefaults(ApplicationInfo ai) { + final VmPolicy.Builder builder = new VmPolicy.Builder(); + final int targetSdkVersion = + (ai != null) ? ai.targetSdkVersion : Build.VERSION_CODES.CUR_DEVELOPMENT; - if (!Build.IS_USER) { - threadPolicyMask |= StrictMode.PENALTY_DROPBOX; - } - if (doFlashes) { - threadPolicyMask |= StrictMode.PENALTY_FLASH; + // Starting in N, we don't allow file:// Uri exposure + if (targetSdkVersion >= Build.VERSION_CODES.N) { + builder.detectFileUriExposure(); + builder.penaltyDeathOnFileUriExposure(); } - StrictMode.setThreadPolicyMask(threadPolicyMask); - - // VM Policy controls CloseGuard, detection of Activity leaks, - // and instance counting. - if (Build.IS_USER) { - setCloseGuardEnabled(false); - } else { - VmPolicy.Builder policyBuilder = new VmPolicy.Builder().detectAll(); - if (!Build.IS_ENG) { - // Activity leak detection causes too much slowdown for userdebug because of the - // GCs. - policyBuilder = policyBuilder.disable(DETECT_VM_ACTIVITY_LEAKS); - } - policyBuilder = policyBuilder.penaltyDropBox(); - if (Build.IS_ENG) { - policyBuilder.penaltyLog(); - } - // All core system components need to tag their sockets to aid - // system health investigations - if (android.os.Process.myUid() < android.os.Process.FIRST_APPLICATION_UID) { - policyBuilder.enable(DETECT_VM_UNTAGGED_SOCKET); - } else { - policyBuilder.disable(DETECT_VM_UNTAGGED_SOCKET); + if (Build.IS_USER || DISABLE || SystemProperties.getBoolean(DISABLE_PROPERTY, false)) { + // Detect nothing extra + } else if (Build.IS_USERDEBUG) { + // Detect everything in bundled apps (except activity leaks, which + // are expensive to track) + if (isBundledSystemApp(ai)) { + builder.detectAll(); + builder.permitActivityLeaks(); + builder.penaltyDropBox(); + } + } else if (Build.IS_ENG) { + // Detect everything in bundled apps + if (isBundledSystemApp(ai)) { + builder.detectAll(); + builder.penaltyDropBox(); + builder.penaltyLog(); } - setVmPolicy(policyBuilder.build()); - setCloseGuardEnabled(vmClosableObjectLeaksEnabled()); } - return true; - } - /** - * Used by the framework to make network usage on the main thread a fatal error. - * - * @hide - */ - public static void enableDeathOnNetwork() { - int oldPolicy = getThreadPolicyMask(); - int newPolicy = oldPolicy | DETECT_NETWORK | PENALTY_DEATH_ON_NETWORK; - setThreadPolicyMask(newPolicy); + setVmPolicy(builder.build()); } /** @@ -1205,7 +1299,9 @@ public final class StrictMode { sVmPolicy.mask | DETECT_VM_FILE_URI_EXPOSURE | PENALTY_DEATH_ON_FILE_URI_EXPOSURE, - sVmPolicy.classInstanceLimit); + sVmPolicy.classInstanceLimit, + sVmPolicy.mListener, + sVmPolicy.mCallbackExecutor); } /** @@ -1220,7 +1316,9 @@ public final class StrictMode { sVmPolicy.mask & ~(DETECT_VM_FILE_URI_EXPOSURE | PENALTY_DEATH_ON_FILE_URI_EXPOSURE), - sVmPolicy.classInstanceLimit); + sVmPolicy.classInstanceLimit, + sVmPolicy.mListener, + sVmPolicy.mCallbackExecutor); } /** @@ -1308,9 +1406,7 @@ public final class StrictMode { if (tooManyViolationsThisLoop()) { return; } - BlockGuard.BlockGuardPolicyException e = new StrictModeDiskWriteViolation(mPolicyMask); - e.fillInStackTrace(); - startHandlingViolationException(e); + startHandlingViolationException(new DiskWriteViolation()); } // Not part of BlockGuard.Policy; just part of StrictMode: @@ -1321,10 +1417,7 @@ public final class StrictMode { if (tooManyViolationsThisLoop()) { return; } - BlockGuard.BlockGuardPolicyException e = - new StrictModeCustomViolation(mPolicyMask, name); - e.fillInStackTrace(); - startHandlingViolationException(e); + startHandlingViolationException(new CustomViolation(name)); } // Not part of BlockGuard.Policy; just part of StrictMode: @@ -1335,13 +1428,10 @@ public final class StrictMode { if (tooManyViolationsThisLoop()) { return; } - BlockGuard.BlockGuardPolicyException e = - new StrictModeResourceMismatchViolation(mPolicyMask, tag); - e.fillInStackTrace(); - startHandlingViolationException(e); + startHandlingViolationException(new ResourceMismatchViolation(tag)); } - // Part of BlockGuard.Policy; just part of StrictMode: + // Not part of BlockGuard.Policy; just part of StrictMode: public void onUnbufferedIO() { if ((mPolicyMask & DETECT_UNBUFFERED_IO) == 0) { return; @@ -1349,10 +1439,7 @@ public final class StrictMode { if (tooManyViolationsThisLoop()) { return; } - BlockGuard.BlockGuardPolicyException e = - new StrictModeUnbufferedIOViolation(mPolicyMask); - e.fillInStackTrace(); - startHandlingViolationException(e); + startHandlingViolationException(new UnbufferedIoViolation()); } // Part of BlockGuard.Policy interface: @@ -1363,9 +1450,7 @@ public final class StrictMode { if (tooManyViolationsThisLoop()) { return; } - BlockGuard.BlockGuardPolicyException e = new StrictModeDiskReadViolation(mPolicyMask); - e.fillInStackTrace(); - startHandlingViolationException(e); + startHandlingViolationException(new DiskReadViolation()); } // Part of BlockGuard.Policy interface: @@ -1379,9 +1464,7 @@ public final class StrictMode { if (tooManyViolationsThisLoop()) { return; } - BlockGuard.BlockGuardPolicyException e = new StrictModeNetworkViolation(mPolicyMask); - e.fillInStackTrace(); - startHandlingViolationException(e); + startHandlingViolationException(new NetworkViolation()); } public void setPolicyMask(int policyMask) { @@ -1393,8 +1476,8 @@ public final class StrictMode { // has yet occurred). This sees if we're in an event loop // thread and, if so, uses it to roughly measure how long the // violation took. - void startHandlingViolationException(BlockGuard.BlockGuardPolicyException e) { - final ViolationInfo info = new ViolationInfo(e, e.getPolicy()); + void startHandlingViolationException(Violation e) { + final ViolationInfo info = new ViolationInfo(e, mPolicyMask); info.violationUptimeMillis = SystemClock.uptimeMillis(); handleViolationWithTimingAttempt(info); } @@ -1423,9 +1506,9 @@ public final class StrictMode { // // TODO: if in gather mode, ignore Looper.myLooper() and always // go into this immediate mode? - if (looper == null || (info.policy & THREAD_PENALTY_MASK) == PENALTY_DEATH) { + if (looper == null || (info.mPolicy & THREAD_PENALTY_MASK) == PENALTY_DEATH) { info.durationMillis = -1; // unknown (redundant, already set) - handleViolation(info); + onThreadPolicyViolation(info); return; } @@ -1443,7 +1526,7 @@ public final class StrictMode { } final IWindowManager windowManager = - (info.policy & PENALTY_FLASH) != 0 ? sWindowManager.get() : null; + info.penaltyEnabled(PENALTY_FLASH) ? sWindowManager.get() : null; if (windowManager != null) { try { windowManager.showStrictModeViolation(true); @@ -1463,30 +1546,28 @@ public final class StrictMode { THREAD_HANDLER .get() .postAtFrontOfQueue( - new Runnable() { - public void run() { - long loopFinishTime = SystemClock.uptimeMillis(); - - // Note: we do this early, before handling the - // violation below, as handling the violation - // may include PENALTY_DEATH and we don't want - // to keep the red border on. - if (windowManager != null) { - try { - windowManager.showStrictModeViolation(false); - } catch (RemoteException unused) { - } + () -> { + long loopFinishTime = SystemClock.uptimeMillis(); + + // Note: we do this early, before handling the + // violation below, as handling the violation + // may include PENALTY_DEATH and we don't want + // to keep the red border on. + if (windowManager != null) { + try { + windowManager.showStrictModeViolation(false); + } catch (RemoteException unused) { } + } - for (int n = 0; n < records.size(); ++n) { - ViolationInfo v = records.get(n); - v.violationNumThisLoop = n + 1; - v.durationMillis = - (int) (loopFinishTime - v.violationUptimeMillis); - handleViolation(v); - } - records.clear(); + for (int n = 0; n < records.size(); ++n) { + ViolationInfo v = records.get(n); + v.violationNumThisLoop = n + 1; + v.durationMillis = + (int) (loopFinishTime - v.violationUptimeMillis); + onThreadPolicyViolation(v); } + records.clear(); }); } @@ -1495,18 +1576,13 @@ public final class StrictMode { // violation fired and now (after the violating code ran) due // to people who push/pop temporary policy in regions of code, // hence the policy being passed around. - void handleViolation(final ViolationInfo info) { - if (info == null || !info.hasStackTrace()) { - Log.wtf(TAG, "unexpected null stacktrace"); - return; - } - - if (LOG_V) Log.d(TAG, "handleViolation; policy=" + info.policy); + void onThreadPolicyViolation(final ViolationInfo info) { + if (LOG_V) Log.d(TAG, "onThreadPolicyViolation; policy=" + info.mPolicy); - if ((info.policy & PENALTY_GATHER) != 0) { + if (info.penaltyEnabled(PENALTY_GATHER)) { ArrayList<ViolationInfo> violations = gatheredViolations.get(); if (violations == null) { - violations = new ArrayList<ViolationInfo>(1); + violations = new ArrayList<>(1); gatheredViolations.set(violations); } for (ViolationInfo previous : violations) { @@ -1535,31 +1611,32 @@ public final class StrictMode { long timeSinceLastViolationMillis = lastViolationTime == 0 ? Long.MAX_VALUE : (now - lastViolationTime); - if ((info.policy & PENALTY_LOG) != 0 + if (info.penaltyEnabled(PENALTY_LOG) && timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { sLogger.log(info); } + final Violation violation = info.mViolation; + // The violationMaskSubset, passed to ActivityManager, is a // subset of the original StrictMode policy bitmask, with // only the bit violated and penalty bits to be executed // by the ActivityManagerService remaining set. int violationMaskSubset = 0; - if ((info.policy & PENALTY_DIALOG) != 0 + if (info.penaltyEnabled(PENALTY_DIALOG) && timeSinceLastViolationMillis > MIN_DIALOG_INTERVAL_MS) { violationMaskSubset |= PENALTY_DIALOG; } - if ((info.policy & PENALTY_DROPBOX) != 0 && lastViolationTime == 0) { + if (info.penaltyEnabled(PENALTY_DROPBOX) && lastViolationTime == 0) { violationMaskSubset |= PENALTY_DROPBOX; } if (violationMaskSubset != 0) { violationMaskSubset |= info.getViolationBit(); - final int savedPolicyMask = getThreadPolicyMask(); - final boolean justDropBox = (info.policy & THREAD_PENALTY_MASK) == PENALTY_DROPBOX; + final boolean justDropBox = (info.mPolicy & THREAD_PENALTY_MASK) == PENALTY_DROPBOX; if (justDropBox) { // If all we're going to ask the activity manager // to do is dropbox it (the common case during @@ -1568,42 +1645,38 @@ public final class StrictMode { // isn't always super fast, despite the implementation // in the ActivityManager trying to be mostly async. dropboxViolationAsync(violationMaskSubset, info); - return; + } else { + handleApplicationStrictModeViolation(violationMaskSubset, info); } + } - // Normal synchronous call to the ActivityManager. - try { - // First, remove any policy before we call into the Activity Manager, - // otherwise we'll infinite recurse as we try to log policy violations - // to disk, thus violating policy, thus requiring logging, etc... - // We restore the current policy below, in the finally block. - setThreadPolicyMask(0); - - ActivityManager.getService() - .handleApplicationStrictModeViolation( - RuntimeInit.getApplicationObject(), violationMaskSubset, info); - } catch (RemoteException e) { - if (e instanceof DeadObjectException) { - // System process is dead; ignore - } else { - Log.e(TAG, "RemoteException trying to handle StrictMode violation", e); - } - } finally { - // Restore the policy. - setThreadPolicyMask(savedPolicyMask); - } + if ((info.getPolicyMask() & PENALTY_DEATH) != 0) { + throw new RuntimeException("StrictMode ThreadPolicy violation", violation); } - if ((info.policy & PENALTY_DEATH) != 0) { - executeDeathPenalty(info); + // penaltyDeath will cause penaltyCallback to no-op since we cannot guarantee the + // executor finishes before crashing. + final OnThreadViolationListener listener = sThreadViolationListener.get(); + final Executor executor = sThreadViolationExecutor.get(); + if (listener != null && executor != null) { + try { + executor.execute( + () -> { + // Lift violated policy to prevent infinite recursion. + ThreadPolicy oldPolicy = allowThreadViolations(); + try { + listener.onThreadViolation(violation); + } finally { + setThreadPolicy(oldPolicy); + } + }); + } catch (RejectedExecutionException e) { + Log.e(TAG, "ThreadPolicy penaltyCallback failed", e); + } } } } - private static void executeDeathPenalty(ViolationInfo info) { - throw new StrictModeViolation(info.policy, info.getViolationBit(), null); - } - /** * In the common case, as set by conditionallyEnableDebugLogging, we're just dropboxing any * violations but not showing a dialog, not loggging, and not killing the process. In these @@ -1622,33 +1695,44 @@ public final class StrictMode { if (LOG_V) Log.d(TAG, "Dropboxing async; in-flight=" + outstanding); - new Thread("callActivityManagerForStrictModeDropbox") { - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - try { - IActivityManager am = ActivityManager.getService(); - if (am == null) { - Log.d(TAG, "No activity manager; failed to Dropbox violation."); - } else { - am.handleApplicationStrictModeViolation( - RuntimeInit.getApplicationObject(), violationMaskSubset, info); - } - } catch (RemoteException e) { - if (e instanceof DeadObjectException) { - // System process is dead; ignore - } else { - Log.e(TAG, "RemoteException handling StrictMode violation", e); - } - } - int outstanding = sDropboxCallsInFlight.decrementAndGet(); - if (LOG_V) Log.d(TAG, "Dropbox complete; in-flight=" + outstanding); + BackgroundThread.getHandler().post(() -> { + handleApplicationStrictModeViolation(violationMaskSubset, info); + int outstandingInner = sDropboxCallsInFlight.decrementAndGet(); + if (LOG_V) Log.d(TAG, "Dropbox complete; in-flight=" + outstandingInner); + }); + } + + private static void handleApplicationStrictModeViolation(int violationMaskSubset, + ViolationInfo info) { + final int oldMask = getThreadPolicyMask(); + try { + // First, remove any policy before we call into the Activity Manager, + // otherwise we'll infinite recurse as we try to log policy violations + // to disk, thus violating policy, thus requiring logging, etc... + // We restore the current policy below, in the finally block. + setThreadPolicyMask(0); + + IActivityManager am = ActivityManager.getService(); + if (am == null) { + Log.w(TAG, "No activity manager; failed to Dropbox violation."); + } else { + am.handleApplicationStrictModeViolation( + RuntimeInit.getApplicationObject(), violationMaskSubset, info); } - }.start(); + } catch (RemoteException e) { + if (e instanceof DeadObjectException) { + // System process is dead; ignore + } else { + Log.e(TAG, "RemoteException handling StrictMode violation", e); + } + } finally { + setThreadPolicyMask(oldMask); + } } private static class AndroidCloseGuardReporter implements CloseGuard.Reporter { public void report(String message, Throwable allocationSite) { - onVmPolicyViolation(allocationSite); + onVmPolicyViolation(new LeakedClosableViolation(message, allocationSite)); } } @@ -1686,8 +1770,7 @@ public final class StrictMode { int limit = policy.classInstanceLimit.get(klass); long instances = instanceCounts[i]; if (instances > limit) { - Throwable tr = new InstanceCountViolation(klass, instances, limit); - onVmPolicyViolation(tr); + onVmPolicyViolation(new InstanceCountViolation(klass, instances, limit)); } } } @@ -1769,9 +1852,8 @@ public final class StrictMode { * #setThreadPolicy}. */ public static void enableDefaults() { - StrictMode.setThreadPolicy( - new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); - StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); + setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); + setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); } /** @hide */ @@ -1811,24 +1893,22 @@ public final class StrictMode { /** @hide */ public static void onSqliteObjectLeaked(String message, Throwable originStack) { - Throwable t = new Throwable(message); - t.setStackTrace(originStack.getStackTrace()); - onVmPolicyViolation(t); + onVmPolicyViolation(new SqliteObjectLeakedViolation(message, originStack)); } /** @hide */ public static void onWebViewMethodCalledOnWrongThread(Throwable originStack) { - onVmPolicyViolation(originStack); + onVmPolicyViolation(new WebViewMethodCalledOnWrongThreadViolation(originStack)); } /** @hide */ public static void onIntentReceiverLeaked(Throwable originStack) { - onVmPolicyViolation(originStack); + onVmPolicyViolation(new IntentReceiverLeakedViolation(originStack)); } /** @hide */ public static void onServiceConnectionLeaked(Throwable originStack) { - onVmPolicyViolation(originStack); + onVmPolicyViolation(new ServiceConnectionLeakedViolation(originStack)); } /** @hide */ @@ -1837,19 +1917,13 @@ public final class StrictMode { if ((sVmPolicy.mask & PENALTY_DEATH_ON_FILE_URI_EXPOSURE) != 0) { throw new FileUriExposedException(message); } else { - onVmPolicyViolation(new Throwable(message)); + onVmPolicyViolation(new FileUriExposedViolation(message)); } } /** @hide */ public static void onContentUriWithoutPermission(Uri uri, String location) { - final String message = - uri - + " exposed beyond app through " - + location - + " without permission grant flags; did you forget" - + " FLAG_GRANT_READ_URI_PERMISSION?"; - onVmPolicyViolation(new Throwable(message)); + onVmPolicyViolation(new ContentUriWithoutPermissionViolation(uri, location)); } /** @hide */ @@ -1881,33 +1955,28 @@ public final class StrictMode { } msg += HexDump.dumpHexString(firstPacket).trim() + " "; final boolean forceDeath = (sVmPolicy.mask & PENALTY_DEATH_ON_CLEARTEXT_NETWORK) != 0; - onVmPolicyViolation(new Throwable(msg), forceDeath); + onVmPolicyViolation(new CleartextNetworkViolation(msg), forceDeath); } /** @hide */ - public static final String UNTAGGED_SOCKET_VIOLATION_MESSAGE = - "Untagged socket detected; use" - + " TrafficStats.setThreadSocketTag() to track all network usage"; - - /** @hide */ public static void onUntaggedSocket() { - onVmPolicyViolation(new Throwable(UNTAGGED_SOCKET_VIOLATION_MESSAGE)); + onVmPolicyViolation(new UntaggedSocketViolation()); } // Map from VM violation fingerprint to uptime millis. - private static final HashMap<Integer, Long> sLastVmViolationTime = new HashMap<Integer, Long>(); + private static final HashMap<Integer, Long> sLastVmViolationTime = new HashMap<>(); /** @hide */ - public static void onVmPolicyViolation(Throwable originStack) { + public static void onVmPolicyViolation(Violation originStack) { onVmPolicyViolation(originStack, false); } /** @hide */ - public static void onVmPolicyViolation(Throwable originStack, boolean forceDeath) { + public static void onVmPolicyViolation(Violation violation, boolean forceDeath) { final boolean penaltyDropbox = (sVmPolicy.mask & PENALTY_DROPBOX) != 0; final boolean penaltyDeath = ((sVmPolicy.mask & PENALTY_DEATH) != 0) || forceDeath; final boolean penaltyLog = (sVmPolicy.mask & PENALTY_LOG) != 0; - final ViolationInfo info = new ViolationInfo(originStack, sVmPolicy.mask); + final ViolationInfo info = new ViolationInfo(violation, sVmPolicy.mask); // Erase stuff not relevant for process-wide violations info.numAnimationsRunning = 0; @@ -1916,60 +1985,36 @@ public final class StrictMode { final Integer fingerprint = info.hashCode(); final long now = SystemClock.uptimeMillis(); - long lastViolationTime = 0; + long lastViolationTime; long timeSinceLastViolationMillis = Long.MAX_VALUE; synchronized (sLastVmViolationTime) { if (sLastVmViolationTime.containsKey(fingerprint)) { lastViolationTime = sLastVmViolationTime.get(fingerprint); timeSinceLastViolationMillis = now - lastViolationTime; } - if (timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { + if (timeSinceLastViolationMillis > MIN_VM_INTERVAL_MS) { sLastVmViolationTime.put(fingerprint, now); } } - - if (penaltyLog && sLogger != null) { - sLogger.log(info); + if (timeSinceLastViolationMillis <= MIN_VM_INTERVAL_MS) { + // Rate limit all penalties. + return; } - if (penaltyLog && timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { + + if (penaltyLog && sLogger != null && timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { sLogger.log(info); } int violationMaskSubset = PENALTY_DROPBOX | (ALL_VM_DETECT_BITS & sVmPolicy.mask); - if (penaltyDropbox && !penaltyDeath) { - // Common case for userdebug/eng builds. If no death and - // just dropboxing, we can do the ActivityManager call - // asynchronously. - dropboxViolationAsync(violationMaskSubset, info); - return; - } - - if (penaltyDropbox && lastViolationTime == 0) { - // The violationMask, passed to ActivityManager, is a - // subset of the original StrictMode policy bitmask, with - // only the bit violated and penalty bits to be executed - // by the ActivityManagerService remaining set. - final int savedPolicyMask = getThreadPolicyMask(); - try { - // First, remove any policy before we call into the Activity Manager, - // otherwise we'll infinite recurse as we try to log policy violations - // to disk, thus violating policy, thus requiring logging, etc... - // We restore the current policy below, in the finally block. - setThreadPolicyMask(0); - - ActivityManager.getService() - .handleApplicationStrictModeViolation( - RuntimeInit.getApplicationObject(), violationMaskSubset, info); - } catch (RemoteException e) { - if (e instanceof DeadObjectException) { - // System process is dead; ignore - } else { - Log.e(TAG, "RemoteException trying to handle StrictMode violation", e); - } - } finally { - // Restore the policy. - setThreadPolicyMask(savedPolicyMask); + if (penaltyDropbox) { + if (penaltyDeath) { + handleApplicationStrictModeViolation(violationMaskSubset, info); + } else { + // Common case for userdebug/eng builds. If no death and + // just dropboxing, we can do the ActivityManager call + // asynchronously. + dropboxViolationAsync(violationMaskSubset, info); } } @@ -1978,6 +2023,26 @@ public final class StrictMode { Process.killProcess(Process.myPid()); System.exit(10); } + + // If penaltyDeath, we can't guarantee this callback finishes before the process dies for + // all executors. penaltyDeath supersedes penaltyCallback. + if (sVmPolicy.mListener != null && sVmPolicy.mCallbackExecutor != null) { + final OnVmViolationListener listener = sVmPolicy.mListener; + try { + sVmPolicy.mCallbackExecutor.execute( + () -> { + // Lift violated policy to prevent infinite recursion. + VmPolicy oldPolicy = allowVmViolations(); + try { + listener.onVmViolation(violation); + } finally { + setVmPolicy(oldPolicy); + } + }); + } catch (RejectedExecutionException e) { + Log.e(TAG, "VmPolicy penaltyCallback failed", e); + } + } } /** Called from Parcel.writeNoException() */ @@ -1998,14 +2063,12 @@ public final class StrictMode { gatheredViolations.set(null); } - private static class LogStackTrace extends Exception {} - /** * Called from Parcel.readException() when the exception is EX_STRICT_MODE_VIOLATIONS, we here * read back all the encoded violations. */ /* package */ static void readAndHandleBinderCallViolations(Parcel p) { - LogStackTrace localCallSite = new LogStackTrace(); + Throwable localCallSite = new Throwable(); final int policyMask = getThreadPolicyMask(); final boolean currentlyGathering = (policyMask & PENALTY_GATHER) != 0; @@ -2323,8 +2386,7 @@ public final class StrictMode { long instances = VMDebug.countInstancesOfClass(klass, false); if (instances > limit) { - Throwable tr = new InstanceCountViolation(klass, instances, limit); - onVmPolicyViolation(tr); + onVmPolicyViolation(new InstanceCountViolation(klass, instances, limit)); } } @@ -2337,17 +2399,16 @@ public final class StrictMode { @TestApi public static final class ViolationInfo implements Parcelable { /** Stack and violation details. */ - @Nullable private final Throwable mThrowable; + private final Violation mViolation; - private final Deque<Throwable> mBinderStack = new ArrayDeque<>(); + /** Path leading to a violation that occurred across binder. */ + private final Deque<StackTraceElement[]> mBinderStack = new ArrayDeque<>(); /** Memoized stack trace of full violation. */ @Nullable private String mStackTrace; - /** Memoized violation bit. */ - private int mViolationBit; /** The strict mode policy mask at the time of violation. */ - public final int policy; + private final int mPolicy; /** The wall time duration of the violation, when known. -1 when not known. */ public int durationMillis = -1; @@ -2377,17 +2438,11 @@ public final class StrictMode { /** If this is a instance count violation, the number of instances in memory, else -1. */ public long numInstances = -1; - /** Create an uninitialized instance of ViolationInfo */ - public ViolationInfo() { - mThrowable = null; - policy = 0; - } - /** Create an instance of ViolationInfo initialized from an exception. */ - public ViolationInfo(Throwable tr, int policy) { - this.mThrowable = tr; + ViolationInfo(Violation tr, int policy) { + this.mViolation = tr; + this.mPolicy = policy; violationUptimeMillis = SystemClock.uptimeMillis(); - this.policy = policy; this.numAnimationsRunning = ValueAnimator.getCurrentAnimationsCount(); Intent broadcastIntent = ActivityThread.getIntentBeingBroadcast(); if (broadcastIntent != null) { @@ -2395,7 +2450,7 @@ public final class StrictMode { } ThreadSpanState state = sThisThreadSpanState.get(); if (tr instanceof InstanceCountViolation) { - this.numInstances = ((InstanceCountViolation) tr).mInstances; + this.numInstances = ((InstanceCountViolation) tr).getNumberOfInstances(); } synchronized (state) { int spanActiveCount = state.mActiveSize; @@ -2417,13 +2472,17 @@ public final class StrictMode { /** Equivalent output to {@link ApplicationErrorReport.CrashInfo#stackTrace}. */ public String getStackTrace() { - if (mThrowable != null && mStackTrace == null) { + if (mStackTrace == null) { StringWriter sw = new StringWriter(); PrintWriter pw = new FastPrintWriter(sw, false, 256); - mThrowable.printStackTrace(pw); - for (Throwable t : mBinderStack) { + mViolation.printStackTrace(pw); + for (StackTraceElement[] traces : mBinderStack) { pw.append("# via Binder call with stack:\n"); - t.printStackTrace(pw); + for (StackTraceElement traceElement : traces) { + pw.append("\tat "); + pw.append(traceElement.toString()); + pw.append('\n'); + } } pw.flush(); pw.close(); @@ -2439,29 +2498,31 @@ public final class StrictMode { */ @TestApi public String getViolationDetails() { - if (mThrowable != null) { - return mThrowable.getMessage(); - } else { - return ""; - } + return mViolation.getMessage(); } /** - * If this violation has a useful stack trace. + * Policy mask at time of violation. * * @hide */ - public boolean hasStackTrace() { - return mThrowable != null; + @TestApi + public int getPolicyMask() { + return mPolicy; + } + + boolean penaltyEnabled(int p) { + return (mPolicy & p) != 0; } /** - * Add a {@link Throwable} from the current process that caused the underlying violation. + * Add a {@link Throwable} from the current process that caused the underlying violation. We + * only preserve the stack trace elements. * * @hide */ void addLocalStack(Throwable t) { - mBinderStack.addFirst(t); + mBinderStack.addFirst(t.getStackTrace()); } /** @@ -2469,37 +2530,47 @@ public final class StrictMode { * * @hide */ - int getViolationBit() { - if (mThrowable == null || mThrowable.getMessage() == null) { - return 0; - } - if (mViolationBit != 0) { - return mViolationBit; - } - String message = mThrowable.getMessage(); - int violationIndex = message.indexOf("violation="); - if (violationIndex == -1) { - return 0; - } - int numberStartIndex = violationIndex + "violation=".length(); - int numberEndIndex = message.indexOf(' ', numberStartIndex); - if (numberEndIndex == -1) { - numberEndIndex = message.length(); - } - String violationString = message.substring(numberStartIndex, numberEndIndex); - try { - mViolationBit = Integer.parseInt(violationString); - return mViolationBit; - } catch (NumberFormatException e) { - return 0; - } + @TestApi + public int getViolationBit() { + if (mViolation instanceof DiskWriteViolation) { + return DETECT_DISK_WRITE; + } else if (mViolation instanceof DiskReadViolation) { + return DETECT_DISK_READ; + } else if (mViolation instanceof NetworkViolation) { + return DETECT_NETWORK; + } else if (mViolation instanceof CustomViolation) { + return DETECT_CUSTOM; + } else if (mViolation instanceof ResourceMismatchViolation) { + return DETECT_RESOURCE_MISMATCH; + } else if (mViolation instanceof UnbufferedIoViolation) { + return DETECT_UNBUFFERED_IO; + } else if (mViolation instanceof SqliteObjectLeakedViolation) { + return DETECT_VM_CURSOR_LEAKS; + } else if (mViolation instanceof LeakedClosableViolation) { + return DETECT_VM_CLOSABLE_LEAKS; + } else if (mViolation instanceof InstanceCountViolation) { + return DETECT_VM_INSTANCE_LEAKS; + } else if (mViolation instanceof IntentReceiverLeakedViolation) { + return DETECT_VM_REGISTRATION_LEAKS; + } else if (mViolation instanceof ServiceConnectionLeakedViolation) { + return DETECT_VM_REGISTRATION_LEAKS; + } else if (mViolation instanceof FileUriExposedViolation) { + return DETECT_VM_FILE_URI_EXPOSURE; + } else if (mViolation instanceof CleartextNetworkViolation) { + return DETECT_VM_CLEARTEXT_NETWORK; + } else if (mViolation instanceof ContentUriWithoutPermissionViolation) { + return DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION; + } else if (mViolation instanceof UntaggedSocketViolation) { + return DETECT_VM_UNTAGGED_SOCKET; + } + throw new IllegalStateException("missing violation bit"); } @Override public int hashCode() { int result = 17; - if (mThrowable != null) { - result = 37 * result + mThrowable.hashCode(); + if (mViolation != null) { + result = 37 * result + mViolation.hashCode(); } if (numAnimationsRunning != 0) { result *= 37; @@ -2527,16 +2598,26 @@ public final class StrictMode { * should be removed. */ public ViolationInfo(Parcel in, boolean unsetGatheringBit) { - mThrowable = (Throwable) in.readSerializable(); + mViolation = (Violation) in.readSerializable(); int binderStackSize = in.readInt(); for (int i = 0; i < binderStackSize; i++) { - mBinderStack.add((Throwable) in.readSerializable()); + StackTraceElement[] traceElements = new StackTraceElement[in.readInt()]; + for (int j = 0; j < traceElements.length; j++) { + StackTraceElement element = + new StackTraceElement( + in.readString(), + in.readString(), + in.readString(), + in.readInt()); + traceElements[j] = element; + } + mBinderStack.add(traceElements); } int rawPolicy = in.readInt(); if (unsetGatheringBit) { - policy = rawPolicy & ~PENALTY_GATHER; + mPolicy = rawPolicy & ~PENALTY_GATHER; } else { - policy = rawPolicy; + mPolicy = rawPolicy; } durationMillis = in.readInt(); violationNumThisLoop = in.readInt(); @@ -2550,13 +2631,19 @@ public final class StrictMode { /** Save a ViolationInfo instance to a parcel. */ @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeSerializable(mThrowable); + dest.writeSerializable(mViolation); dest.writeInt(mBinderStack.size()); - for (Throwable t : mBinderStack) { - dest.writeSerializable(t); + for (StackTraceElement[] traceElements : mBinderStack) { + dest.writeInt(traceElements.length); + for (StackTraceElement element : traceElements) { + dest.writeString(element.getClassName()); + dest.writeString(element.getMethodName()); + dest.writeString(element.getFileName()); + dest.writeInt(element.getLineNumber()); + } } int start = dest.dataPosition(); - dest.writeInt(policy); + dest.writeInt(mPolicy); dest.writeInt(durationMillis); dest.writeInt(violationNumThisLoop); dest.writeInt(numAnimationsRunning); @@ -2569,7 +2656,7 @@ public final class StrictMode { Slog.d( TAG, "VIO: policy=" - + policy + + mPolicy + " dur=" + durationMillis + " numLoop=" @@ -2588,10 +2675,8 @@ public final class StrictMode { /** Dump a ViolationInfo instance to a Printer. */ public void dump(Printer pw, String prefix) { - if (mThrowable != null) { - pw.println(prefix + "stackTrace: " + getStackTrace()); - } - pw.println(prefix + "policy: " + policy); + pw.println(prefix + "stackTrace: " + getStackTrace()); + pw.println(prefix + "policy: " + mPolicy); if (durationMillis != -1) { pw.println(prefix + "durationMillis: " + durationMillis); } @@ -2635,27 +2720,6 @@ public final class StrictMode { }; } - // Dummy throwable, for now, since we don't know when or where the - // leaked instances came from. We might in the future, but for - // now we suppress the stack trace because it's useless and/or - // misleading. - private static class InstanceCountViolation extends Throwable { - private final long mInstances; - private final int mLimit; - - private static final StackTraceElement[] FAKE_STACK = { - new StackTraceElement( - "android.os.StrictMode", "setClassInstanceLimit", "StrictMode.java", 1) - }; - - public InstanceCountViolation(Class klass, long instances, int limit) { - super(klass.toString() + "; instances=" + instances + "; limit=" + limit); - setStackTrace(FAKE_STACK); - mInstances = instances; - mLimit = limit; - } - } - private static final class InstanceTracker { private static final HashMap<Class<?>, Integer> sInstanceCounts = new HashMap<Class<?>, Integer>(); diff --git a/android/os/SystemClock.java b/android/os/SystemClock.java index b3d76d73..c52c22d6 100644 --- a/android/os/SystemClock.java +++ b/android/os/SystemClock.java @@ -16,12 +16,18 @@ package android.os; +import android.annotation.NonNull; import android.app.IAlarmManager; import android.content.Context; import android.util.Slog; import dalvik.annotation.optimization.CriticalNative; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; + /** * Core timekeeping facilities. * @@ -168,6 +174,31 @@ public final class SystemClock { native public static long uptimeMillis(); /** + * Return {@link Clock} that starts at system boot, not counting time spent + * in deep sleep. + */ + public static @NonNull Clock uptimeMillisClock() { + return new Clock() { + @Override + public ZoneId getZone() { + return ZoneOffset.UTC; + } + @Override + public Clock withZone(ZoneId zone) { + throw new UnsupportedOperationException(); + } + @Override + public long millis() { + return SystemClock.uptimeMillis(); + } + @Override + public Instant instant() { + return Instant.ofEpochMilli(millis()); + } + }; + } + + /** * Returns milliseconds since boot, including time spent in sleep. * * @return elapsed milliseconds since boot. @@ -176,6 +207,31 @@ public final class SystemClock { native public static long elapsedRealtime(); /** + * Return {@link Clock} that starts at system boot, including time spent in + * sleep. + */ + public static @NonNull Clock elapsedRealtimeClock() { + return new Clock() { + @Override + public ZoneId getZone() { + return ZoneOffset.UTC; + } + @Override + public Clock withZone(ZoneId zone) { + throw new UnsupportedOperationException(); + } + @Override + public long millis() { + return SystemClock.elapsedRealtime(); + } + @Override + public Instant instant() { + return Instant.ofEpochMilli(millis()); + } + }; + } + + /** * Returns nanoseconds since boot, including time spent in sleep. * * @return elapsed nanoseconds since boot. diff --git a/android/os/TokenWatcher.java b/android/os/TokenWatcher.java index 9b3a2d68..00333dad 100644 --- a/android/os/TokenWatcher.java +++ b/android/os/TokenWatcher.java @@ -16,17 +16,23 @@ package android.os; +import android.util.Log; + import java.io.PrintWriter; import java.util.ArrayList; -import java.util.WeakHashMap; import java.util.Set; -import android.util.Log; +import java.util.WeakHashMap; /** - * Helper class that helps you use IBinder objects as reference counted - * tokens. IBinders make good tokens because we find out when they are - * removed + * A TokenWatcher watches a collection of {@link IBinder}s. IBinders are added + * to the collection by calling {@link #acquire}, and removed by calling {@link + * #release}. IBinders are also implicitly removed when they become weakly + * reachable. Each IBinder may be added at most once. * + * The {@link #acquired} method is invoked by posting to the specified handler + * whenever the size of the watched collection becomes nonzero. The {@link + * #released} method is invoked on the specified handler whenever the size of + * the watched collection becomes zero. */ public abstract class TokenWatcher { @@ -59,15 +65,23 @@ public abstract class TokenWatcher * Record that this token has been acquired. When acquire is called, and * the current count is 0, the acquired method is called on the given * handler. - * - * @param token An IBinder object. If this token has already been acquired, - * no action is taken. + * + * Note that the same {@code token} can only be acquired once. If this + * {@code token} has already been acquired, no action is taken. The first + * subsequent call to {@link #release} will release this {@code token} + * immediately. + * + * @param token An IBinder object. * @param tag A string used by the {@link #dump} method for debugging, * to see who has references. */ public void acquire(IBinder token, String tag) { synchronized (mTokens) { + if (mTokens.containsKey(token)) { + return; + } + // explicitly checked to avoid bogus sendNotification calls because // of the WeakHashMap and the GC int oldSize = mTokens.size(); diff --git a/android/os/UpdateEngine.java b/android/os/UpdateEngine.java index ee0b6230..c6149bed 100644 --- a/android/os/UpdateEngine.java +++ b/android/os/UpdateEngine.java @@ -67,6 +67,7 @@ public class UpdateEngine { public static final int PAYLOAD_HASH_MISMATCH_ERROR = 10; public static final int PAYLOAD_SIZE_MISMATCH_ERROR = 11; public static final int DOWNLOAD_PAYLOAD_VERIFICATION_ERROR = 12; + public static final int UPDATED_BUT_NOT_ACTIVE = 52; } /** diff --git a/android/os/UserHandle.java b/android/os/UserHandle.java index e8ebf631..6381b56a 100644 --- a/android/os/UserHandle.java +++ b/android/os/UserHandle.java @@ -27,6 +27,8 @@ import java.io.PrintWriter; * Representation of a user on the device. */ public final class UserHandle implements Parcelable { + // NOTE: keep logic in sync with system/core/libcutils/multiuser.c + /** * @hide Range of uids allocated for a user. */ @@ -88,6 +90,19 @@ public final class UserHandle implements Parcelable { */ public static final boolean MU_ENABLED = true; + /** @hide */ + public static final int ERR_GID = -1; + /** @hide */ + public static final int AID_ROOT = android.os.Process.ROOT_UID; + /** @hide */ + public static final int AID_APP_START = android.os.Process.FIRST_APPLICATION_UID; + /** @hide */ + public static final int AID_APP_END = android.os.Process.LAST_APPLICATION_UID; + /** @hide */ + public static final int AID_SHARED_GID_START = android.os.Process.FIRST_SHARED_APPLICATION_GID; + /** @hide */ + public static final int AID_CACHE_GID_START = android.os.Process.FIRST_APPLICATION_CACHE_GID; + final int mHandle; /** @@ -197,13 +212,20 @@ public final class UserHandle implements Parcelable { return getUid(userId, Process.SHARED_USER_GID); } - /** - * Returns the shared app gid for a given uid or appId. - * @hide - */ - public static int getSharedAppGid(int id) { - return Process.FIRST_SHARED_APPLICATION_GID + (id % PER_USER_RANGE) - - Process.FIRST_APPLICATION_UID; + /** @hide */ + public static int getSharedAppGid(int uid) { + return getSharedAppGid(getUserId(uid), getAppId(uid)); + } + + /** @hide */ + public static int getSharedAppGid(int userId, int appId) { + if (appId >= AID_APP_START && appId <= AID_APP_END) { + return (appId - AID_APP_START) + AID_SHARED_GID_START; + } else if (appId >= AID_ROOT && appId <= AID_APP_START) { + return appId; + } else { + return -1; + } } /** @@ -219,13 +241,18 @@ public final class UserHandle implements Parcelable { return appId; } - /** - * Returns the cache GID for a given UID or appId. - * @hide - */ - public static int getCacheAppGid(int id) { - return Process.FIRST_APPLICATION_CACHE_GID + (id % PER_USER_RANGE) - - Process.FIRST_APPLICATION_UID; + /** @hide */ + public static int getCacheAppGid(int uid) { + return getCacheAppGid(getUserId(uid), getAppId(uid)); + } + + /** @hide */ + public static int getCacheAppGid(int userId, int appId) { + if (appId >= AID_APP_START && appId <= AID_APP_END) { + return getUid(userId, (appId - AID_APP_START) + AID_CACHE_GID_START); + } else { + return -1; + } } /** diff --git a/android/os/UserManager.java b/android/os/UserManager.java index c54b72d4..22967af7 100644 --- a/android/os/UserManager.java +++ b/android/os/UserManager.java @@ -140,6 +140,18 @@ public class UserManager { public static final String DISALLOW_CONFIG_WIFI = "no_config_wifi"; /** + * Specifies if a user is disallowed from changing the device + * language. The default value is <code>false</code>. + * + * <p>Key for user restrictions. + * <p>Type: Boolean + * @see DevicePolicyManager#addUserRestriction(ComponentName, String) + * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_LOCALE = "no_config_locale"; + + /** * Specifies if a user is disallowed from installing applications. * The default value is <code>false</code>. * @@ -792,6 +804,19 @@ public class UserManager { public static final String DISALLOW_AUTOFILL = "no_autofill"; /** + * Specifies if user switching is blocked on the current user. + * + * <p> This restriction can only be set by the device owner, it will be applied to all users. + * + * <p>The default value is <code>false</code>. + * + * @see DevicePolicyManager#addUserRestriction(ComponentName, String) + * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_USER_SWITCH = "no_user_switch"; + + /** * Application restriction key that is used to indicate the pending arrival * of real restrictions for the app. * @@ -917,7 +942,7 @@ public class UserManager { /** * Returns whether switching users is currently allowed. * <p>For instance switching users is not allowed if the current user is in a phone call, - * or system user hasn't been unlocked yet + * system user hasn't been unlocked yet, or {@link #DISALLOW_USER_SWITCH} is set. * @hide */ public boolean canSwitchUsers() { @@ -927,7 +952,9 @@ public class UserManager { boolean isSystemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM); boolean inCall = TelephonyManager.getDefault().getCallState() != TelephonyManager.CALL_STATE_IDLE; - return (allowUserSwitchingWhenSystemUserLocked || isSystemUserUnlocked) && !inCall; + boolean isUserSwitchDisallowed = hasUserRestriction(DISALLOW_USER_SWITCH); + return (allowUserSwitchingWhenSystemUserLocked || isSystemUserUnlocked) && !inCall + && !isUserSwitchDisallowed; } /** @@ -1022,12 +1049,22 @@ public class UserManager { } /** - * Used to check if the user making this call is linked to another user. Linked users may have + * @hide + * @deprecated Use {@link #isRestrictedProfile()} + */ + @Deprecated + public boolean isLinkedUser() { + return isRestrictedProfile(); + } + + /** + * Returns whether the caller is running as restricted profile. Restricted profile may have * a reduced number of available apps, app restrictions and account restrictions. * @return whether the user making this call is a linked user * @hide */ - public boolean isLinkedUser() { + @SystemApi + public boolean isRestrictedProfile() { try { return mService.isRestricted(); } catch (RemoteException re) { @@ -1048,6 +1085,20 @@ public class UserManager { } /** + * Returns whether the calling user has at least one restricted profile associated with it. + * @return + * @hide + */ + @SystemApi + public boolean hasRestrictedProfiles() { + try { + return mService.hasRestrictedProfiles(); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Checks if a user is a guest user. * @return whether user is a guest user. * @hide @@ -1067,6 +1118,7 @@ public class UserManager { return user != null && user.isGuest(); } + /** * Checks if the calling app is running in a demo user. When running in a demo user, * apps can be more helpful to the user, or explain their features in more detail. @@ -2298,6 +2350,9 @@ public class UserManager { if (!supportsMultipleUsers()) { return false; } + if (hasUserRestriction(DISALLOW_USER_SWITCH)) { + return false; + } // If Demo Mode is on, don't show user switcher if (isDeviceInDemoMode(mContext)) { return false; diff --git a/android/os/storage/StorageManager.java b/android/os/storage/StorageManager.java index 6594cd07..0b007ddf 100644 --- a/android/os/storage/StorageManager.java +++ b/android/os/storage/StorageManager.java @@ -42,10 +42,12 @@ import android.os.Environment; import android.os.FileUtils; import android.os.Handler; import android.os.IVold; +import android.os.IVoldTaskListener; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.ParcelableException; +import android.os.PersistableBundle; import android.os.ProxyFileDescriptorCallback; import android.os.RemoteException; import android.os.ServiceManager; @@ -87,7 +89,9 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** @@ -884,9 +888,32 @@ public class StorageManager { } /** {@hide} */ + @Deprecated public long benchmark(String volId) { + final CompletableFuture<PersistableBundle> result = new CompletableFuture<>(); + benchmark(volId, new IVoldTaskListener.Stub() { + @Override + public void onStatus(int status, PersistableBundle extras) { + // Ignored + } + + @Override + public void onFinished(int status, PersistableBundle extras) { + result.complete(extras); + } + }); + try { + // Convert ms to ns + return result.get(3, TimeUnit.MINUTES).getLong("run", Long.MAX_VALUE) * 1000000; + } catch (Exception e) { + return Long.MAX_VALUE; + } + } + + /** {@hide} */ + public void benchmark(String volId, IVoldTaskListener listener) { try { - return mStorageManager.benchmark(volId); + mStorageManager.benchmark(volId, listener); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/android/telephony/ims/feature/IRcsFeature.java b/android/os/strictmode/CleartextNetworkViolation.java index e28e1b38..6a0d381d 100644 --- a/android/telephony/ims/feature/IRcsFeature.java +++ b/android/os/strictmode/CleartextNetworkViolation.java @@ -11,16 +11,13 @@ * 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 + * limitations under the License. */ +package android.os.strictmode; -package android.telephony.ims.feature; - -/** - * Feature interface that provides access to RCS APIs. Currently empty until RCS support is added - * in the framework. - * @hide - */ - -public interface IRcsFeature { +public final class CleartextNetworkViolation extends Violation { + /** @hide */ + public CleartextNetworkViolation(String msg) { + super(msg); + } } diff --git a/android/os/strictmode/ContentUriWithoutPermissionViolation.java b/android/os/strictmode/ContentUriWithoutPermissionViolation.java new file mode 100644 index 00000000..e78dc79d --- /dev/null +++ b/android/os/strictmode/ContentUriWithoutPermissionViolation.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +import android.net.Uri; + +public final class ContentUriWithoutPermissionViolation extends Violation { + /** @hide */ + public ContentUriWithoutPermissionViolation(Uri uri, String location) { + super( + uri + + " exposed beyond app through " + + location + + " without permission grant flags; did you forget" + + " FLAG_GRANT_READ_URI_PERMISSION?"); + } +} diff --git a/android/os/strictmode/CustomViolation.java b/android/os/strictmode/CustomViolation.java new file mode 100644 index 00000000..d4ad0671 --- /dev/null +++ b/android/os/strictmode/CustomViolation.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class CustomViolation extends Violation { + /** @hide */ + public CustomViolation(String name) { + super(name); + } +} diff --git a/android/os/strictmode/DiskReadViolation.java b/android/os/strictmode/DiskReadViolation.java new file mode 100644 index 00000000..fad32dbf --- /dev/null +++ b/android/os/strictmode/DiskReadViolation.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class DiskReadViolation extends Violation { + /** @hide */ + public DiskReadViolation() { + super(null); + } +} diff --git a/android/os/strictmode/DiskWriteViolation.java b/android/os/strictmode/DiskWriteViolation.java new file mode 100644 index 00000000..cb9ca381 --- /dev/null +++ b/android/os/strictmode/DiskWriteViolation.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class DiskWriteViolation extends Violation { + /** @hide */ + public DiskWriteViolation() { + super(null); + } +} diff --git a/android/os/strictmode/FileUriExposedViolation.java b/android/os/strictmode/FileUriExposedViolation.java new file mode 100644 index 00000000..e3e6f833 --- /dev/null +++ b/android/os/strictmode/FileUriExposedViolation.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class FileUriExposedViolation extends Violation { + /** @hide */ + public FileUriExposedViolation(String msg) { + super(msg); + } +} diff --git a/android/os/strictmode/InstanceCountViolation.java b/android/os/strictmode/InstanceCountViolation.java new file mode 100644 index 00000000..9ee2c8e5 --- /dev/null +++ b/android/os/strictmode/InstanceCountViolation.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public class InstanceCountViolation extends Violation { + private final long mInstances; + + private static final StackTraceElement[] FAKE_STACK = { + new StackTraceElement( + "android.os.StrictMode", "setClassInstanceLimit", "StrictMode.java", 1) + }; + + /** @hide */ + public InstanceCountViolation(Class klass, long instances, int limit) { + super(klass.toString() + "; instances=" + instances + "; limit=" + limit); + setStackTrace(FAKE_STACK); + mInstances = instances; + } + + public long getNumberOfInstances() { + return mInstances; + } +} diff --git a/android/os/strictmode/IntentReceiverLeakedViolation.java b/android/os/strictmode/IntentReceiverLeakedViolation.java new file mode 100644 index 00000000..f416c940 --- /dev/null +++ b/android/os/strictmode/IntentReceiverLeakedViolation.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class IntentReceiverLeakedViolation extends Violation { + /** @hide */ + public IntentReceiverLeakedViolation(Throwable originStack) { + super(null); + setStackTrace(originStack.getStackTrace()); + } +} diff --git a/android/os/strictmode/LeakedClosableViolation.java b/android/os/strictmode/LeakedClosableViolation.java new file mode 100644 index 00000000..c795a6b8 --- /dev/null +++ b/android/os/strictmode/LeakedClosableViolation.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class LeakedClosableViolation extends Violation { + /** @hide */ + public LeakedClosableViolation(String message, Throwable allocationSite) { + super(message); + initCause(allocationSite); + } +} diff --git a/android/os/strictmode/NetworkViolation.java b/android/os/strictmode/NetworkViolation.java new file mode 100644 index 00000000..abcf009d --- /dev/null +++ b/android/os/strictmode/NetworkViolation.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class NetworkViolation extends Violation { + /** @hide */ + public NetworkViolation() { + super(null); + } +} diff --git a/android/os/strictmode/ResourceMismatchViolation.java b/android/os/strictmode/ResourceMismatchViolation.java new file mode 100644 index 00000000..97c44993 --- /dev/null +++ b/android/os/strictmode/ResourceMismatchViolation.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class ResourceMismatchViolation extends Violation { + /** @hide */ + public ResourceMismatchViolation(Object tag) { + super(tag.toString()); + } +} diff --git a/android/os/strictmode/ServiceConnectionLeakedViolation.java b/android/os/strictmode/ServiceConnectionLeakedViolation.java new file mode 100644 index 00000000..2d6b58f0 --- /dev/null +++ b/android/os/strictmode/ServiceConnectionLeakedViolation.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class ServiceConnectionLeakedViolation extends Violation { + /** @hide */ + public ServiceConnectionLeakedViolation(Throwable originStack) { + super(null); + setStackTrace(originStack.getStackTrace()); + } +} diff --git a/android/os/strictmode/SqliteObjectLeakedViolation.java b/android/os/strictmode/SqliteObjectLeakedViolation.java new file mode 100644 index 00000000..02002207 --- /dev/null +++ b/android/os/strictmode/SqliteObjectLeakedViolation.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class SqliteObjectLeakedViolation extends Violation { + + /** @hide */ + public SqliteObjectLeakedViolation(String message, Throwable originStack) { + super(message); + initCause(originStack); + } +} diff --git a/android/os/strictmode/UnbufferedIoViolation.java b/android/os/strictmode/UnbufferedIoViolation.java new file mode 100644 index 00000000..a5c326d1 --- /dev/null +++ b/android/os/strictmode/UnbufferedIoViolation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017 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 android.os.strictmode; + +import android.os.StrictMode.ThreadPolicy.Builder; + +/** + * See #{@link Builder#detectUnbufferedIo()} + */ +public final class UnbufferedIoViolation extends Violation { + /** @hide */ + public UnbufferedIoViolation() { + super(null); + } +} diff --git a/android/os/strictmode/UntaggedSocketViolation.java b/android/os/strictmode/UntaggedSocketViolation.java new file mode 100644 index 00000000..836a8b9d --- /dev/null +++ b/android/os/strictmode/UntaggedSocketViolation.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class UntaggedSocketViolation extends Violation { + /** @hide */ + public static final String MESSAGE = + "Untagged socket detected; use" + + " TrafficStats.setThreadSocketTag() to track all network usage"; + + /** @hide */ + public UntaggedSocketViolation() { + super(MESSAGE); + } +} diff --git a/android/os/strictmode/Violation.java b/android/os/strictmode/Violation.java new file mode 100644 index 00000000..31c7d584 --- /dev/null +++ b/android/os/strictmode/Violation.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +/** Root class for all StrictMode violations. */ +public abstract class Violation extends Throwable { + Violation(String message) { + super(message); + } +} diff --git a/android/os/strictmode/WebViewMethodCalledOnWrongThreadViolation.java b/android/os/strictmode/WebViewMethodCalledOnWrongThreadViolation.java new file mode 100644 index 00000000..c328d147 --- /dev/null +++ b/android/os/strictmode/WebViewMethodCalledOnWrongThreadViolation.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 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 android.os.strictmode; + +public final class WebViewMethodCalledOnWrongThreadViolation extends Violation { + /** @hide */ + public WebViewMethodCalledOnWrongThreadViolation(Throwable originStack) { + super(null); + setStackTrace(originStack.getStackTrace()); + } +} diff --git a/android/preference/PreferenceFragment.java b/android/preference/PreferenceFragment.java index 73fa01e5..4c556efa 100644 --- a/android/preference/PreferenceFragment.java +++ b/android/preference/PreferenceFragment.java @@ -23,7 +23,6 @@ import android.app.Fragment; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -105,7 +104,10 @@ import android.widget.TextView; * * @see Preference * @see PreferenceScreen + * + * @deprecated Use {@link android.support.v7.preference.PreferenceFragmentCompat} */ +@Deprecated public abstract class PreferenceFragment extends Fragment implements PreferenceManager.OnPreferenceTreeClickListener { @@ -146,7 +148,11 @@ public abstract class PreferenceFragment extends Fragment implements * Interface that PreferenceFragment's containing activity should * implement to be able to process preference items that wish to * switch to a new fragment. + * + * @deprecated Use {@link + * android.support.v7.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback} */ + @Deprecated public interface OnPreferenceStartFragmentCallback { /** * Called when the user has clicked on a Preference that has diff --git a/android/provider/MediaStore.java b/android/provider/MediaStore.java index 13e1e26b..32d68cd9 100644 --- a/android/provider/MediaStore.java +++ b/android/provider/MediaStore.java @@ -81,6 +81,13 @@ public final class MediaStore { public static final String UNHIDE_CALL = "unhide"; /** + * The method name used by the media scanner service to reload all localized ringtone titles due + * to a locale change. + * @hide + */ + public static final String RETRANSLATE_CALL = "update_titles"; + + /** * This is for internal use by the media scanner only. * Name of the (optional) Uri parameter that determines whether to skip deleting * the file pointed to by the _data column, when deleting the database entry. @@ -1358,6 +1365,18 @@ public final class MediaStore { * @hide */ public static final String GENRE = "genre"; + + /** + * The resource URI of a localized title, if any + * <P>Type: TEXT</P> + * Conforms to this pattern: + * Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE} + * Authority: Package Name of ringtone title provider + * First Path Segment: Type of resource (must be "string") + * Second Path Segment: Resource ID of title + * @hide + */ + public static final String TITLE_RESOURCE_URI = "title_resource_uri"; } /** diff --git a/android/provider/Settings.java b/android/provider/Settings.java index 62f4bf58..6decc305 100644 --- a/android/provider/Settings.java +++ b/android/provider/Settings.java @@ -66,12 +66,14 @@ import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.UserHandle; import android.speech.tts.TextToSpeech; +import android.telephony.SubscriptionManager; import android.text.TextUtils; import android.util.AndroidException; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.MemoryIntArray; +import android.util.StatsLog; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; @@ -210,8 +212,13 @@ public final class Settings { /** @hide */ public static final String EXTRA_NETWORK_TEMPLATE = "network_template"; - /** @hide */ - public static final String EXTRA_SUB_ID = "sub_id"; + + /** + * An int extra specifying a subscription ID. + * + * @see android.telephony.SubscriptionInfo#getSubscriptionId + */ + public static final String EXTRA_SUB_ID = "android.provider.extra.SUB_ID"; /** * Activity Action: Modify Airplane mode settings using a voice command. @@ -915,6 +922,9 @@ public final class Settings { * In some cases, a matching Activity may not exist, so ensure you * safeguard against this. * <p> + * The subscription ID of the subscription for which available network operators should be + * displayed may be optionally specified with {@link #EXTRA_SUB_ID}. + * <p> * Input: Nothing. * <p> * Output: Nothing. @@ -1886,7 +1896,11 @@ public final class Settings { arg.putBoolean(CALL_METHOD_MAKE_DEFAULT_KEY, true); } IContentProvider cp = mProviderHolder.getProvider(cr); + String prevValue = getStringForUser(cr, name, userHandle); cp.call(cr.getPackageName(), mCallSetCommand, name, arg); + String newValue = getStringForUser(cr, name, userHandle); + StatsLog.write(StatsLog.SETTING_CHANGED, name, value, newValue, prevValue, tag, + makeDefault ? 1 : 0, userHandle); } catch (RemoteException e) { Log.w(TAG, "Can't set key " + name + " in " + mUri, e); return false; @@ -2100,6 +2114,9 @@ public final class Settings { * functions for accessing individual settings entries. */ public static final class System extends NameValueTable { + // NOTE: If you add new settings here, be sure to add them to + // com.android.providers.settings.SettingsProtoDumpUtil#dumpProtoSystemSettingsLocked. + private static final float DEFAULT_FONT_SCALE = 1.0f; /** @hide */ @@ -4549,6 +4566,9 @@ public final class Settings { * APIs for those values, not modified directly by applications. */ public static final class Secure extends NameValueTable { + // NOTE: If you add new settings here, be sure to add them to + // com.android.providers.settings.SettingsProtoDumpUtil#dumpProtoSecureSettingsLocked. + /** * The content:// style URL for this table */ @@ -5304,6 +5324,15 @@ public final class Settings { public static final String AUTOFILL_SERVICE = "autofill_service"; /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): remove once feature is finished + * @hide + */ + @TestApi + public static final String AUTOFILL_FEATURE_FIELD_DETECTION = "autofill_field_detection"; + + /** * @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead */ @Deprecated @@ -7542,6 +7571,9 @@ public final class Settings { * explicitly modify through the system UI or specialized APIs for those values. */ public static final class Global extends NameValueTable { + // NOTE: If you add new settings here, be sure to add them to + // com.android.providers.settings.SettingsProtoDumpUtil#dumpProtoGlobalSettingsLocked. + /** * The content:// style URL for global secure settings items. Not public. */ @@ -8007,28 +8039,40 @@ public final class Settings { public static final String HDMI_SYSTEM_AUDIO_CONTROL_ENABLED = "hdmi_system_audio_control_enabled"; - /** - * Whether TV will automatically turn on upon reception of the CEC command - * <Text View On> or <Image View On>. (0 = false, 1 = true) - * @hide - */ - public static final String HDMI_CONTROL_AUTO_WAKEUP_ENABLED = - "hdmi_control_auto_wakeup_enabled"; + /** + * Whether TV will automatically turn on upon reception of the CEC command + * <Text View On> or <Image View On>. (0 = false, 1 = true) + * + * @hide + */ + public static final String HDMI_CONTROL_AUTO_WAKEUP_ENABLED = + "hdmi_control_auto_wakeup_enabled"; - /** - * Whether TV will also turn off other CEC devices when it goes to standby mode. - * (0 = false, 1 = true) - * @hide - */ - public static final String HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED = - "hdmi_control_auto_device_off_enabled"; + /** + * Whether TV will also turn off other CEC devices when it goes to standby mode. + * (0 = false, 1 = true) + * + * @hide + */ + public static final String HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED = + "hdmi_control_auto_device_off_enabled"; - /** - * The interval in milliseconds at which location requests will be throttled when they are - * coming from the background. - * @hide - */ - public static final String LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS = + /** + * If <b>true</b>, enables out-of-the-box execution for priv apps. + * Default: false + * Values: 0 = false, 1 = true + * + * @hide + */ + public static final String PRIV_APP_OOB_ENABLED = "priv_app_oob_enabled"; + + /** + * The interval in milliseconds at which location requests will be throttled when they are + * coming from the background. + * + * @hide + */ + public static final String LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS = "location_background_throttle_interval_ms"; /** @@ -8488,6 +8532,13 @@ public final class Settings { public static final String NETWORK_METERED_MULTIPATH_PREFERENCE = "network_metered_multipath_preference"; + /** + * Network watchlist last report time. + * @hide + */ + public static final String NETWORK_WATCHLIST_LAST_REPORT_TIME = + "network_watchlist_last_report_time"; + /** * The thresholds of the wifi throughput badging (SD, HD etc.) as a comma-delimited list of * colon-delimited key-value pairs. The key is the badging enum value defined in @@ -9280,11 +9331,20 @@ public final class Settings { public static final String DEFAULT_DNS_SERVER = "default_dns_server"; /** - * Whether to disable DNS over TLS (boolean) + * The requested Private DNS mode (string), and an accompanying specifier (string). + * + * Currently, the specifier holds the chosen provider name when the mode requests + * a specific provider. It may be used to store the provider name even when the + * mode changes so that temporarily disabling and re-enabling the specific + * provider mode does not necessitate retyping the provider hostname. * * @hide */ - public static final String DNS_TLS_DISABLED = "dns_tls_disabled"; + public static final String PRIVATE_DNS_MODE = "private_dns_mode"; + /** + * @hide + */ + public static final String PRIVATE_DNS_SPECIFIER = "private_dns_specifier"; /** {@hide} */ public static final String @@ -9418,6 +9478,16 @@ public final class Settings { public static final String BATTERY_SAVER_CONSTANTS = "battery_saver_constants"; /** + * Battery Saver device specific settings + * This is encoded as a key=value list, separated by commas. + * See {@link com.android.server.power.BatterySaverPolicy} for the details. + * + * @hide + */ + public static final String BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS = + "battery_saver_device_specific_constants"; + + /** * Battery anomaly detection specific settings * This is encoded as a key=value list, separated by commas. * wakeup_blacklisted_tags is a string, encoded as a set of tags, encoded via @@ -10094,12 +10164,17 @@ public final class Settings { public static final String REQUIRE_PASSWORD_TO_DECRYPT = "require_password_to_decrypt"; /** - * Whether the Volte is enabled + * Whether the Volte is enabled. If this setting is not set then we use the Carrier Config + * value {@link CarrierConfigManager#KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL}. * <p> * Type: int (0 for false, 1 for true) * @hide + * @deprecated Use {@link android.telephony.SubscriptionManager#ENHANCED_4G_MODE_ENABLED} + * instead. */ - public static final String ENHANCED_4G_MODE_ENABLED = "volte_vt_enabled"; + @Deprecated + public static final String ENHANCED_4G_MODE_ENABLED = + SubscriptionManager.ENHANCED_4G_MODE_ENABLED; /** * Whether VT (Video Telephony over IMS) is enabled @@ -10107,8 +10182,10 @@ public final class Settings { * Type: int (0 for false, 1 for true) * * @hide + * @deprecated Use {@link android.telephony.SubscriptionManager#VT_IMS_ENABLED} instead. */ - public static final String VT_IMS_ENABLED = "vt_ims_enabled"; + @Deprecated + public static final String VT_IMS_ENABLED = SubscriptionManager.VT_IMS_ENABLED; /** * Whether WFC is enabled @@ -10116,8 +10193,10 @@ public final class Settings { * Type: int (0 for false, 1 for true) * * @hide + * @deprecated Use {@link android.telephony.SubscriptionManager#WFC_IMS_ENABLED} instead. */ - public static final String WFC_IMS_ENABLED = "wfc_ims_enabled"; + @Deprecated + public static final String WFC_IMS_ENABLED = SubscriptionManager.WFC_IMS_ENABLED; /** * WFC mode on home/non-roaming network. @@ -10125,8 +10204,10 @@ public final class Settings { * Type: int - 2=Wi-Fi preferred, 1=Cellular preferred, 0=Wi-Fi only * * @hide + * @deprecated Use {@link android.telephony.SubscriptionManager#WFC_IMS_MODE} instead. */ - public static final String WFC_IMS_MODE = "wfc_ims_mode"; + @Deprecated + public static final String WFC_IMS_MODE = SubscriptionManager.WFC_IMS_MODE; /** * WFC mode on roaming network. @@ -10134,8 +10215,11 @@ public final class Settings { * Type: int - see {@link #WFC_IMS_MODE} for values * * @hide + * @deprecated Use {@link android.telephony.SubscriptionManager#WFC_IMS_ROAMING_MODE} + * instead. */ - public static final String WFC_IMS_ROAMING_MODE = "wfc_ims_roaming_mode"; + @Deprecated + public static final String WFC_IMS_ROAMING_MODE = SubscriptionManager.WFC_IMS_ROAMING_MODE; /** * Whether WFC roaming is enabled @@ -10143,8 +10227,12 @@ public final class Settings { * Type: int (0 for false, 1 for true) * * @hide + * @deprecated Use {@link android.telephony.SubscriptionManager#WFC_IMS_ROAMING_ENABLED} + * instead */ - public static final String WFC_IMS_ROAMING_ENABLED = "wfc_ims_roaming_enabled"; + @Deprecated + public static final String WFC_IMS_ROAMING_ENABLED = + SubscriptionManager.WFC_IMS_ROAMING_ENABLED; /** * Whether user can enable/disable LTE as a preferred network. A carrier might control @@ -10370,7 +10458,9 @@ public final class Settings { DOCK_AUDIO_MEDIA_ENABLED, ENCODED_SURROUND_OUTPUT, LOW_POWER_MODE_TRIGGER_LEVEL, - BLUETOOTH_ON + BLUETOOTH_ON, + PRIVATE_DNS_MODE, + PRIVATE_DNS_SPECIFIER }; /** @hide */ @@ -10823,7 +10913,7 @@ public final class Settings { /** User preferred subscriptions setting. * This holds the details of the user selected subscription from the card and - * the activation status. Each settings string have the coma separated values + * the activation status. Each settings string have the comma separated values * iccId,appType,appId,activationStatus,3gppIndex,3gpp2Index * @hide */ diff --git a/android/provider/Telephony.java b/android/provider/Telephony.java index 216d28cb..d7b6142a 100644 --- a/android/provider/Telephony.java +++ b/android/provider/Telephony.java @@ -2828,6 +2828,26 @@ public final class Telephony { * @hide */ public static final int CARRIER_DELETED_BUT_PRESENT_IN_XML = 6; + + /** + * The owner of the APN. + * <p>Type: INTEGER</p> + * @hide + */ + public static final String OWNED_BY = "owned_by"; + + /** + * Possible value for the OWNED_BY field. + * APN is owned by DPC. + * @hide + */ + public static final int OWNED_BY_DPC = 0; + /** + * Possible value for the OWNED_BY field. + * APN is owned by other sources. + * @hide + */ + public static final int OWNED_BY_OTHERS = 1; } /** @@ -3273,4 +3293,69 @@ public final class Telephony { */ public static final String IS_USING_CARRIER_AGGREGATION = "is_using_carrier_aggregation"; } + + /** + * Contains carrier identification information. + * @hide + */ + public static final class CarrierIdentification implements BaseColumns { + /** + * Numeric operator ID (as String). {@code MCC + MNC} + * <P>Type: TEXT </P> + */ + public static final String MCCMNC = "mccmnc"; + + /** + * Group id level 1 (as String). + * <P>Type: TEXT </P> + */ + public static final String GID1 = "gid1"; + + /** + * Group id level 2 (as String). + * <P>Type: TEXT </P> + */ + public static final String GID2 = "gid2"; + + /** + * Public Land Mobile Network name. + * <P>Type: TEXT </P> + */ + public static final String PLMN = "plmn"; + + /** + * Prefix xpattern of IMSI (International Mobile Subscriber Identity). + * <P>Type: TEXT </P> + */ + public static final String IMSI_PREFIX_XPATTERN = "imsi_prefix_xpattern"; + + /** + * Service Provider Name. + * <P>Type: TEXT </P> + */ + public static final String SPN = "spn"; + + /** + * Prefer APN name. + * <P>Type: TEXT </P> + */ + public static final String APN = "apn"; + + /** + * User facing carrier name. + * <P>Type: TEXT </P> + */ + public static final String NAME = "carrier_name"; + + /** + * A unique carrier id + * <P>Type: INTEGER </P> + */ + public static final String CID = "carrier_id"; + + /** + * The {@code content://} URI for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://carrier_identification"); + } } diff --git a/android/security/KeyStore.java b/android/security/KeyStore.java index 7e959a87..399dddd7 100644 --- a/android/security/KeyStore.java +++ b/android/security/KeyStore.java @@ -20,6 +20,7 @@ import android.app.ActivityThread; import android.app.Application; import android.app.KeyguardManager; import android.content.Context; +import android.content.pm.PackageManager; import android.hardware.fingerprint.FingerprintManager; import android.os.Binder; import android.os.IBinder; @@ -53,7 +54,7 @@ import java.util.Locale; public class KeyStore { private static final String TAG = "KeyStore"; - // ResponseCodes + // ResponseCodes - see system/security/keystore/include/keystore/keystore.h public static final int NO_ERROR = 1; public static final int LOCKED = 2; public static final int UNINITIALIZED = 3; @@ -167,10 +168,14 @@ public class KeyStore { public byte[] get(String key, int uid) { try { + key = key != null ? key : ""; return mBinder.get(key, uid); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return null; + } catch (android.os.ServiceSpecificException e) { + Log.w(TAG, "KeyStore exception", e); + return null; } } @@ -184,6 +189,9 @@ public class KeyStore { public int insert(String key, byte[] value, int uid, int flags) { try { + if (value == null) { + value = new byte[0]; + } return mBinder.insert(key, value, uid, flags); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -227,6 +235,9 @@ public class KeyStore { } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return null; + } catch (android.os.ServiceSpecificException e) { + Log.w(TAG, "KeyStore exception", e); + return null; } } @@ -275,6 +286,7 @@ public class KeyStore { */ public boolean unlock(int userId, String password) { try { + password = password != null ? password : ""; mError = mBinder.unlock(userId, password); return mError == NO_ERROR; } catch (RemoteException e) { @@ -329,16 +341,25 @@ public class KeyStore { } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return null; + } catch (android.os.ServiceSpecificException e) { + Log.w(TAG, "KeyStore exception", e); + return null; } + } public boolean verify(String key, byte[] data, byte[] signature) { try { + signature = signature != null ? signature : new byte[0]; return mBinder.verify(key, data, signature) == NO_ERROR; } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); return false; + } catch (android.os.ServiceSpecificException e) { + Log.w(TAG, "KeyStore exception", e); + return false; } + } public String grant(String key, int uid) { @@ -431,6 +452,8 @@ public class KeyStore { public int generateKey(String alias, KeymasterArguments args, byte[] entropy, int uid, int flags, KeyCharacteristics outCharacteristics) { try { + entropy = entropy != null ? entropy : new byte[0]; + args = args != null ? args : new KeymasterArguments(); return mBinder.generateKey(alias, args, entropy, uid, flags, outCharacteristics); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -446,6 +469,8 @@ public class KeyStore { public int getKeyCharacteristics(String alias, KeymasterBlob clientId, KeymasterBlob appId, int uid, KeyCharacteristics outCharacteristics) { try { + clientId = clientId != null ? clientId : new KeymasterBlob(new byte[0]); + appId = appId != null ? appId : new KeymasterBlob(new byte[0]); return mBinder.getKeyCharacteristics(alias, clientId, appId, uid, outCharacteristics); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -477,6 +502,8 @@ public class KeyStore { public ExportResult exportKey(String alias, int format, KeymasterBlob clientId, KeymasterBlob appId, int uid) { try { + clientId = clientId != null ? clientId : new KeymasterBlob(new byte[0]); + appId = appId != null ? appId : new KeymasterBlob(new byte[0]); return mBinder.exportKey(alias, format, clientId, appId, uid); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -491,6 +518,8 @@ public class KeyStore { public OperationResult begin(String alias, int purpose, boolean pruneable, KeymasterArguments args, byte[] entropy, int uid) { try { + args = args != null ? args : new KeymasterArguments(); + entropy = entropy != null ? entropy : new byte[0]; return mBinder.begin(getToken(), alias, purpose, pruneable, args, entropy, uid); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -500,11 +529,15 @@ public class KeyStore { public OperationResult begin(String alias, int purpose, boolean pruneable, KeymasterArguments args, byte[] entropy) { + entropy = entropy != null ? entropy : new byte[0]; + args = args != null ? args : new KeymasterArguments(); return begin(alias, purpose, pruneable, args, entropy, UID_SELF); } public OperationResult update(IBinder token, KeymasterArguments arguments, byte[] input) { try { + arguments = arguments != null ? arguments : new KeymasterArguments(); + input = input != null ? input : new byte[0]; return mBinder.update(token, arguments, input); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -515,6 +548,9 @@ public class KeyStore { public OperationResult finish(IBinder token, KeymasterArguments arguments, byte[] signature, byte[] entropy) { try { + arguments = arguments != null ? arguments : new KeymasterArguments(); + entropy = entropy != null ? entropy : new byte[0]; + signature = signature != null ? signature : new byte[0]; return mBinder.finish(token, arguments, signature, entropy); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -631,6 +667,12 @@ public class KeyStore { public int attestKey( String alias, KeymasterArguments params, KeymasterCertificateChain outChain) { try { + if (params == null) { + params = new KeymasterArguments(); + } + if (outChain == null) { + outChain = new KeymasterCertificateChain(); + } return mBinder.attestKey(alias, params, outChain); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -640,6 +682,12 @@ public class KeyStore { public int attestDeviceIds(KeymasterArguments params, KeymasterCertificateChain outChain) { try { + if (params == null) { + params = new KeymasterArguments(); + } + if (outChain == null) { + outChain = new KeymasterCertificateChain(); + } return mBinder.attestDeviceIds(params, outChain); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to keystore", e); @@ -762,6 +810,10 @@ public class KeyStore { } private long getFingerprintOnlySid() { + final PackageManager packageManager = mContext.getPackageManager(); + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + return 0; + } FingerprintManager fingerprintManager = mContext.getSystemService(FingerprintManager.class); if (fingerprintManager == null) { return 0; diff --git a/android/service/autofill/FieldsDetection.java b/android/service/autofill/FieldsDetection.java new file mode 100644 index 00000000..550ecf68 --- /dev/null +++ b/android/service/autofill/FieldsDetection.java @@ -0,0 +1,127 @@ +/* + * Copyright 2017 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 android.service.autofill; + +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.autofill.AutofillId; + +/** + * Class by service to improve autofillable fields detection by tracking the meaning of fields + * manually edited by the user (when they match values provided by the service). + * + * TODO(b/67867469): + * - proper javadoc + * - unhide / remove testApi + * - add FieldsDetection management so service can set it just once and reference it in further + * calls to improve performance (and also API to refresh it) + * - rename to FieldsDetectionInfo or FieldClassification? (same for CTS tests) + * - add FieldsDetectionUnitTest once API is well-defined + * @hide + */ +@TestApi +public final class FieldsDetection implements Parcelable { + + private final AutofillId mFieldId; + private final String mRemoteId; + private final String mValue; + + /** + * Creates a field detection for just one field / value pair. + * + * @param fieldId autofill id of the field in the screen. + * @param remoteId id used by the service to identify the field later. + * @param value field value known to the service. + * + * TODO(b/67867469): + * - proper javadoc + * - change signature to allow more fields / values / match methods + * - might also need to use a builder, where the constructor is the id for the fieldsdetector + * - might need id for values as well + * - add @NonNull / check it / add unit tests + * - make 'value' input more generic so it can accept distance-based match and other matches + * - throw exception if field value is less than X characters (somewhere between 7-10) + * - make sure to limit total number of fields to around 10 or so + * - use AutofillValue instead of String (so it can compare dates, for example) + */ + public FieldsDetection(AutofillId fieldId, String remoteId, String value) { + mFieldId = fieldId; + mRemoteId = remoteId; + mValue = value; + } + + /** @hide */ + public AutofillId getFieldId() { + return mFieldId; + } + + /** @hide */ + public String getRemoteId() { + return mRemoteId; + } + + /** @hide */ + public String getValue() { + return mValue; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + // Cannot disclose remoteId or value because they could contain PII + return new StringBuilder("FieldsDetection: [field=").append(mFieldId) + .append(", remoteId_length=").append(mRemoteId.length()) + .append(", value_length=").append(mValue.length()) + .append("]").toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mFieldId, flags); + parcel.writeString(mRemoteId); + parcel.writeString(mValue); + } + + public static final Parcelable.Creator<FieldsDetection> CREATOR = + new Parcelable.Creator<FieldsDetection>() { + @Override + public FieldsDetection createFromParcel(Parcel parcel) { + // TODO(b/67867469): remove comment below if it does not use a builder at the end + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + return new FieldsDetection(parcel.readParcelable(null), parcel.readString(), + parcel.readString()); + } + + @Override + public FieldsDetection[] newArray(int size) { + return new FieldsDetection[size]; + } + }; +} diff --git a/android/service/autofill/FillEventHistory.java b/android/service/autofill/FillEventHistory.java index b1857b30..736d9ef4 100644 --- a/android/service/autofill/FillEventHistory.java +++ b/android/service/autofill/FillEventHistory.java @@ -19,6 +19,7 @@ package android.service.autofill; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.content.IntentSender; import android.os.Bundle; import android.os.Parcel; @@ -164,6 +165,10 @@ public final class FillEventHistory implements Parcelable { dest.writeStringList(event.mManuallyFilledDatasetIds.get(j)); } } + dest.writeString(event.mDetectedRemoteId); + if (event.mDetectedRemoteId != null) { + dest.writeInt(event.mDetectedFieldScore); + } } } } @@ -226,6 +231,7 @@ public final class FillEventHistory implements Parcelable { * <p>See {@link android.view.autofill.AutofillManager} for more information about autofill * contexts. */ + // TODO(b/67867469): update with field detection behavior public static final int TYPE_CONTEXT_COMMITTED = 4; /** @hide */ @@ -253,6 +259,9 @@ public final class FillEventHistory implements Parcelable { @Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds; @Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds; + @Nullable private final String mDetectedRemoteId; + private final int mDetectedFieldScore; + /** * Returns the type of the event. * @@ -355,6 +364,39 @@ public final class FillEventHistory implements Parcelable { } /** + * Gets the results of the last {@link FieldsDetection} request. + * + * @return map of edit-distance match ({@code 0} means full match, + * {@code 1} means 1 character different, etc...) by remote id (as set in the + * {@link FieldsDetection} constructor), or {@code null} if none of the user-input values + * matched the requested detection. + * + * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the + * service requested {@link FillResponse.Builder#setFieldsDetection(FieldsDetection) fields + * detection}. + * + * TODO(b/67867469): + * - improve javadoc + * - refine score meaning (for example, should 1 be different of -1?) + * - mention when it's set + * - unhide + * - unhide / remove testApi + * - add @NonNull / check it / add unit tests + * + * @hide + */ + @TestApi + @NonNull public Map<String, Integer> getDetectedFields() { + if (mDetectedRemoteId == null || mDetectedFieldScore == -1) { + return Collections.emptyMap(); + } + + final ArrayMap<String, Integer> map = new ArrayMap<>(1); + map.put(mDetectedRemoteId, mDetectedFieldScore); + return map; + } + + /** * Returns which fields were available on datasets provided by the service but manually * entered by the user. * @@ -430,7 +472,6 @@ public final class FillEventHistory implements Parcelable { * and belonged to datasets. * @param manuallyFilledDatasetIds The ids of datasets that had values matching the * respective entry on {@code manuallyFilledFieldIds}. - * * @throws IllegalArgumentException If the length of {@code changedFieldIds} and * {@code changedDatasetIds} doesn't match. * @throws IllegalArgumentException If the length of {@code manuallyFilledFieldIds} and @@ -438,13 +479,15 @@ public final class FillEventHistory implements Parcelable { * * @hide */ + // TODO(b/67867469): document detection field parameters once stable public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState, @Nullable List<String> selectedDatasetIds, @Nullable ArraySet<String> ignoredDatasetIds, @Nullable ArrayList<AutofillId> changedFieldIds, @Nullable ArrayList<String> changedDatasetIds, @Nullable ArrayList<AutofillId> manuallyFilledFieldIds, - @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds) { + @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds, + @Nullable String detectedRemoteId, int detectedFieldScore) { mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED, "eventType"); mDatasetId = datasetId; @@ -467,6 +510,8 @@ public final class FillEventHistory implements Parcelable { } mManuallyFilledFieldIds = manuallyFilledFieldIds; mManuallyFilledDatasetIds = manuallyFilledDatasetIds; + mDetectedRemoteId = detectedRemoteId; + mDetectedFieldScore = detectedFieldScore; } @Override @@ -479,6 +524,8 @@ public final class FillEventHistory implements Parcelable { + ", changedDatasetsIds=" + mChangedDatasetIds + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds + + ", detectedRemoteId=" + mDetectedRemoteId + + ", detectedFieldScore=" + mDetectedFieldScore + "]"; } } @@ -514,11 +561,15 @@ public final class FillEventHistory implements Parcelable { } else { manuallyFilledDatasetIds = null; } + final String detectedRemoteId = parcel.readString(); + final int detectedFieldScore = detectedRemoteId == null ? -1 + : parcel.readInt(); selection.addEvent(new Event(eventType, datasetId, clientState, selectedDatasetIds, ignoredDatasets, changedFieldIds, changedDatasetIds, - manuallyFilledFieldIds, manuallyFilledDatasetIds)); + manuallyFilledFieldIds, manuallyFilledDatasetIds, + detectedRemoteId, detectedFieldScore)); } return selection; } diff --git a/android/service/autofill/FillResponse.java b/android/service/autofill/FillResponse.java index 2f6342af..4e6a8845 100644 --- a/android/service/autofill/FillResponse.java +++ b/android/service/autofill/FillResponse.java @@ -22,6 +22,7 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.Activity; import android.content.IntentSender; import android.content.pm.ParceledListSlice; @@ -75,6 +76,7 @@ public final class FillResponse implements Parcelable { private final @Nullable AutofillId[] mAuthenticationIds; private final @Nullable AutofillId[] mIgnoredIds; private final long mDisableDuration; + private final @Nullable FieldsDetection mFieldsDetection; private final int mFlags; private int mRequestId; @@ -87,6 +89,7 @@ public final class FillResponse implements Parcelable { mAuthenticationIds = builder.mAuthenticationIds; mIgnoredIds = builder.mIgnoredIds; mDisableDuration = builder.mDisableDuration; + mFieldsDetection = builder.mFieldsDetection; mFlags = builder.mFlags; mRequestId = INVALID_REQUEST_ID; } @@ -132,6 +135,11 @@ public final class FillResponse implements Parcelable { } /** @hide */ + public @Nullable FieldsDetection getFieldsDetection() { + return mFieldsDetection; + } + + /** @hide */ public int getFlags() { return mFlags; } @@ -167,6 +175,7 @@ public final class FillResponse implements Parcelable { private AutofillId[] mAuthenticationIds; private AutofillId[] mIgnoredIds; private long mDisableDuration; + private FieldsDetection mFieldsDetection; private int mFlags; private boolean mDestroyed; @@ -315,6 +324,25 @@ public final class FillResponse implements Parcelable { } /** + * TODO(b/67867469): + * - javadoc it + * - javadoc how to check results + * - unhide + * - unhide / remove testApi + * - throw exception (and document) if response has datasets or saveinfo + * - throw exception (and document) if id on fieldsDetection is ignored + * + * @hide + */ + @TestApi + public Builder setFieldsDetection(@NonNull FieldsDetection fieldsDetection) { + throwIfDestroyed(); + throwIfDisableAutofillCalled(); + mFieldsDetection = Preconditions.checkNotNull(fieldsDetection); + return this; + } + + /** * Sets flags changing the response behavior. * * @param flags a combination of {@link #FLAG_TRACK_CONTEXT_COMMITED} and @@ -365,7 +393,8 @@ public final class FillResponse implements Parcelable { if (duration <= 0) { throw new IllegalArgumentException("duration must be greater than 0"); } - if (mAuthentication != null || mDatasets != null || mSaveInfo != null) { + if (mAuthentication != null || mDatasets != null || mSaveInfo != null + || mFieldsDetection != null) { throw new IllegalStateException("disableAutofill() must be the only method called"); } @@ -388,11 +417,11 @@ public final class FillResponse implements Parcelable { */ public FillResponse build() { throwIfDestroyed(); - if (mAuthentication == null && mDatasets == null && mSaveInfo == null - && mDisableDuration == 0) { - throw new IllegalStateException("need to provide at least one DataSet or a " - + "SaveInfo or an authentication with a presentation or disable autofill"); + && mDisableDuration == 0 && mFieldsDetection == null) { + throw new IllegalStateException("need to provide: at least one DataSet, or a " + + "SaveInfo, or an authentication with a presentation, " + + "or a FieldsDetection, or disable autofill"); } mDestroyed = true; return new FillResponse(this); @@ -430,6 +459,7 @@ public final class FillResponse implements Parcelable { .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds)) .append(", disableDuration=").append(mDisableDuration) .append(", flags=").append(mFlags) + .append(", fieldDetection=").append(mFieldsDetection) .append("]") .toString(); } @@ -453,6 +483,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelable(mPresentation, flags); parcel.writeParcelableArray(mIgnoredIds, flags); parcel.writeLong(mDisableDuration); + parcel.writeParcelable(mFieldsDetection, flags); parcel.writeInt(mFlags); parcel.writeInt(mRequestId); } @@ -488,6 +519,10 @@ public final class FillResponse implements Parcelable { if (disableDuration > 0) { builder.disableAutofill(disableDuration); } + final FieldsDetection fieldsDetection = parcel.readParcelable(null); + if (fieldsDetection != null) { + builder.setFieldsDetection(fieldsDetection); + } builder.setFlags(parcel.readInt()); final FillResponse response = builder.build(); diff --git a/android/service/dreams/DreamService.java b/android/service/dreams/DreamService.java index 6a15aded..2a245d04 100644 --- a/android/service/dreams/DreamService.java +++ b/android/service/dreams/DreamService.java @@ -680,8 +680,8 @@ public class DreamService extends Service implements Window.Callback { * * @return The screen state to use while dozing, such as {@link Display#STATE_ON}, * {@link Display#STATE_DOZE}, {@link Display#STATE_DOZE_SUSPEND}, - * or {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN} for the default - * behavior. + * {@link Display#STATE_ON_SUSPEND}, {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN} + * for the default behavior. * * @see #setDozeScreenState * @hide For use by system UI components only. @@ -700,12 +700,18 @@ public class DreamService extends Service implements Window.Callback { * perform transitions between states while dozing to conserve power and * achieve various effects. * </p><p> - * It is recommended that the state be set to {@link Display#STATE_DOZE_SUSPEND} - * once the dream has completely finished drawing and before it releases its wakelock - * to allow the display hardware to be fully suspended. While suspended, the - * display will preserve its on-screen contents or hand off control to dedicated - * doze hardware if the devices supports it. If the doze suspend state is - * used, the dream must make sure to set the mode back + * Some devices will have dedicated hardware ("Sidekick") to animate + * the display content while the CPU sleeps. If the dream and the hardware support + * this, {@link Display#STATE_ON_SUSPEND} or {@link Display#STATE_DOZE_SUSPEND} + * will switch control to the Sidekick. + * </p><p> + * If not using Sidekick, it is recommended that the state be set to + * {@link Display#STATE_DOZE_SUSPEND} once the dream has completely + * finished drawing and before it releases its wakelock + * to allow the display hardware to be fully suspended. While suspended, + * the display will preserve its on-screen contents. + * </p><p> + * If the doze suspend state is used, the dream must make sure to set the mode back * to {@link Display#STATE_DOZE} or {@link Display#STATE_ON} before drawing again * since the display updates may be ignored and not seen by the user otherwise. * </p><p> @@ -716,8 +722,8 @@ public class DreamService extends Service implements Window.Callback { * * @param state The screen state to use while dozing, such as {@link Display#STATE_ON}, * {@link Display#STATE_DOZE}, {@link Display#STATE_DOZE_SUSPEND}, - * or {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN} for the default - * behavior. + * {@link Display#STATE_ON_SUSPEND}, {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN} + * for the default behavior. * * @hide For use by system UI components only. */ diff --git a/android/service/euicc/EuiccService.java b/android/service/euicc/EuiccService.java index 0c2e4b7a..cd233b83 100644 --- a/android/service/euicc/EuiccService.java +++ b/android/service/euicc/EuiccService.java @@ -97,6 +97,10 @@ public abstract class EuiccService extends Service { public static final String ACTION_RESOLVE_NO_PRIVILEGES = "android.service.euicc.action.RESOLVE_NO_PRIVILEGES"; + /** Ask the user to input carrier confirmation code. */ + public static final String ACTION_RESOLVE_CONFIRMATION_CODE = + "android.service.euicc.action.RESOLVE_CONFIRMATION_CODE"; + /** Intent extra set for resolution requests containing the package name of the calling app. */ public static final String EXTRA_RESOLUTION_CALLING_PACKAGE = "android.service.euicc.extra.RESOLUTION_CALLING_PACKAGE"; @@ -105,6 +109,8 @@ public abstract class EuiccService extends Service { public static final int RESULT_OK = 0; /** Result code indicating that an active SIM must be deactivated to perform the operation. */ public static final int RESULT_MUST_DEACTIVATE_SIM = -1; + /** Result code indicating that the user must input a carrier confirmation code. */ + public static final int RESULT_NEED_CONFIRMATION_CODE = -2; // New predefined codes should have negative values. /** Start of implementation-specific error results. */ @@ -119,10 +125,13 @@ public abstract class EuiccService extends Service { RESOLUTION_ACTIONS = new ArraySet<>(); RESOLUTION_ACTIONS.add(EuiccService.ACTION_RESOLVE_DEACTIVATE_SIM); RESOLUTION_ACTIONS.add(EuiccService.ACTION_RESOLVE_NO_PRIVILEGES); + RESOLUTION_ACTIONS.add(EuiccService.ACTION_RESOLVE_CONFIRMATION_CODE); } /** Boolean extra for resolution actions indicating whether the user granted consent. */ public static final String RESOLUTION_EXTRA_CONSENT = "consent"; + /** String extra for resolution actions indicating the carrier confirmation code. */ + public static final String RESOLUTION_EXTRA_CONFIRMATION_CODE = "confirmation_code"; private final IEuiccService.Stub mStubWrapper; diff --git a/android/service/notification/ConditionProviderService.java b/android/service/notification/ConditionProviderService.java index 3e992ec3..6fc689ab 100644 --- a/android/service/notification/ConditionProviderService.java +++ b/android/service/notification/ConditionProviderService.java @@ -18,6 +18,8 @@ package android.service.notification; import android.annotation.SdkConstant; import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.app.ActivityManager; import android.app.INotificationManager; import android.app.Service; import android.content.ComponentName; @@ -56,6 +58,8 @@ import android.util.Log; * </meta-data> * </service></pre> * + * <p> Condition providers cannot be bound by the system on + * {@link ActivityManager#isLowRamDevice() low ram} devices</p> */ public abstract class ConditionProviderService extends Service { private final String TAG = ConditionProviderService.class.getSimpleName() @@ -197,7 +201,11 @@ public abstract class ConditionProviderService extends Service { return mProvider; } - private boolean isBound() { + /** + * @hide + */ + @TestApi + public boolean isBound() { if (mProvider == null) { Log.w(TAG, "Condition provider service not yet bound."); return false; diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java index 08d3118b..dac663e7 100644 --- a/android/service/notification/NotificationListenerService.java +++ b/android/service/notification/NotificationListenerService.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.TestApi; +import android.app.ActivityManager; import android.app.INotificationManager; import android.app.Notification; import android.app.Notification.Builder; @@ -82,6 +83,8 @@ import java.util.List; * method is the <i>only</i> one that is safe to call before {@link #onListenerConnected()} * or after {@link #onListenerDisconnected()}. * </p> + * <p> Notification listeners cannot get notification access or be bound by the system on + * {@link ActivityManager#isLowRamDevice() low ram} devices</p> */ public abstract class NotificationListenerService extends Service { diff --git a/android/service/voice/VoiceInteractionSession.java b/android/service/voice/VoiceInteractionSession.java index 625dd9eb..cd177c42 100644 --- a/android/service/voice/VoiceInteractionSession.java +++ b/android/service/voice/VoiceInteractionSession.java @@ -16,6 +16,8 @@ package android.service.voice; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + import android.annotation.Nullable; import android.app.Activity; import android.app.Dialog; @@ -46,7 +48,6 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; @@ -63,8 +64,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; - /** * An active voice interaction session, providing a facility for the implementation * to interact with the user in the voice interaction layer. The user interface is @@ -110,16 +109,6 @@ public class VoiceInteractionSession implements KeyEvent.Callback, ComponentCall */ public static final int SHOW_SOURCE_ACTIVITY = 1<<4; - // Keys for Bundle values - /** @hide */ - public static final String KEY_DATA = "data"; - /** @hide */ - public static final String KEY_STRUCTURE = "structure"; - /** @hide */ - public static final String KEY_CONTENT = "content"; - /** @hide */ - public static final String KEY_RECEIVER_EXTRAS = "receiverExtras"; - final Context mContext; final HandlerCaller mHandlerCaller; @@ -1423,9 +1412,7 @@ public class VoiceInteractionSession implements KeyEvent.Callback, ComponentCall public void setContentView(View view) { ensureWindowCreated(); mContentFrame.removeAllViews(); - mContentFrame.addView(view, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); + mContentFrame.addView(view, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentFrame.requestApplyInsets(); } diff --git a/android/support/LibraryGroups.java b/android/support/LibraryGroups.java new file mode 100644 index 00000000..feaefbc6 --- /dev/null +++ b/android/support/LibraryGroups.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017 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 android.support; + +/** + * The list of maven group names of all the libraries in this project. + */ +public class LibraryGroups { + public static final String SUPPORT = "com.android.support"; + public static final String ROOM = "android.arch.persistence.room"; + public static final String PERSISTENCE = "android.arch.persistence"; + public static final String LIFECYCLE = "android.arch.lifecycle"; + public static final String ARCH_CORE = "android.arch.core"; + public static final String PAGING = "android.arch.paging"; + public static final String NAVIGATION = "android.arch.navigation"; +} diff --git a/android/support/LibraryVersions.java b/android/support/LibraryVersions.java index 2f5730a2..efa0cbae 100644 --- a/android/support/LibraryVersions.java +++ b/android/support/LibraryVersions.java @@ -28,7 +28,7 @@ public class LibraryVersions { /** * Version code for flatfoot 1.0 projects (room, lifecycles) */ - private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0-rc1"); + private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0"); /** * Version code for Room diff --git a/android/support/SourceJarTaskHelper.java b/android/support/SourceJarTaskHelper.java new file mode 100644 index 00000000..9fbd1dba --- /dev/null +++ b/android/support/SourceJarTaskHelper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 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 android.support; + +import com.android.build.gradle.LibraryExtension; +import com.android.builder.core.BuilderConstants; + +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.bundling.Jar; + +/** + * Helper class to handle creation of source jars. + */ +public class SourceJarTaskHelper { + /** + * Sets up a source jar task for an Android library project. + */ + public static void setUpAndroidProject(Project project, LibraryExtension extension) { + // Create sources jar for release builds + extension.getLibraryVariants().all(libraryVariant -> { + if (!libraryVariant.getBuildType().getName().equals(BuilderConstants.RELEASE)) { + return; // Skip non-release builds. + } + + Jar sourceJar = project.getTasks().create("sourceJarRelease", Jar.class); + sourceJar.setPreserveFileTimestamps(false); + sourceJar.setClassifier("sources"); + sourceJar.from(extension.getSourceSets().findByName("main").getJava().getSrcDirs()); + project.getArtifacts().add("archives", sourceJar); + }); + } + + /** + * Sets up a source jar task for a Java library project. + */ + public static void setUpJavaProject(Project project) { + Jar sourceJar = project.getTasks().create("sourceJar", Jar.class); + sourceJar.setPreserveFileTimestamps(false); + sourceJar.setClassifier("sources"); + JavaPluginConvention convention = + project.getConvention().getPlugin(JavaPluginConvention.class); + sourceJar.from(convention.getSourceSets().findByName("main").getAllSource().getSrcDirs()); + project.getArtifacts().add("archives", sourceJar); + } +} diff --git a/android/support/car/drawer/CarDrawerActivity.java b/android/support/car/drawer/CarDrawerActivity.java index 7100218a..f46c652b 100644 --- a/android/support/car/drawer/CarDrawerActivity.java +++ b/android/support/car/drawer/CarDrawerActivity.java @@ -46,7 +46,7 @@ import android.view.ViewGroup; * * <p>The rootAdapter can implement nested-navigation, in its click-handling, by passing the * CarDrawerAdapter for the next level to - * {@link CarDrawerController#switchToAdapter(CarDrawerAdapter)}. + * {@link CarDrawerController#pushAdapter(CarDrawerAdapter)}. * * <p>Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a * derivative. diff --git a/android/support/car/drawer/CarDrawerController.java b/android/support/car/drawer/CarDrawerController.java index 4d9f4e99..7b23714c 100644 --- a/android/support/car/drawer/CarDrawerController.java +++ b/android/support/car/drawer/CarDrawerController.java @@ -19,16 +19,19 @@ package android.support.car.drawer; import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; +import android.support.annotation.AnimRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.car.R; import android.support.car.widget.PagedListView; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.view.Gravity; import android.view.MenuItem; import android.view.View; +import android.view.animation.AnimationUtils; import android.widget.ProgressBar; import java.util.Stack; @@ -39,13 +42,21 @@ import java.util.Stack; * navigation. */ public class CarDrawerController { + /** An animation for when a user navigates into a submenu. */ + @AnimRes + private static final int DRILL_DOWN_ANIM = R.anim.fade_in_trans_right_layout_anim; + + /** An animation for when a user navigates up (when the back button is pressed). */ + @AnimRes + private static final int NAVIGATE_UP_ANIM = R.anim.fade_in_trans_left_layout_anim; + /** The amount that the drawer has been opened before its color should be switched. */ private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f; /** * A representation of the hierarchy of navigation being displayed in the list. The ordering of * this stack is the order that the user has visited each level. When the user navigates up, - * the adapters are poopped from this list. + * the adapters are popped from this list. */ private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>(); @@ -78,16 +89,14 @@ public class CarDrawerController { ActionBarDrawerToggle drawerToggle) { mToolbar = toolbar; mContext = drawerLayout.getContext(); - + mDrawerToggle = drawerToggle; mDrawerLayout = drawerLayout; mDrawerContent = drawerLayout.findViewById(R.id.drawer_content); mDrawerList = drawerLayout.findViewById(R.id.drawer_list); mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED); - mProgressBar = drawerLayout.findViewById(R.id.drawer_progress); - mDrawerToggle = drawerToggle; setupDrawerToggling(); } @@ -104,7 +113,13 @@ public class CarDrawerController { return; } - mAdapterStack.push(rootAdapter); + // The root adapter is always the last item in the stack. + if (mAdapterStack.size() > 0) { + mAdapterStack.set(0, rootAdapter); + } else { + mAdapterStack.push(rootAdapter); + } + setToolbarTitleFrom(rootAdapter); mDrawerList.setAdapter(rootAdapter); } @@ -120,10 +135,11 @@ public class CarDrawerController { * * @param adapter Adapter for next level of content in the drawer. */ - public final void switchToAdapter(CarDrawerAdapter adapter) { + public final void pushAdapter(CarDrawerAdapter adapter) { mAdapterStack.peek().setTitleChangeListener(null); mAdapterStack.push(adapter); - switchToAdapterInternal(adapter); + setDisplayAdapter(adapter); + runLayoutAnimation(DRILL_DOWN_ANIM); } /** Close the drawer. */ @@ -264,15 +280,15 @@ public class CarDrawerController { } /** - * Sets the navigation drawer's title to be the one supplied by the given adapter and updates - * the navigation drawer list with the adapter's contents. + * Sets the given adapter as the one displaying the current contents of the drawer. + * + * <p>The drawer's title will also be derived from the given adapter. */ - private void switchToAdapterInternal(CarDrawerAdapter adapter) { + private void setDisplayAdapter(CarDrawerAdapter adapter) { setToolbarTitleFrom(adapter); // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts. mDrawerList.getRecyclerView().setAdapter(adapter); - scrollToPosition(0); } /** @@ -290,7 +306,8 @@ public class CarDrawerController { CarDrawerAdapter adapter = mAdapterStack.pop(); adapter.setTitleChangeListener(null); adapter.cleanup(); - switchToAdapterInternal(mAdapterStack.peek()); + setDisplayAdapter(mAdapterStack.peek()); + runLayoutAnimation(NAVIGATE_UP_ANIM); return true; } @@ -301,6 +318,18 @@ public class CarDrawerController { adapter.setTitleChangeListener(null); adapter.cleanup(); } - switchToAdapterInternal(mAdapterStack.peek()); + setDisplayAdapter(mAdapterStack.peek()); + runLayoutAnimation(NAVIGATE_UP_ANIM); + } + + /** + * Runs the given layout animation on the PagedListView. Running this animation will also + * refresh the contents of the list. + */ + private void runLayoutAnimation(@AnimRes int animation) { + RecyclerView recyclerView = mDrawerList.getRecyclerView(); + recyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, animation)); + recyclerView.getAdapter().notifyDataSetChanged(); + recyclerView.scheduleLayoutAnimation(); } } diff --git a/android/support/car/utils/ColumnCalculator.java b/android/support/car/utils/ColumnCalculator.java index 96e081b9..fa5dd432 100644 --- a/android/support/car/utils/ColumnCalculator.java +++ b/android/support/car/utils/ColumnCalculator.java @@ -65,7 +65,7 @@ public class ColumnCalculator { private ColumnCalculator(Context context) { Resources res = context.getResources(); - int marginSize = res.getDimensionPixelSize(R.dimen.car_screen_margin_size); + int marginSize = res.getDimensionPixelSize(R.dimen.car_margin); mGutterSize = res.getDimensionPixelSize(R.dimen.car_screen_gutter_size); mNumOfColumns = res.getInteger(R.integer.car_screen_num_of_columns); diff --git a/android/support/car/widget/CarItemAnimator.java b/android/support/car/widget/CarItemAnimator.java index 4dd32127..ef22c484 100644 --- a/android/support/car/widget/CarItemAnimator.java +++ b/android/support/car/widget/CarItemAnimator.java @@ -22,9 +22,9 @@ import android.support.v7.widget.RecyclerView; /** {@link DefaultItemAnimator} with a few minor changes where it had undesired behavior. */ public class CarItemAnimator extends DefaultItemAnimator { - private final CarLayoutManager mLayoutManager; + private final PagedLayoutManager mLayoutManager; - public CarItemAnimator(CarLayoutManager layoutManager) { + public CarItemAnimator(PagedLayoutManager layoutManager) { mLayoutManager = layoutManager; } diff --git a/android/support/car/widget/CarRecyclerView.java b/android/support/car/widget/CarRecyclerView.java index 2684c58a..bb9cb71a 100644 --- a/android/support/car/widget/CarRecyclerView.java +++ b/android/support/car/widget/CarRecyclerView.java @@ -26,7 +26,7 @@ import android.view.View; import android.view.ViewGroup; /** - * Custom {@link RecyclerView} that helps {@link CarLayoutManager} properly fling and paginate. + * Custom {@link RecyclerView} that helps {@link PagedLayoutManager} properly fling and paginate. * * <p>It also has the ability to fade children as they scroll off screen that can be set with {@link * #setFadeLastItem(boolean)}. @@ -57,7 +57,7 @@ public class CarRecyclerView extends RecyclerView { @Override public boolean fling(int velocityX, int velocityY) { mWasFlingCalledForGesture = true; - return ((CarLayoutManager) getLayoutManager()).settleScrollForFling(this, velocityY); + return ((PagedLayoutManager) getLayoutManager()).settleScrollForFling(this, velocityY); } @Override @@ -69,7 +69,7 @@ public class CarRecyclerView extends RecyclerView { int action = e.getActionMasked(); if (action == MotionEvent.ACTION_UP) { if (!mWasFlingCalledForGesture) { - ((CarLayoutManager) getLayoutManager()).settleScrollForFling(this, 0); + ((PagedLayoutManager) getLayoutManager()).settleScrollForFling(this, 0); } mWasFlingCalledForGesture = false; } @@ -102,7 +102,7 @@ public class CarRecyclerView extends RecyclerView { * number of items that fit completely on the screen. */ public void pageUp() { - CarLayoutManager lm = (CarLayoutManager) getLayoutManager(); + PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager(); int pageUpPosition = lm.getPageUpPosition(); if (pageUpPosition == -1) { return; @@ -116,7 +116,7 @@ public class CarRecyclerView extends RecyclerView { * number of items that fit completely on the screen. */ public void pageDown() { - CarLayoutManager lm = (CarLayoutManager) getLayoutManager(); + PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager(); int pageDownPosition = lm.getPageDownPosition(); if (pageDownPosition == -1) { return; diff --git a/android/support/car/widget/CarLayoutManager.java b/android/support/car/widget/PagedLayoutManager.java index d0d3a9e1..c4f469a3 100644 --- a/android/support/car/widget/CarLayoutManager.java +++ b/android/support/car/widget/PagedLayoutManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright 2017 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. @@ -18,6 +18,8 @@ package android.support.car.widget; import android.content.Context; import android.graphics.PointF; +import android.os.Parcel; +import android.os.Parcelable; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; @@ -46,9 +48,9 @@ import java.util.ArrayList; * * <ol> * <li>In a normal ListView, when views reach the top of the list, they are clipped. In - * CarLayoutManager, views have the option of flying off of the top of the screen as the next - * row settles in to place. This functionality can be enabled or disabled with {@link - * #setOffsetRows(boolean)}. + * PagedLayoutManager, views have the option of flying off of the top of the screen as the + * next row settles in to place. This functionality can be enabled or disabled with + * {@link #setOffsetRows(boolean)}. * <li>Standard list physics is disabled. Instead, when the user scrolls, it will settle on the * next page. * <li>Items can scroll past the bottom edge of the screen. This helps with pagination so that the @@ -57,8 +59,8 @@ import java.util.ArrayList; * * This LayoutManger should be used with {@link CarRecyclerView}. */ -public class CarLayoutManager extends RecyclerView.LayoutManager { - private static final String TAG = "CarLayoutManager"; +public class PagedLayoutManager extends RecyclerView.LayoutManager { + private static final String TAG = "PagedLayoutManager"; /** * Any fling below the threshold will just scroll to the top fully visible row. The units is @@ -166,7 +168,7 @@ public class CarLayoutManager extends RecyclerView.LayoutManager { /** Set the anchor to the following position on the next layout pass. */ private int mPendingScrollPosition = -1; - public CarLayoutManager(Context context) { + public PagedLayoutManager(Context context) { mContext = context; } @@ -919,6 +921,55 @@ public class CarLayoutManager extends RecyclerView.LayoutManager { return mLowerPageBreakPosition; } + @Override + public Parcelable onSaveInstanceState() { + SavedState savedState = new SavedState(); + savedState.mFirstChildPosition = getFirstFullyVisibleChildPosition(); + return savedState; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + scrollToPosition(((SavedState) state).mFirstChildPosition); + } + } + + /** The state that will be saved across configuration changes. */ + static class SavedState implements Parcelable { + /** The position of the first visible child view in the list. */ + int mFirstChildPosition; + + SavedState() {} + + private SavedState(Parcel in) { + mFirstChildPosition = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mFirstChildPosition); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + /** * Layout the anchor row. The anchor row is the first fully visible row. * diff --git a/android/support/car/widget/PagedListView.java b/android/support/car/widget/PagedListView.java index 46527001..4695c45c 100644 --- a/android/support/car/widget/PagedListView.java +++ b/android/support/car/widget/PagedListView.java @@ -23,7 +23,11 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -33,6 +37,7 @@ import android.support.car.R; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; +import android.util.SparseArray; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -41,13 +46,19 @@ import android.widget.FrameLayout; /** * Custom {@link android.support.v7.widget.RecyclerView} that displays a list of items that * resembles a {@link android.widget.ListView} but also has page up and page down arrows on the - * right side. + * left side. */ public class PagedListView extends FrameLayout { /** Default maximum number of clicks allowed on a list */ public static final int DEFAULT_MAX_CLICKS = 6; /** + * Value to pass to {@link #setMaxPages(int)} to indicate there is no restriction on the + * maximum number of pages to show. + */ + public static final int UNLIMITED_PAGES = -1; + + /** * The amount of time after settling to wait before autoscrolling to the next page when the user * holds down a pagination button. */ @@ -57,7 +68,7 @@ public class PagedListView extends FrameLayout { private static final int INVALID_RESOURCE_ID = -1; protected final CarRecyclerView mRecyclerView; - protected final CarLayoutManager mLayoutManager; + protected final PagedLayoutManager mLayoutManager; protected final Handler mHandler = new Handler(); private final boolean mScrollBarEnabled; private final PagedScrollBarView mScrollBarView; @@ -65,8 +76,8 @@ public class PagedListView extends FrameLayout { private int mRowsPerPage = -1; protected RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter; - /** Maximum number of pages to show. Values < 0 show all pages. */ - private int mMaxPages = -1; + /** Maximum number of pages to show. */ + private int mMaxPages; protected OnScrollListener mOnScrollListener; @@ -115,8 +126,6 @@ public class PagedListView extends FrameLayout { * the item in position 20 instead, for position 1 it will show the item in position 21 instead * and so on. */ - // TODO(b/28003781): ItemPositionOffset and ItemCap interfaces should be merged once - // we enable AlphaJump outside drawer. public interface ItemPositionOffset { /** Sets the position offset for the adapter. */ void setPositionOffset(int positionOffset); @@ -151,7 +160,7 @@ public class PagedListView extends FrameLayout { mMaxPages = getDefaultMaxPages(); - mLayoutManager = new CarLayoutManager(context); + mLayoutManager = new PagedLayoutManager(context); mLayoutManager.setOffsetRows(offsetRows); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener); @@ -162,7 +171,7 @@ public class PagedListView extends FrameLayout { if (offsetScrollBar) { MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); params.setMarginStart(getResources().getDimensionPixelSize( - R.dimen.car_screen_margin_size)); + R.dimen.car_margin)); params.setMarginEnd( a.getDimensionPixelSize(R.styleable.PagedListView_listEndMargin, 0)); mRecyclerView.setLayoutParams(params); @@ -180,6 +189,11 @@ public class PagedListView extends FrameLayout { dividerStartId, dividerEndId)); } + int itemSpacing = a.getDimensionPixelSize(R.styleable.PagedListView_itemSpacing, 0); + if (itemSpacing > 0) { + mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing)); + } + // Set this to true so that this view consumes clicks events and views underneath // don't receive this click event. Without this it's possible to click places in the // view that don't capture the event, and as a result, elements visually hidden consume @@ -212,6 +226,16 @@ public class PagedListView extends FrameLayout { } }); + Drawable upButtonIcon = a.getDrawable(R.styleable.PagedListView_upButtonIcon); + if (upButtonIcon != null) { + setUpButtonIcon(upButtonIcon); + } + + Drawable downButtonIcon = a.getDrawable(R.styleable.PagedListView_downButtonIcon); + if (downButtonIcon != null) { + setDownButtonIcon(downButtonIcon); + } + mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE); // Modify the layout the Scroll Bar is not visible. @@ -236,7 +260,7 @@ public class PagedListView extends FrameLayout { if (e.getAction() == MotionEvent.ACTION_DOWN) { // The user has interacted with the list using touch. All movements will now paginate // the list. - mLayoutManager.setRowOffsetMode(CarLayoutManager.ROW_OFFSET_MODE_PAGE); + mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_PAGE); } return super.onInterceptTouchEvent(e); } @@ -246,7 +270,7 @@ public class PagedListView extends FrameLayout { super.requestChildFocus(child, focused); // The user has interacted with the list using the controller. Movements through the list // will now be one row at a time. - mLayoutManager.setRowOffsetMode(CarLayoutManager.ROW_OFFSET_MODE_INDIVIDUAL); + mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_INDIVIDUAL); } /** @@ -312,19 +336,25 @@ public class PagedListView extends FrameLayout { mHandler.post(mUpdatePaginationRunnable); } + /** Sets the icon to be used for the up button. */ + public void setUpButtonIcon(Drawable icon) { + mScrollBarView.setUpButtonIcon(icon); + } + + /** Sets the icon to be used for the down button. */ + public void setDownButtonIcon(Drawable icon) { + mScrollBarView.setDownButtonIcon(icon); + } + /** * Sets the adapter for the list. * - * <p>It <em>must</em> implement {@link ItemCap}, otherwise, will throw an {@link - * IllegalArgumentException}. + * <p>The given Adapter can implement {@link ItemCap} if it wishes to control the behavior of + * a max number of items. Otherwise, methods in the PagedListView to limit the content, such as + * {@link #setMaxPages(int)}, will do nothing. */ public void setAdapter( @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) { - if (!(adapter instanceof ItemCap)) { - throw new IllegalArgumentException("ERROR: adapter [" - + adapter.getClass().getCanonicalName() + "] MUST implement ItemCap"); - } - mAdapter = adapter; mRecyclerView.setAdapter(adapter); updateMaxItems(); @@ -333,7 +363,7 @@ public class PagedListView extends FrameLayout { /** @hide */ @RestrictTo(LIBRARY_GROUP) @NonNull - public CarLayoutManager getLayoutManager() { + public PagedLayoutManager getLayoutManager() { return mLayoutManager; } @@ -345,15 +375,19 @@ public class PagedListView extends FrameLayout { /** * Sets the maximum number of the pages that can be shown in the PagedListView. The size of a - * page is defined as the number of items that fit completely on the screen at once. + * page is defined as the number of items that fit completely on the screen at once. + * + * <p>Passing {@link #UNLIMITED_PAGES} will remove any restrictions on a maximum number + * of pages. + * + * <p>Note that for any restriction on maximum pages to work, the adapter passed to this + * PagedListView needs to implement {@link ItemCap}. * - * @param maxPages The maximum number of pages that fit on the screen. Should be positive. + * @param maxPages The maximum number of pages that fit on the screen. Should be positive or + * {@link #UNLIMITED_PAGES}. */ public void setMaxPages(int maxPages) { - if (maxPages < 0) { - return; - } - mMaxPages = maxPages; + mMaxPages = Math.max(UNLIMITED_PAGES, maxPages); updateMaxItems(); } @@ -362,7 +396,8 @@ public class PagedListView extends FrameLayout { * {@link #setMaxPages(int)}. If that method has not been called, then this value should match * the default value. * - * @return The maximum number of pages to be shown. + * @return The maximum number of pages to be shown or {@link #UNLIMITED_PAGES} if there is + * no limit. */ public int getMaxPages() { return mMaxPages; @@ -370,7 +405,7 @@ public class PagedListView extends FrameLayout { /** * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of - * CarLayoutManager is null or the height of the first child is 0, it will return 1. + * PagedLayoutManager is null or the height of the first child is 0, it will return 1. */ public int getRowsPerPage() { return mRowsPerPage; @@ -422,6 +457,32 @@ public class PagedListView extends FrameLayout { } /** + * Sets spacing between each item in the list. The spacing will not be added before the first + * item and after the last. + * + * @param itemSpacing the spacing between each item. + */ + public void setItemSpacing(int itemSpacing) { + ItemSpacingDecoration existing = null; + for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) { + RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i); + if (itemDecoration instanceof ItemSpacingDecoration) { + existing = (ItemSpacingDecoration) itemDecoration; + break; + } + } + + if (itemSpacing == 0 && existing != null) { + mRecyclerView.removeItemDecoration(existing); + } else if (existing == null) { + mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing)); + } else { + existing.setItemSpacing(itemSpacing); + } + mRecyclerView.invalidateItemDecorations(); + } + + /** * Adds an {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} to this * PagedListView. * @@ -520,6 +581,7 @@ public class PagedListView extends FrameLayout { return; } mDefaultMaxPages = newDefault; + resetMaxPages(); } /** Returns the default number of pages the list should have */ @@ -646,8 +708,15 @@ public class PagedListView extends FrameLayout { return; } - final int originalCount = mAdapter.getItemCount(); + // Ensure mRowsPerPage regardless of if the adapter implements ItemCap. updateRowsPerPage(); + + // If the adapter does not implement ItemCap, then the max items on it cannot be updated. + if (!(mAdapter instanceof ItemCap)) { + return; + } + + final int originalCount = mAdapter.getItemCount(); ((ItemCap) mAdapter).setMaxItems(calculateMaxItemCount()); final int newCount = mAdapter.getItemCount(); if (newCount == originalCount) { @@ -683,6 +752,78 @@ public class PagedListView extends FrameLayout { } } + @Override + protected Parcelable onSaveInstanceState() { + SavedState savedState = new SavedState(super.onSaveInstanceState()); + savedState.mLayoutManagerState = mLayoutManager.onSaveInstanceState(); + return savedState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + mLayoutManager.onRestoreInstanceState(savedState.mLayoutManagerState); + super.onRestoreInstanceState(savedState.getSuperState()); + } + + @Override + protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { + // There is the possibility of multiple PagedListViews on a page. This means that the ids + // of the child Views of PagedListView are no longer unique, and onSaveInstanceState() + // cannot be used. As a result, PagedListViews needs to manually dispatch the instance + // states. Call dispatchFreezeSelfOnly() so that no child views have onSaveInstanceState() + // called by the system. + dispatchFreezeSelfOnly(container); + } + + @Override + protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { + // Prevent onRestoreInstanceState() from being called on child Views. Instead, PagedListView + // will manually handle passing the state. See the comment in dispatchSaveInstanceState() + // for more information. + dispatchThawSelfOnly(container); + } + + /** The state that will be saved across configuration changes. */ + private static class SavedState extends BaseSavedState { + /** The state of the {@link #mLayoutManager} of this PagedListView. */ + Parcelable mLayoutManagerState; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + mLayoutManagerState = + in.readParcelable(PagedLayoutManager.SavedState.class.getClassLoader()); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeParcelable(mLayoutManagerState, flags); + } + + public static final ClassLoaderCreator<SavedState> CREATOR = + new ClassLoaderCreator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel source, ClassLoader loader) { + return new SavedState(source); + } + + @Override + public SavedState createFromParcel(Parcel source) { + return createFromParcel(source, null /* loader */); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener = new RecyclerView.OnScrollListener() { @Override @@ -766,16 +907,50 @@ public class PagedListView extends FrameLayout { } /** + * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will add spacing + * between each item in the RecyclerView that it is added to. + */ + private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration { + + private int mHalfItemSpacing; + + private ItemSpacingDecoration(int itemSpacing) { + mHalfItemSpacing = itemSpacing / 2; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + // Skip top offset for first item and bottom offset for last. + int position = parent.getChildAdapterPosition(view); + if (position > 0) { + outRect.top = mHalfItemSpacing; + } + if (position < state.getItemCount() - 1) { + outRect.bottom = mHalfItemSpacing; + } + } + + /** + * @param itemSpacing sets spacing between each item. + */ + public void setItemSpacing(int itemSpacing) { + mHalfItemSpacing = itemSpacing / 2; + } + } + + /** * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will draw a dividing * line between each item in the RecyclerView that it is added to. */ - public static class DividerDecoration extends RecyclerView.ItemDecoration { + private static class DividerDecoration extends RecyclerView.ItemDecoration { private final Context mContext; private final Paint mPaint; private final int mDividerHeight; private final int mDividerStartMargin; @IdRes private final int mDividerStartId; - @IdRes private final int mDvidierEndId; + @IdRes private final int mDividerEndId; /** * @param dividerStartMargin The start offset of the dividing line. This offset will be @@ -792,7 +967,7 @@ public class PagedListView extends FrameLayout { mContext = context; mDividerStartMargin = dividerStartMargin; mDividerStartId = dividerStartId; - mDvidierEndId = dividerEndId; + mDividerEndId = dividerEndId; Resources res = context.getResources(); mPaint = new Paint(); @@ -807,16 +982,20 @@ public class PagedListView extends FrameLayout { @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { - for (int i = 0, childCount = parent.getChildCount(); i < childCount; i++) { + // Draw a divider line between each item. No need to draw the line for the last item. + for (int i = 0, childCount = parent.getChildCount(); i < childCount - 1; i++) { View container = parent.getChildAt(i); + View nextContainer = parent.getChildAt(i + 1); + int spacing = nextContainer.getTop() - container.getBottom(); + View startChild = mDividerStartId != INVALID_RESOURCE_ID ? container.findViewById(mDividerStartId) : container; View endChild = - mDvidierEndId != INVALID_RESOURCE_ID - ? container.findViewById(mDvidierEndId) + mDividerEndId != INVALID_RESOURCE_ID + ? container.findViewById(mDividerEndId) : container; if (startChild == null || endChild == null) { @@ -825,14 +1004,24 @@ public class PagedListView extends FrameLayout { int left = mDividerStartMargin + startChild.getLeft(); int right = endChild.getRight(); - int bottom = container.getBottom(); + int bottom = container.getBottom() + spacing / 2 + mDividerHeight / 2; int top = bottom - mDividerHeight; - // Draw a divider line between each item. No need to draw the line for the last - // item. - if (i != childCount - 1) { - c.drawRect(left, top, right, bottom, mPaint); - } + c.drawRect(left, top, right, bottom, mPaint); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + // Skip top offset for first item and bottom offset for last. + int position = parent.getChildAdapterPosition(view); + if (position > 0) { + outRect.top = mDividerHeight / 2; + } + if (position < state.getItemCount() - 1) { + outRect.bottom = mDividerHeight / 2; } } } diff --git a/android/support/car/widget/PagedScrollBarView.java b/android/support/car/widget/PagedScrollBarView.java index 125b354c..1c46b5d4 100644 --- a/android/support/car/widget/PagedScrollBarView.java +++ b/android/support/car/widget/PagedScrollBarView.java @@ -18,6 +18,7 @@ package android.support.car.widget; import android.content.Context; import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.support.car.R; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; @@ -98,6 +99,16 @@ public class PagedScrollBarView extends FrameLayout return true; } + /** Sets the icon to be used for the up button. */ + public void setUpButtonIcon(Drawable icon) { + mUpButton.setImageDrawable(icon); + } + + /** Sets the icon to be used for the down button. */ + public void setDownButtonIcon(Drawable icon) { + mDownButton.setImageDrawable(icon); + } + /** * Sets the listener that will be notified when the up and down buttons have been pressed. * @@ -119,7 +130,7 @@ public class PagedScrollBarView extends FrameLayout /** Sets the range, offset and extent of the scroll bar. See {@link View}. */ public void setParameters(int range, int offset, int extent, boolean animate) { - // This method is where we take the computed parameters from the CarLayoutManager and + // This method is where we take the computed parameters from the PagedLayoutManager and // render it within the specified constraints ({@link #mMaxThumbLength} and // {@link #mMinThumbLength}). final int size = mFiller.getHeight() - mFiller.getPaddingTop() - mFiller.getPaddingBottom(); diff --git a/android/support/mediacompat/testlib/IntentConstants.java b/android/support/mediacompat/testlib/IntentConstants.java deleted file mode 100644 index a18bcf32..00000000 --- a/android/support/mediacompat/testlib/IntentConstants.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2017 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 android.support.mediacompat.testlib; - -/** - * Constants used for sending intent between client and service apps. - */ -public class IntentConstants { - public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD = - "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"; - public static final String ACTION_CALL_MEDIA_SESSION_METHOD = - "android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD"; - public static final String ACTION_CALL_MEDIA_CONTROLLER_METHOD = - "android.support.mediacompat.client.action.CALL_MEDIA_CONTROLLER_METHOD"; - public static final String ACTION_CALL_TRANSPORT_CONTROLS_METHOD = - "android.support.mediacompat.client.action.CALL_TRANSPORT_CONTROLS_METHOD"; - public static final String KEY_METHOD_ID = "method_id"; - public static final String KEY_ARGUMENT = "argument"; - public static final String KEY_SESSION_TOKEN = "session_token"; -} diff --git a/android/support/mediacompat/testlib/MediaSessionConstants.java b/android/support/mediacompat/testlib/MediaSessionConstants.java index 95be1621..cbdccc1b 100644 --- a/android/support/mediacompat/testlib/MediaSessionConstants.java +++ b/android/support/mediacompat/testlib/MediaSessionConstants.java @@ -40,7 +40,6 @@ public class MediaSessionConstants { public static final int SET_RATING_TYPE = 117; public static final String TEST_SESSION_TAG = "test-session-tag"; - public static final String SERVICE_PACKAGE_NAME = "android.support.mediacompat.service.test"; public static final String TEST_KEY = "test-key"; public static final String TEST_VALUE = "test-val"; public static final String TEST_SESSION_EVENT = "test-session-event"; diff --git a/android/support/mediacompat/testlib/VersionConstants.java b/android/support/mediacompat/testlib/VersionConstants.java new file mode 100644 index 00000000..6533ee17 --- /dev/null +++ b/android/support/mediacompat/testlib/VersionConstants.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 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 android.support.mediacompat.testlib; + +/** + * Constants for getting support library version information. + */ +public class VersionConstants { + public static final String KEY_CLIENT_VERSION = "client_version"; + public static final String KEY_SERVICE_VERSION = "service_version"; +} diff --git a/android/support/mediacompat/testlib/util/IntentUtil.java b/android/support/mediacompat/testlib/util/IntentUtil.java new file mode 100644 index 00000000..bbf97524 --- /dev/null +++ b/android/support/mediacompat/testlib/util/IntentUtil.java @@ -0,0 +1,131 @@ +/* + * Copyright 2017 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 android.support.mediacompat.testlib.util; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; + +import java.util.ArrayList; + +/** + * Methods and constants used for sending intent between client and service apps. + */ +public class IntentUtil { + + public static final ComponentName SERVICE_RECEIVER_COMPONENT_NAME = new ComponentName( + "android.support.mediacompat.service.test", + "android.support.mediacompat.service.ServiceBroadcastReceiver"); + public static final ComponentName CLIENT_RECEIVER_COMPONENT_NAME = new ComponentName( + "android.support.mediacompat.client.test", + "android.support.mediacompat.client.ClientBroadcastReceiver"); + + public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD = + "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"; + public static final String ACTION_CALL_MEDIA_SESSION_METHOD = + "android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD"; + public static final String ACTION_CALL_MEDIA_CONTROLLER_METHOD = + "android.support.mediacompat.client.action.CALL_MEDIA_CONTROLLER_METHOD"; + public static final String ACTION_CALL_TRANSPORT_CONTROLS_METHOD = + "android.support.mediacompat.client.action.CALL_TRANSPORT_CONTROLS_METHOD"; + + public static final String KEY_METHOD_ID = "method_id"; + public static final String KEY_ARGUMENT = "argument"; + public static final String KEY_SESSION_TOKEN = "session_token"; + + /** + * Calls a method of MediaBrowserService. Used by client app. + */ + public static void callMediaBrowserServiceMethod(int methodId, Object arg, Context context) { + Intent intent = createIntent(SERVICE_RECEIVER_COMPONENT_NAME, methodId, arg); + intent.setAction(ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD); + if (Build.VERSION.SDK_INT >= 16) { + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + } + context.sendBroadcast(intent); + } + + /** + * Calls a method of MediaSession. Used by client app. + */ + public static void callMediaSessionMethod(int methodId, Object arg, Context context) { + Intent intent = createIntent(SERVICE_RECEIVER_COMPONENT_NAME, methodId, arg); + intent.setAction(ACTION_CALL_MEDIA_SESSION_METHOD); + if (Build.VERSION.SDK_INT >= 16) { + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + } + context.sendBroadcast(intent); + } + + /** + * Calls a method of MediaController. Used by service app. + */ + public static void callMediaControllerMethod( + int methodId, Object arg, Context context, Parcelable token) { + Intent intent = createIntent(CLIENT_RECEIVER_COMPONENT_NAME, methodId, arg); + intent.setAction(ACTION_CALL_MEDIA_CONTROLLER_METHOD); + intent.putExtra(KEY_SESSION_TOKEN, token); + if (Build.VERSION.SDK_INT >= 16) { + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + } + context.sendBroadcast(intent); + } + + /** + * Calls a method of TransportControls. Used by service app. + */ + public static void callTransportControlsMethod( + int methodId, Object arg, Context context, Parcelable token) { + Intent intent = createIntent(CLIENT_RECEIVER_COMPONENT_NAME, methodId, arg); + intent.setAction(ACTION_CALL_TRANSPORT_CONTROLS_METHOD); + intent.putExtra(KEY_SESSION_TOKEN, token); + if (Build.VERSION.SDK_INT >= 16) { + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + } + context.sendBroadcast(intent); + } + + private static Intent createIntent(ComponentName componentName, int methodId, Object arg) { + Intent intent = new Intent(); + intent.setComponent(componentName); + intent.putExtra(KEY_METHOD_ID, methodId); + + if (arg instanceof String) { + intent.putExtra(KEY_ARGUMENT, (String) arg); + } else if (arg instanceof Integer) { + intent.putExtra(KEY_ARGUMENT, (int) arg); + } else if (arg instanceof Long) { + intent.putExtra(KEY_ARGUMENT, (long) arg); + } else if (arg instanceof Boolean) { + intent.putExtra(KEY_ARGUMENT, (boolean) arg); + } else if (arg instanceof Parcelable) { + intent.putExtra(KEY_ARGUMENT, (Parcelable) arg); + } else if (arg instanceof ArrayList<?>) { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(KEY_ARGUMENT, (ArrayList<? extends Parcelable>) arg); + intent.putExtras(bundle); + } else if (arg instanceof Bundle) { + Bundle bundle = new Bundle(); + bundle.putBundle(KEY_ARGUMENT, (Bundle) arg); + intent.putExtras(bundle); + } + return intent; + } +} diff --git a/android/support/mediacompat/testlib/util/PollingCheck.java b/android/support/mediacompat/testlib/util/PollingCheck.java new file mode 100644 index 00000000..3412da02 --- /dev/null +++ b/android/support/mediacompat/testlib/util/PollingCheck.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017 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 android.support.mediacompat.testlib.util; + +import junit.framework.Assert; + +/** + * Utility used for testing that allows to poll for a certain condition to happen within a timeout. + * (Copied from testutils/src/main/java/android/support/testutils/PollingCheck.java.) + */ +public abstract class PollingCheck { + private static final long DEFAULT_TIMEOUT = 3000; + private static final long TIME_SLICE = 50; + private final long mTimeout; + + /** + * The condition that the PollingCheck should use to proceed successfully. + */ + public interface PollingCheckCondition { + /** + * @return Whether the polling condition has been met. + */ + boolean canProceed(); + } + + public PollingCheck(long timeout) { + mTimeout = timeout; + } + + protected abstract boolean check(); + + /** + * Start running the polling check. + */ + public void run() { + if (check()) { + return; + } + + long timeout = mTimeout; + while (timeout > 0) { + try { + Thread.sleep(TIME_SLICE); + } catch (InterruptedException e) { + Assert.fail("unexpected InterruptedException"); + } + + if (check()) { + return; + } + + timeout -= TIME_SLICE; + } + + Assert.fail("unexpected timeout"); + } + + /** + * Instantiate and start polling for a given condition with a default 3000ms timeout. + * @param condition The condition to check for success. + */ + public static void waitFor(final PollingCheckCondition condition) { + new PollingCheck(DEFAULT_TIMEOUT) { + @Override + protected boolean check() { + return condition.canProceed(); + } + }.run(); + } + + /** + * Instantiate and start polling for a given condition. + * @param timeout Time out in ms + * @param condition The condition to check for success. + */ + public static void waitFor(long timeout, final PollingCheckCondition condition) { + new PollingCheck(timeout) { + @Override + protected boolean check() { + return condition.canProceed(); + } + }.run(); + } +} diff --git a/android/support/mediacompat/testlib/util/TestUtil.java b/android/support/mediacompat/testlib/util/TestUtil.java new file mode 100644 index 00000000..d105510c --- /dev/null +++ b/android/support/mediacompat/testlib/util/TestUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017 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 android.support.mediacompat.testlib.util; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertSame; + +import android.os.Bundle; + +/** + * Utility methods used for testing. + */ +public final class TestUtil { + + /** + * Asserts that two Bundles are equal. + */ + public static void assertBundleEquals(Bundle expected, Bundle observed) { + if (expected == null || observed == null) { + assertSame(expected, observed); + } + assertEquals(expected.size(), observed.size()); + for (String key : expected.keySet()) { + assertEquals(expected.get(key), observed.get(key)); + } + } +} diff --git a/android/support/text/emoji/widget/EmojiAppCompatEditText.java b/android/support/text/emoji/widget/EmojiAppCompatEditText.java index 87c17c20..0ae4ea04 100644 --- a/android/support/text/emoji/widget/EmojiAppCompatEditText.java +++ b/android/support/text/emoji/widget/EmojiAppCompatEditText.java @@ -21,6 +21,7 @@ import android.support.annotation.IntRange; import android.support.annotation.Nullable; import android.support.text.emoji.EmojiCompat; import android.support.v7.widget.AppCompatEditText; +import android.text.method.KeyListener; import android.util.AttributeSet; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -67,8 +68,11 @@ public class EmojiAppCompatEditText extends AppCompatEditText { } @Override - public void setKeyListener(android.text.method.KeyListener input) { - super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input)); + public void setKeyListener(@Nullable KeyListener keyListener) { + if (keyListener != null) { + keyListener = getEmojiEditTextHelper().getKeyListener(keyListener); + } + super.setKeyListener(keyListener); } @Override diff --git a/android/support/text/emoji/widget/EmojiEditText.java b/android/support/text/emoji/widget/EmojiEditText.java index a0e8a69e..70ca7a66 100644 --- a/android/support/text/emoji/widget/EmojiEditText.java +++ b/android/support/text/emoji/widget/EmojiEditText.java @@ -21,6 +21,7 @@ import android.os.Build; import android.support.annotation.IntRange; import android.support.annotation.Nullable; import android.support.text.emoji.EmojiCompat; +import android.text.method.KeyListener; import android.util.AttributeSet; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -73,8 +74,11 @@ public class EmojiEditText extends EditText { } @Override - public void setKeyListener(android.text.method.KeyListener keyListener) { - super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener)); + public void setKeyListener(@Nullable KeyListener keyListener) { + if (keyListener != null) { + keyListener = getEmojiEditTextHelper().getKeyListener(keyListener); + } + super.setKeyListener(keyListener); } @Override diff --git a/android/support/text/emoji/widget/EmojiExtractEditText.java b/android/support/text/emoji/widget/EmojiExtractEditText.java index ca1868e2..2e4d3caa 100644 --- a/android/support/text/emoji/widget/EmojiExtractEditText.java +++ b/android/support/text/emoji/widget/EmojiExtractEditText.java @@ -27,6 +27,7 @@ import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.text.emoji.EmojiCompat; import android.support.text.emoji.EmojiSpan; +import android.text.method.KeyListener; import android.util.AttributeSet; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -81,8 +82,11 @@ public class EmojiExtractEditText extends ExtractEditText { } @Override - public void setKeyListener(android.text.method.KeyListener keyListener) { - super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener)); + public void setKeyListener(@Nullable KeyListener keyListener) { + if (keyListener != null) { + keyListener = getEmojiEditTextHelper().getKeyListener(keyListener); + } + super.setKeyListener(keyListener); } @Override diff --git a/android/support/transition/Transition.java b/android/support/transition/Transition.java index 04cc57bd..9c198a94 100644 --- a/android/support/transition/Transition.java +++ b/android/support/transition/Transition.java @@ -1017,7 +1017,7 @@ public abstract class Transition implements Cloneable { */ @NonNull public Transition addTarget(@IdRes int targetId) { - if (targetId > 0) { + if (targetId != 0) { mTargetIds.add(targetId); } return this; @@ -1107,7 +1107,7 @@ public abstract class Transition implements Cloneable { */ @NonNull public Transition removeTarget(@IdRes int targetId) { - if (targetId > 0) { + if (targetId != 0) { mTargetIds.remove((Integer) targetId); } return this; diff --git a/android/support/v17/leanback/app/BaseFragment.java b/android/support/v17/leanback/app/BaseFragment.java index bdb213f2..ea460111 100644 --- a/android/support/v17/leanback/app/BaseFragment.java +++ b/android/support/v17/leanback/app/BaseFragment.java @@ -31,7 +31,9 @@ import android.view.ViewTreeObserver; /** * Base class for leanback Fragments. This class is not intended to be subclassed by apps. + * @deprecated use {@link BaseSupportFragment} */ +@Deprecated @SuppressWarnings("FragmentNotInstantiable") public class BaseFragment extends BrandedFragment { diff --git a/android/support/v17/leanback/app/BaseRowFragment.java b/android/support/v17/leanback/app/BaseRowFragment.java index 2d79f3e1..97a5b848 100644 --- a/android/support/v17/leanback/app/BaseRowFragment.java +++ b/android/support/v17/leanback/app/BaseRowFragment.java @@ -34,7 +34,9 @@ import android.view.ViewGroup; /** * An internal base class for a fragment containing a list of rows. + * @deprecated use {@link BaseRowSupportFragment} */ +@Deprecated abstract class BaseRowFragment extends Fragment { private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition"; private ObjectAdapter mAdapter; @@ -164,8 +166,10 @@ abstract class BaseRowFragment extends Fragment { * Set the presenter selector used to create and bind views. */ public final void setPresenterSelector(PresenterSelector presenterSelector) { - mPresenterSelector = presenterSelector; - updateAdapter(); + if (mPresenterSelector != presenterSelector) { + mPresenterSelector = presenterSelector; + updateAdapter(); + } } /** @@ -180,8 +184,10 @@ abstract class BaseRowFragment extends Fragment { * @param rowsAdapter Adapter that represents list of rows. */ public final void setAdapter(ObjectAdapter rowsAdapter) { - mAdapter = rowsAdapter; - updateAdapter(); + if (mAdapter != rowsAdapter) { + mAdapter = rowsAdapter; + updateAdapter(); + } } /** diff --git a/android/support/v17/leanback/app/BaseRowSupportFragment.java b/android/support/v17/leanback/app/BaseRowSupportFragment.java index dba78daf..6a477ab0 100644 --- a/android/support/v17/leanback/app/BaseRowSupportFragment.java +++ b/android/support/v17/leanback/app/BaseRowSupportFragment.java @@ -161,8 +161,10 @@ abstract class BaseRowSupportFragment extends Fragment { * Set the presenter selector used to create and bind views. */ public final void setPresenterSelector(PresenterSelector presenterSelector) { - mPresenterSelector = presenterSelector; - updateAdapter(); + if (mPresenterSelector != presenterSelector) { + mPresenterSelector = presenterSelector; + updateAdapter(); + } } /** @@ -177,8 +179,10 @@ abstract class BaseRowSupportFragment extends Fragment { * @param rowsAdapter Adapter that represents list of rows. */ public final void setAdapter(ObjectAdapter rowsAdapter) { - mAdapter = rowsAdapter; - updateAdapter(); + if (mAdapter != rowsAdapter) { + mAdapter = rowsAdapter; + updateAdapter(); + } } /** diff --git a/android/support/v17/leanback/app/BrandedFragment.java b/android/support/v17/leanback/app/BrandedFragment.java index 1f6ad299..415c13e0 100644 --- a/android/support/v17/leanback/app/BrandedFragment.java +++ b/android/support/v17/leanback/app/BrandedFragment.java @@ -33,7 +33,9 @@ import android.view.ViewGroup; /** * Fragment class for managing search and branding using a view that implements * {@link TitleViewAdapter.Provider}. + * @deprecated use {@link BrandedSupportFragment} */ +@Deprecated public class BrandedFragment extends Fragment { // BUNDLE attribute for title is showing diff --git a/android/support/v17/leanback/app/BrowseFragment.java b/android/support/v17/leanback/app/BrowseFragment.java index ae31c4fb..c561ea99 100644 --- a/android/support/v17/leanback/app/BrowseFragment.java +++ b/android/support/v17/leanback/app/BrowseFragment.java @@ -81,7 +81,9 @@ import java.util.Map; * The recommended theme to use with a BrowseFragment is * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}. * </p> + * @deprecated use {@link BrowseSupportFragment} */ +@Deprecated public class BrowseFragment extends BaseFragment { // BUNDLE attribute for saving header show/hide status when backstack is used: @@ -203,7 +205,9 @@ public class BrowseFragment extends BaseFragment { /** * Listener for transitions between browse headers and rows. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public static class BrowseTransitionListener { /** * Callback when headers transition starts. @@ -267,7 +271,9 @@ public class BrowseFragment extends BaseFragment { /** * Possible set of actions that {@link BrowseFragment} exposes to clients. Custom * fragments can interact with {@link BrowseFragment} using this interface. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public interface FragmentHost { /** * Fragments are required to invoke this callback once their view is created @@ -376,7 +382,9 @@ public class BrowseFragment extends BaseFragment { * and provide that through {@link MainFragmentAdapterRegistry}. * {@link MainFragmentAdapter} implementation can supply any fragment and override * just those interactions that makes sense. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public static class MainFragmentAdapter<T extends Fragment> { private boolean mScalingEnabled; private final T mFragment; @@ -466,7 +474,9 @@ public class BrowseFragment extends BaseFragment { * Interface to be implemented by all fragments for providing an instance of * {@link MainFragmentAdapter}. Both {@link RowsFragment} and custom fragment provided * against {@link PageRow} will need to implement this interface. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public interface MainFragmentAdapterProvider { /** * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseFragment} @@ -478,7 +488,9 @@ public class BrowseFragment extends BaseFragment { /** * Interface to be implemented by {@link RowsFragment} and its subclasses for providing * an instance of {@link MainFragmentRowsAdapter}. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public interface MainFragmentRowsAdapterProvider { /** * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseFragment} @@ -491,7 +503,9 @@ public class BrowseFragment extends BaseFragment { * This is used to pass information to {@link RowsFragment} or its subclasses. * {@link BrowseFragment} uses this interface to pass row based interaction events to * the target fragment. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public static class MainFragmentRowsAdapter<T extends Fragment> { private final T mFragment; @@ -570,14 +584,27 @@ public class BrowseFragment extends BaseFragment { } boolean oldIsPageRow = mIsPageRow; + Object oldPageRow = mPageRow; mIsPageRow = mCanShowHeaders && item instanceof PageRow; + mPageRow = mIsPageRow ? item : null; boolean swap; if (mMainFragment == null) { swap = true; } else { if (oldIsPageRow) { - swap = true; + if (mIsPageRow) { + if (oldPageRow == null) { + // fragment is restored, page row object not yet set, so just set the + // mPageRow object and there is no need to replace the fragment + swap = false; + } else { + // swap if page row object changes + swap = oldPageRow != mPageRow; + } + } else { + swap = true; + } } else { swap = mIsPageRow; } @@ -590,37 +617,45 @@ public class BrowseFragment extends BaseFragment { "Fragment must implement MainFragmentAdapterProvider"); } - mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) - .getMainFragmentAdapter(); - mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); - if (!mIsPageRow) { - if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { - mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider)mMainFragment) - .getMainFragmentRowsAdapter(); - } else { - mMainFragmentRowsAdapter = null; - } - mIsPageRow = mMainFragmentRowsAdapter == null; - } else { - mMainFragmentRowsAdapter = null; - } + setMainFragmentAdapter(); } return swap; } + void setMainFragmentAdapter() { + mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment) + .getMainFragmentAdapter(); + mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); + if (!mIsPageRow) { + if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { + setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment) + .getMainFragmentRowsAdapter()); + } else { + setMainFragmentRowsAdapter(null); + } + mIsPageRow = mMainFragmentRowsAdapter == null; + } else { + setMainFragmentRowsAdapter(null); + } + } + /** * Factory class responsible for creating fragment given the current item. {@link ListRow} * should return {@link RowsFragment} or its subclass whereas {@link PageRow} * can return any fragment class. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public abstract static class FragmentFactory<T extends Fragment> { public abstract T createFragment(Object row); } /** * FragmentFactory implementation for {@link ListRow}. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public static class ListRowFragmentFactory extends FragmentFactory<RowsFragment> { @Override public RowsFragment createFragment(Object row) { @@ -634,7 +669,9 @@ public class BrowseFragment extends BaseFragment { * handling {@link ListRow}. Developers can override that and also if they want to * use custom fragment, they can register a custom {@link FragmentFactory} * against {@link PageRow}. + * @deprecated use {@link BrowseSupportFragment} */ + @Deprecated public final static class MainFragmentAdapterRegistry { private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap<>(); private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory(); @@ -678,7 +715,8 @@ public class BrowseFragment extends BaseFragment { MainFragmentAdapter mMainFragmentAdapter; Fragment mMainFragment; HeadersFragment mHeadersFragment; - private MainFragmentRowsAdapter mMainFragmentRowsAdapter; + MainFragmentRowsAdapter mMainFragmentRowsAdapter; + ListRowDataAdapter mMainFragmentListRowDataAdapter; private ObjectAdapter mAdapter; private PresenterSelector mAdapterPresenter; @@ -701,6 +739,7 @@ public class BrowseFragment extends BaseFragment { private int mSelectedPosition = -1; private float mScaleFactor; boolean mIsPageRow; + Object mPageRow; private PresenterSelector mHeaderPresenterSelector; private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); @@ -820,11 +859,45 @@ public class BrowseFragment extends BaseFragment { return; } + updateMainFragmentRowsAdapter(); + mHeadersFragment.setAdapter(mAdapter); + } + + void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) { + if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) { + return; + } + // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter if (mMainFragmentRowsAdapter != null) { - mMainFragmentRowsAdapter.setAdapter( - adapter == null ? null : new ListRowDataAdapter(adapter)); + // RowsFragment cannot change click/select listeners after view created. + // The main fragment and adapter should be GCed as long as there is no reference from + // BrowseFragment to it. + mMainFragmentRowsAdapter.setAdapter(null); + } + mMainFragmentRowsAdapter = mainFragmentRowsAdapter; + if (mMainFragmentRowsAdapter != null) { + mMainFragmentRowsAdapter.setOnItemViewSelectedListener( + new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter)); + mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); + } + // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter + updateMainFragmentRowsAdapter(); + } + + /** + * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter. + * It also clears old mMainFragmentListRowDataAdapter. + */ + void updateMainFragmentRowsAdapter() { + if (mMainFragmentListRowDataAdapter != null) { + mMainFragmentListRowDataAdapter.detach(); + mMainFragmentListRowDataAdapter = null; + } + if (mMainFragmentRowsAdapter != null) { + mMainFragmentListRowDataAdapter = mAdapter == null + ? null : new ListRowDataAdapter(mAdapter); + mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter); } - mHeadersFragment.setAdapter(adapter); } public final MainFragmentAdapterRegistry getMainFragmentRegistry() { @@ -1144,7 +1217,8 @@ public class BrowseFragment extends BaseFragment { @Override public void onDestroyView() { - mMainFragmentRowsAdapter = null; + setMainFragmentRowsAdapter(null); + mPageRow = null; mMainFragmentAdapter = null; mMainFragment = null; mHeadersFragment = null; @@ -1198,26 +1272,17 @@ public class BrowseFragment extends BaseFragment { mHeadersFragment = (HeadersFragment) getChildFragmentManager() .findFragmentById(R.id.browse_headers_dock); mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame); - mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) - .getMainFragmentAdapter(); - mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); mIsPageRow = savedInstanceState != null && savedInstanceState.getBoolean(IS_PAGE_ROW, false); + // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is + // the case for restoring, later if setSelection() triggers a createMainFragment(), + // should not create fragment. mSelectedPosition = savedInstanceState != null ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0; - if (!mIsPageRow) { - if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { - mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment) - .getMainFragmentRowsAdapter(); - } else { - mMainFragmentRowsAdapter = null; - } - } else { - mMainFragmentRowsAdapter = null; - } + setMainFragmentAdapter(); } mHeadersFragment.setHeadersGone(!mCanShowHeaders); @@ -1242,8 +1307,6 @@ public class BrowseFragment extends BaseFragment { mScaleFrameLayout.setPivotX(0); mScaleFrameLayout.setPivotY(mContainerListAlignTop); - setupMainFragment(); - if (mBrandColorSet) { mHeadersFragment.setBackgroundColor(mBrandColor); } @@ -1270,17 +1333,6 @@ public class BrowseFragment extends BaseFragment { return root; } - private void setupMainFragment() { - if (mMainFragmentRowsAdapter != null) { - if (mAdapter != null) { - mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(mAdapter)); - } - mMainFragmentRowsAdapter.setOnItemViewSelectedListener( - new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter)); - mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); - } - } - void createHeadersTransition() { mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this), mShowingHeaders @@ -1470,10 +1522,10 @@ public class BrowseFragment extends BaseFragment { }; void onRowSelected(int position) { - if (position != mSelectedPosition) { - mSetSelectionRunnable.post( - position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); - } + // even position is same, it could be data changed, always post selection runnable + // to possibly swap main fragment. + mSetSelectionRunnable.post( + position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); } void setSelection(int position, boolean smooth) { @@ -1500,7 +1552,6 @@ public class BrowseFragment extends BaseFragment { if (createMainFragment(mAdapter, position)) { swapToMainFragment(); expandMainFragment(!(mCanShowHeaders && mShowingHeaders)); - setupMainFragment(); } } diff --git a/android/support/v17/leanback/app/BrowseSupportFragment.java b/android/support/v17/leanback/app/BrowseSupportFragment.java index 4a2502a8..c28064ca 100644 --- a/android/support/v17/leanback/app/BrowseSupportFragment.java +++ b/android/support/v17/leanback/app/BrowseSupportFragment.java @@ -567,14 +567,27 @@ public class BrowseSupportFragment extends BaseSupportFragment { } boolean oldIsPageRow = mIsPageRow; + Object oldPageRow = mPageRow; mIsPageRow = mCanShowHeaders && item instanceof PageRow; + mPageRow = mIsPageRow ? item : null; boolean swap; if (mMainFragment == null) { swap = true; } else { if (oldIsPageRow) { - swap = true; + if (mIsPageRow) { + if (oldPageRow == null) { + // fragment is restored, page row object not yet set, so just set the + // mPageRow object and there is no need to replace the fragment + swap = false; + } else { + // swap if page row object changes + swap = oldPageRow != mPageRow; + } + } else { + swap = true; + } } else { swap = mIsPageRow; } @@ -587,25 +600,29 @@ public class BrowseSupportFragment extends BaseSupportFragment { "Fragment must implement MainFragmentAdapterProvider"); } - mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) - .getMainFragmentAdapter(); - mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); - if (!mIsPageRow) { - if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { - mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider)mMainFragment) - .getMainFragmentRowsAdapter(); - } else { - mMainFragmentRowsAdapter = null; - } - mIsPageRow = mMainFragmentRowsAdapter == null; - } else { - mMainFragmentRowsAdapter = null; - } + setMainFragmentAdapter(); } return swap; } + void setMainFragmentAdapter() { + mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment) + .getMainFragmentAdapter(); + mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); + if (!mIsPageRow) { + if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { + setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment) + .getMainFragmentRowsAdapter()); + } else { + setMainFragmentRowsAdapter(null); + } + mIsPageRow = mMainFragmentRowsAdapter == null; + } else { + setMainFragmentRowsAdapter(null); + } + } + /** * Factory class responsible for creating fragment given the current item. {@link ListRow} * should return {@link RowsSupportFragment} or its subclass whereas {@link PageRow} @@ -675,7 +692,8 @@ public class BrowseSupportFragment extends BaseSupportFragment { MainFragmentAdapter mMainFragmentAdapter; Fragment mMainFragment; HeadersSupportFragment mHeadersSupportFragment; - private MainFragmentRowsAdapter mMainFragmentRowsAdapter; + MainFragmentRowsAdapter mMainFragmentRowsAdapter; + ListRowDataAdapter mMainFragmentListRowDataAdapter; private ObjectAdapter mAdapter; private PresenterSelector mAdapterPresenter; @@ -698,6 +716,7 @@ public class BrowseSupportFragment extends BaseSupportFragment { private int mSelectedPosition = -1; private float mScaleFactor; boolean mIsPageRow; + Object mPageRow; private PresenterSelector mHeaderPresenterSelector; private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); @@ -817,11 +836,45 @@ public class BrowseSupportFragment extends BaseSupportFragment { return; } + updateMainFragmentRowsAdapter(); + mHeadersSupportFragment.setAdapter(mAdapter); + } + + void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) { + if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) { + return; + } + // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter + if (mMainFragmentRowsAdapter != null) { + // RowsFragment cannot change click/select listeners after view created. + // The main fragment and adapter should be GCed as long as there is no reference from + // BrowseSupportFragment to it. + mMainFragmentRowsAdapter.setAdapter(null); + } + mMainFragmentRowsAdapter = mainFragmentRowsAdapter; + if (mMainFragmentRowsAdapter != null) { + mMainFragmentRowsAdapter.setOnItemViewSelectedListener( + new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter)); + mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); + } + // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter + updateMainFragmentRowsAdapter(); + } + + /** + * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter. + * It also clears old mMainFragmentListRowDataAdapter. + */ + void updateMainFragmentRowsAdapter() { + if (mMainFragmentListRowDataAdapter != null) { + mMainFragmentListRowDataAdapter.detach(); + mMainFragmentListRowDataAdapter = null; + } if (mMainFragmentRowsAdapter != null) { - mMainFragmentRowsAdapter.setAdapter( - adapter == null ? null : new ListRowDataAdapter(adapter)); + mMainFragmentListRowDataAdapter = mAdapter == null + ? null : new ListRowDataAdapter(mAdapter); + mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter); } - mHeadersSupportFragment.setAdapter(adapter); } public final MainFragmentAdapterRegistry getMainFragmentRegistry() { @@ -1141,7 +1194,8 @@ public class BrowseSupportFragment extends BaseSupportFragment { @Override public void onDestroyView() { - mMainFragmentRowsAdapter = null; + setMainFragmentRowsAdapter(null); + mPageRow = null; mMainFragmentAdapter = null; mMainFragment = null; mHeadersSupportFragment = null; @@ -1195,26 +1249,17 @@ public class BrowseSupportFragment extends BaseSupportFragment { mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager() .findFragmentById(R.id.browse_headers_dock); mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame); - mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) - .getMainFragmentAdapter(); - mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); mIsPageRow = savedInstanceState != null && savedInstanceState.getBoolean(IS_PAGE_ROW, false); + // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is + // the case for restoring, later if setSelection() triggers a createMainFragment(), + // should not create fragment. mSelectedPosition = savedInstanceState != null ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0; - if (!mIsPageRow) { - if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { - mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment) - .getMainFragmentRowsAdapter(); - } else { - mMainFragmentRowsAdapter = null; - } - } else { - mMainFragmentRowsAdapter = null; - } + setMainFragmentAdapter(); } mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders); @@ -1239,8 +1284,6 @@ public class BrowseSupportFragment extends BaseSupportFragment { mScaleFrameLayout.setPivotX(0); mScaleFrameLayout.setPivotY(mContainerListAlignTop); - setupMainFragment(); - if (mBrandColorSet) { mHeadersSupportFragment.setBackgroundColor(mBrandColor); } @@ -1267,17 +1310,6 @@ public class BrowseSupportFragment extends BaseSupportFragment { return root; } - private void setupMainFragment() { - if (mMainFragmentRowsAdapter != null) { - if (mAdapter != null) { - mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(mAdapter)); - } - mMainFragmentRowsAdapter.setOnItemViewSelectedListener( - new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter)); - mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); - } - } - void createHeadersTransition() { mHeadersTransition = TransitionHelper.loadTransition(getContext(), mShowingHeaders @@ -1467,10 +1499,10 @@ public class BrowseSupportFragment extends BaseSupportFragment { }; void onRowSelected(int position) { - if (position != mSelectedPosition) { - mSetSelectionRunnable.post( - position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); - } + // even position is same, it could be data changed, always post selection runnable + // to possibly swap main fragment. + mSetSelectionRunnable.post( + position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); } void setSelection(int position, boolean smooth) { @@ -1497,7 +1529,6 @@ public class BrowseSupportFragment extends BaseSupportFragment { if (createMainFragment(mAdapter, position)) { swapToMainFragment(); expandMainFragment(!(mCanShowHeaders && mShowingHeaders)); - setupMainFragment(); } } diff --git a/android/support/v17/leanback/app/DetailsFragment.java b/android/support/v17/leanback/app/DetailsFragment.java index 36559637..18934f45 100644 --- a/android/support/v17/leanback/app/DetailsFragment.java +++ b/android/support/v17/leanback/app/DetailsFragment.java @@ -91,7 +91,9 @@ import java.lang.ref.WeakReference; * DetailsFragment can use {@link DetailsFragmentBackgroundController} to add a parallax drawable * background and embedded video playing fragment. * </p> + * @deprecated use {@link DetailsSupportFragment} */ +@Deprecated public class DetailsFragment extends BaseFragment { static final String TAG = "DetailsFragment"; static boolean DEBUG = false; diff --git a/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java index 223b8ef2..25ed723e 100644 --- a/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java +++ b/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java @@ -107,7 +107,9 @@ import android.app.Fragment; * {@link #onCreateGlueHost()}. * </p> * + * @deprecated use {@link DetailsSupportFragmentBackgroundController} */ +@Deprecated public class DetailsFragmentBackgroundController { final DetailsFragment mFragment; diff --git a/android/support/v17/leanback/app/ErrorFragment.java b/android/support/v17/leanback/app/ErrorFragment.java index 2896d0f4..eda0de16 100644 --- a/android/support/v17/leanback/app/ErrorFragment.java +++ b/android/support/v17/leanback/app/ErrorFragment.java @@ -32,7 +32,9 @@ import android.widget.TextView; /** * A fragment for displaying an error indication. + * @deprecated use {@link ErrorSupportFragment} */ +@Deprecated public class ErrorFragment extends BrandedFragment { private ViewGroup mErrorFrame; diff --git a/android/support/v17/leanback/app/GuidedStepFragment.java b/android/support/v17/leanback/app/GuidedStepFragment.java index 2b7f2d0d..9be350d8 100644 --- a/android/support/v17/leanback/app/GuidedStepFragment.java +++ b/android/support/v17/leanback/app/GuidedStepFragment.java @@ -27,6 +27,7 @@ import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import android.support.v17.leanback.R; import android.support.v17.leanback.transition.TransitionHelper; +import android.support.v17.leanback.widget.DiffCallback; import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; @@ -140,7 +141,9 @@ import java.util.List; * @see GuidanceStylist.Guidance * @see GuidedAction * @see GuidedActionsStylist + * @deprecated use {@link GuidedStepSupportFragment} */ +@Deprecated public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.FocusListener { private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment"; @@ -806,6 +809,8 @@ public class GuidedStepFragment extends Fragment implements GuidedActionAdapter. /** * Sets the list of GuidedActions that the user may take in this fragment. + * Uses DiffCallback set by {@link #setActionsDiffCallback(DiffCallback)}. + * * @param actions The list of GuidedActions for this fragment. */ public void setActions(List<GuidedAction> actions) { @@ -816,6 +821,18 @@ public class GuidedStepFragment extends Fragment implements GuidedActionAdapter. } /** + * Sets the RecyclerView DiffCallback used when {@link #setActions(List)} is called. By default + * GuidedStepFragment uses + * {@link android.support.v17.leanback.widget.GuidedActionDiffCallback}. + * Sets it to null if app wants to refresh the whole list. + * + * @param diffCallback DiffCallback used in {@link #setActions(List)}. + */ + public void setActionsDiffCallback(DiffCallback<GuidedAction> diffCallback) { + mAdapter.setDiffCallback(diffCallback); + } + + /** * Notify an action has changed and update its UI. * @param position Position of the GuidedAction in array. */ diff --git a/android/support/v17/leanback/app/GuidedStepSupportFragment.java b/android/support/v17/leanback/app/GuidedStepSupportFragment.java index aeb2d334..e276d076 100644 --- a/android/support/v17/leanback/app/GuidedStepSupportFragment.java +++ b/android/support/v17/leanback/app/GuidedStepSupportFragment.java @@ -24,6 +24,7 @@ import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import android.support.v17.leanback.R; import android.support.v17.leanback.transition.TransitionHelper; +import android.support.v17.leanback.widget.DiffCallback; import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; @@ -803,6 +804,8 @@ public class GuidedStepSupportFragment extends Fragment implements GuidedActionA /** * Sets the list of GuidedActions that the user may take in this fragment. + * Uses DiffCallback set by {@link #setActionsDiffCallback(DiffCallback)}. + * * @param actions The list of GuidedActions for this fragment. */ public void setActions(List<GuidedAction> actions) { @@ -813,6 +816,18 @@ public class GuidedStepSupportFragment extends Fragment implements GuidedActionA } /** + * Sets the RecyclerView DiffCallback used when {@link #setActions(List)} is called. By default + * GuidedStepSupportFragment uses + * {@link android.support.v17.leanback.widget.GuidedActionDiffCallback}. + * Sets it to null if app wants to refresh the whole list. + * + * @param diffCallback DiffCallback used in {@link #setActions(List)}. + */ + public void setActionsDiffCallback(DiffCallback<GuidedAction> diffCallback) { + mAdapter.setDiffCallback(diffCallback); + } + + /** * Notify an action has changed and update its UI. * @param position Position of the GuidedAction in array. */ diff --git a/android/support/v17/leanback/app/HeadersFragment.java b/android/support/v17/leanback/app/HeadersFragment.java index dd037d2f..08780a50 100644 --- a/android/support/v17/leanback/app/HeadersFragment.java +++ b/android/support/v17/leanback/app/HeadersFragment.java @@ -52,12 +52,16 @@ import android.widget.FrameLayout; * </ul> * Use {@link #setPresenterSelector(PresenterSelector)} in subclass constructor to customize * Presenters. App may override {@link BrowseFragment#onCreateHeadersFragment()}. + * @deprecated use {@link HeadersSupportFragment} */ +@Deprecated public class HeadersFragment extends BaseRowFragment { /** * Interface definition for a callback to be invoked when a header item is clicked. + * @deprecated use {@link HeadersSupportFragment} */ + @Deprecated public interface OnHeaderClickedListener { /** * Called when a header item has been clicked. @@ -70,7 +74,9 @@ public class HeadersFragment extends BaseRowFragment { /** * Interface definition for a callback to be invoked when a header item is selected. + * @deprecated use {@link HeadersSupportFragment} */ + @Deprecated public interface OnHeaderViewSelectedListener { /** * Called when a header item has been selected. diff --git a/android/support/v17/leanback/app/ListRowDataAdapter.java b/android/support/v17/leanback/app/ListRowDataAdapter.java index f9af12f3..03d948be 100644 --- a/android/support/v17/leanback/app/ListRowDataAdapter.java +++ b/android/support/v17/leanback/app/ListRowDataAdapter.java @@ -13,6 +13,7 @@ import android.support.v17.leanback.widget.Row; * thinks there are items even though they're invisible. This class takes care of filtering out * the invisible rows at the end. In case the data inside the adapter changes, it adjusts the * bounds to reflect the latest data. + * {@link #detach()} must be called to release DataObserver from Adapter. */ class ListRowDataAdapter extends ObjectAdapter { public static final int ON_ITEM_RANGE_CHANGED = 2; @@ -22,6 +23,7 @@ class ListRowDataAdapter extends ObjectAdapter { private final ObjectAdapter mAdapter; int mLastVisibleRowIndex; + final DataObserver mDataObserver; public ListRowDataAdapter(ObjectAdapter adapter) { super(adapter.getPresenterSelector()); @@ -34,10 +36,20 @@ class ListRowDataAdapter extends ObjectAdapter { // operation. To handle this case, we use QueueBasedDataObserver which forces // recyclerview to do a full data refresh after each update operation. if (adapter.isImmediateNotifySupported()) { - mAdapter.registerObserver(new SimpleDataObserver()); + mDataObserver = new SimpleDataObserver(); } else { - mAdapter.registerObserver(new QueueBasedDataObserver()); + mDataObserver = new QueueBasedDataObserver(); } + attach(); + } + + void detach() { + mAdapter.unregisterObserver(mDataObserver); + } + + void attach() { + initialize(); + mAdapter.registerObserver(mDataObserver); } void initialize() { diff --git a/android/support/v17/leanback/app/OnboardingFragment.java b/android/support/v17/leanback/app/OnboardingFragment.java index b69d5a72..f352c413 100644 --- a/android/support/v17/leanback/app/OnboardingFragment.java +++ b/android/support/v17/leanback/app/OnboardingFragment.java @@ -154,7 +154,9 @@ import java.util.List; * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle + * @deprecated use {@link OnboardingSupportFragment} */ +@Deprecated abstract public class OnboardingFragment extends Fragment { private static final String TAG = "OnboardingF"; private static final boolean DEBUG = false; diff --git a/android/support/v17/leanback/app/PlaybackFragment.java b/android/support/v17/leanback/app/PlaybackFragment.java index 33e787c3..e2e6be48 100644 --- a/android/support/v17/leanback/app/PlaybackFragment.java +++ b/android/support/v17/leanback/app/PlaybackFragment.java @@ -81,7 +81,9 @@ import android.view.animation.AccelerateInterpolator; * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will * be cancelled upon {@link #tickle()} triggered by input event. * </p> + * @deprecated use {@link PlaybackSupportFragment} */ +@Deprecated public class PlaybackFragment extends Fragment { static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview"; @@ -181,7 +183,9 @@ public class PlaybackFragment extends Fragment { * Listener allowing the application to receive notification of fade in and/or fade out * completion events. * @hide + * @deprecated use {@link PlaybackSupportFragment} */ + @Deprecated public static class OnFadeCompleteListener { public void onFadeInComplete() { } diff --git a/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java b/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java index 4a9d10f8..9e342fdb 100644 --- a/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java +++ b/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java @@ -30,7 +30,9 @@ import android.view.View; /** * {@link PlaybackGlueHost} implementation * the interaction between this class and {@link PlaybackFragment}. + * @deprecated use {@link PlaybackSupportFragmentGlueHost} */ +@Deprecated public class PlaybackFragmentGlueHost extends PlaybackGlueHost implements PlaybackSeekUi { private final PlaybackFragment mFragment; diff --git a/android/support/v17/leanback/app/RowsFragment.java b/android/support/v17/leanback/app/RowsFragment.java index a008ad60..aa346bd9 100644 --- a/android/support/v17/leanback/app/RowsFragment.java +++ b/android/support/v17/leanback/app/RowsFragment.java @@ -53,7 +53,9 @@ import java.util.ArrayList; * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses * of {@link RowPresenter}. * </p> + * @deprecated use {@link RowsSupportFragment} */ +@Deprecated public class RowsFragment extends BaseRowFragment implements BrowseFragment.MainFragmentRowsAdapterProvider, BrowseFragment.MainFragmentAdapterProvider { @@ -634,7 +636,9 @@ public class RowsFragment extends BaseRowFragment implements * The adapter that RowsFragment implements * BrowseFragment.MainFragmentRowsAdapter. * @see #getMainFragmentRowsAdapter(). + * @deprecated use {@link RowsSupportFragment} */ + @Deprecated public static class MainFragmentRowsAdapter extends BrowseFragment.MainFragmentRowsAdapter<RowsFragment> { diff --git a/android/support/v17/leanback/app/SearchFragment.java b/android/support/v17/leanback/app/SearchFragment.java index 2154ff28..00f2cca8 100644 --- a/android/support/v17/leanback/app/SearchFragment.java +++ b/android/support/v17/leanback/app/SearchFragment.java @@ -66,7 +66,9 @@ import java.util.List; * not when fragment is restored from an instance state. Activity may manually * call {@link #startRecognition()}, typically in onNewIntent(). * </p> + * @deprecated use {@link SearchSupportFragment} */ +@Deprecated public class SearchFragment extends Fragment { static final String TAG = SearchFragment.class.getSimpleName(); static final boolean DEBUG = false; diff --git a/android/support/v17/leanback/app/VerticalGridFragment.java b/android/support/v17/leanback/app/VerticalGridFragment.java index 5bc52ff5..bff3dbab 100644 --- a/android/support/v17/leanback/app/VerticalGridFragment.java +++ b/android/support/v17/leanback/app/VerticalGridFragment.java @@ -39,7 +39,9 @@ import android.view.ViewGroup; * * <p>Renders a vertical grid of objects given a {@link VerticalGridPresenter} and * an {@link ObjectAdapter}. + * @deprecated use {@link VerticalGridSupportFragment} */ +@Deprecated public class VerticalGridFragment extends BaseFragment { static final String TAG = "VerticalGF"; static boolean DEBUG = false; diff --git a/android/support/v17/leanback/app/VideoFragment.java b/android/support/v17/leanback/app/VideoFragment.java index 1b2b8d07..e4d75f30 100644 --- a/android/support/v17/leanback/app/VideoFragment.java +++ b/android/support/v17/leanback/app/VideoFragment.java @@ -27,7 +27,9 @@ import android.view.ViewGroup; /** * Subclass of {@link PlaybackFragment} that is responsible for providing a {@link SurfaceView} * and rendering video. + * @deprecated use {@link VideoSupportFragment} */ +@Deprecated public class VideoFragment extends PlaybackFragment { static final int SURFACE_NOT_CREATED = 0; static final int SURFACE_CREATED = 1; diff --git a/android/support/v17/leanback/app/VideoFragmentGlueHost.java b/android/support/v17/leanback/app/VideoFragmentGlueHost.java index d123676f..546e581c 100644 --- a/android/support/v17/leanback/app/VideoFragmentGlueHost.java +++ b/android/support/v17/leanback/app/VideoFragmentGlueHost.java @@ -24,7 +24,9 @@ import android.view.SurfaceHolder; /** * {@link PlaybackGlueHost} implementation * the interaction between {@link PlaybackGlue} and {@link VideoFragment}. + * @deprecated use {@link VideoSupportFragmentGlueHost} */ +@Deprecated public class VideoFragmentGlueHost extends PlaybackFragmentGlueHost implements SurfaceHolderGlueHost { private final VideoFragment mFragment; diff --git a/android/support/v17/leanback/widget/ArrayObjectAdapter.java b/android/support/v17/leanback/widget/ArrayObjectAdapter.java index 00bc073d..2dcf51f7 100644 --- a/android/support/v17/leanback/widget/ArrayObjectAdapter.java +++ b/android/support/v17/leanback/widget/ArrayObjectAdapter.java @@ -225,6 +225,8 @@ public class ArrayObjectAdapter extends ObjectAdapter { return true; } + ListUpdateCallback mListUpdateCallback; + /** * Set a new item list to adapter. The DiffUtil will compute the difference and dispatch it to * specified position. @@ -280,39 +282,43 @@ public class ArrayObjectAdapter extends ObjectAdapter { mItems.addAll(itemList); // dispatch diff result - diffResult.dispatchUpdatesTo(new ListUpdateCallback() { - - @Override - public void onInserted(int position, int count) { - if (DEBUG) { - Log.d(TAG, "onInserted"); + if (mListUpdateCallback == null) { + mListUpdateCallback = new ListUpdateCallback() { + + @Override + public void onInserted(int position, int count) { + if (DEBUG) { + Log.d(TAG, "onInserted"); + } + notifyItemRangeInserted(position, count); } - notifyItemRangeInserted(position, count); - } - @Override - public void onRemoved(int position, int count) { - if (DEBUG) { - Log.d(TAG, "onRemoved"); + @Override + public void onRemoved(int position, int count) { + if (DEBUG) { + Log.d(TAG, "onRemoved"); + } + notifyItemRangeRemoved(position, count); } - notifyItemRangeRemoved(position, count); - } - @Override - public void onMoved(int fromPosition, int toPosition) { - if (DEBUG) { - Log.d(TAG, "onMoved"); + @Override + public void onMoved(int fromPosition, int toPosition) { + if (DEBUG) { + Log.d(TAG, "onMoved"); + } + notifyItemMoved(fromPosition, toPosition); } - notifyItemMoved(fromPosition, toPosition); - } - @Override - public void onChanged(int position, int count, Object payload) { - if (DEBUG) { - Log.d(TAG, "onChanged"); + @Override + public void onChanged(int position, int count, Object payload) { + if (DEBUG) { + Log.d(TAG, "onChanged"); + } + notifyItemRangeChanged(position, count, payload); } - notifyItemRangeChanged(position, count, payload); - } - }); + }; + } + diffResult.dispatchUpdatesTo(mListUpdateCallback); + mOldItems.clear(); } } diff --git a/android/support/v17/leanback/widget/BaseGridView.java b/android/support/v17/leanback/widget/BaseGridView.java index f4e01c0b..2ebec47e 100644 --- a/android/support/v17/leanback/widget/BaseGridView.java +++ b/android/support/v17/leanback/widget/BaseGridView.java @@ -1134,7 +1134,7 @@ public abstract class BaseGridView extends RecyclerView { @Override public void scrollToPosition(int position) { // dont abort the animateOut() animation, just record the position - if (mLayoutManager.mIsSlidingChildViews) { + if (mLayoutManager.isSlidingChildViews()) { mLayoutManager.setSelectionWithSub(position, 0, 0); return; } @@ -1144,7 +1144,7 @@ public abstract class BaseGridView extends RecyclerView { @Override public void smoothScrollToPosition(int position) { // dont abort the animateOut() animation, just record the position - if (mLayoutManager.mIsSlidingChildViews) { + if (mLayoutManager.isSlidingChildViews()) { mLayoutManager.setSelectionWithSub(position, 0, 0); return; } diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java index dded0715..d7020e91 100644 --- a/android/support/v17/leanback/widget/GridLayoutManager.java +++ b/android/support/v17/leanback/widget/GridLayoutManager.java @@ -217,9 +217,9 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mFocusPosition = getTargetPosition(); } if (hasFocus()) { - mInSelection = true; + mFlag |= PF_IN_SELECTION; targetView.requestFocus(); - mInSelection = false; + mFlag &= ~PF_IN_SELECTION; } dispatchChildSelected(); dispatchChildSelectedAndPositioned(); @@ -320,9 +320,9 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } } if (newSelected != null && hasFocus()) { - mInSelection = true; + mFlag |= PF_IN_SELECTION; newSelected.requestFocus(); - mInSelection = false; + mFlag &= ~PF_IN_SELECTION; } } @@ -355,7 +355,8 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { if (mPendingMoves == 0) { return null; } - int direction = (mReverseFlowPrimary ? mPendingMoves > 0 : mPendingMoves < 0) + int direction = ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 + ? mPendingMoves > 0 : mPendingMoves < 0) ? -1 : 1; if (mOrientation == HORIZONTAL) { return new PointF(direction, 0); @@ -386,10 +387,6 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { // effect smooth scrolling too over to bind an item view then drag the item view back. final static int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30; - // Represents whether child views are temporarily sliding out - boolean mIsSlidingChildViews; - boolean mLayoutEatenInSliding; - String getTag() { return TAG + ":" + mBaseGridView.getId(); } @@ -444,15 +441,101 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { private static final Rect sTempRect = new Rect(); - boolean mInLayout; - private boolean mInScroll; - boolean mInFastRelayout; + // 2 bits mask is for 3 STAGEs: 0, PF_STAGE_LAYOUT or PF_STAGE_SCROLL. + static final int PF_STAGE_MASK = 0x3; + static final int PF_STAGE_LAYOUT = 0x1; + static final int PF_STAGE_SCROLL = 0x2; + + // Flag for "in fast relayout", determined by layoutInit() result. + static final int PF_FAST_RELAYOUT = 1 << 2; + + // Flag for the selected item being updated in fast relayout. + static final int PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION = 1 << 3; /** * During full layout pass, when GridView had focus: onLayoutChildren will * skip non-focusable child and adjust mFocusPosition. */ - boolean mInLayoutSearchFocus; - boolean mInSelection = false; + static final int PF_IN_LAYOUT_SEARCH_FOCUS = 1 << 4; + + // flag to prevent reentry if it's already processing selection request. + static final int PF_IN_SELECTION = 1 << 5; + + // Represents whether child views are temporarily sliding out + static final int PF_SLIDING = 1 << 6; + static final int PF_LAYOUT_EATEN_IN_SLIDING = 1 << 7; + + /** + * Force a full layout under certain situations. E.g. Rows change, jump to invisible child. + */ + static final int PF_FORCE_FULL_LAYOUT = 1 << 8; + + /** + * True if layout is enabled. + */ + static final int PF_LAYOUT_ENABLED = 1 << 9; + + /** + * Flag controlling whether the current/next layout should + * be updating the secondary size of rows. + */ + static final int PF_ROW_SECONDARY_SIZE_REFRESH = 1 << 10; + + /** + * Allow DPAD key to navigate out at the front of the View (where position = 0), + * default is false. + */ + static final int PF_FOCUS_OUT_FRONT = 1 << 11; + + /** + * Allow DPAD key to navigate out at the end of the view, default is false. + */ + static final int PF_FOCUS_OUT_END = 1 << 12; + + static final int PF_FOCUS_OUT_MASKS = PF_FOCUS_OUT_FRONT | PF_FOCUS_OUT_END; + + /** + * Allow DPAD key to navigate out of second axis. + * default is true. + */ + static final int PF_FOCUS_OUT_SIDE_START = 1 << 13; + + /** + * Allow DPAD key to navigate out of second axis. + */ + static final int PF_FOCUS_OUT_SIDE_END = 1 << 14; + + static final int PF_FOCUS_OUT_SIDE_MASKS = PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END; + + /** + * True if focus search is disabled. + */ + static final int PF_FOCUS_SEARCH_DISABLED = 1 << 15; + + /** + * True if prune child, might be disabled during transition. + */ + static final int PF_PRUNE_CHILD = 1 << 16; + + /** + * True if scroll content, might be disabled during transition. + */ + static final int PF_SCROLL_ENABLED = 1 << 17; + + /** + * Set to true for RTL layout in horizontal orientation + */ + static final int PF_REVERSE_FLOW_PRIMARY = 1 << 18; + + /** + * Set to true for RTL layout in vertical orientation + */ + static final int PF_REVERSE_FLOW_SECONDARY = 1 << 19; + + static final int PF_REVERSE_FLOW_MASK = PF_REVERSE_FLOW_PRIMARY | PF_REVERSE_FLOW_SECONDARY; + + int mFlag = PF_LAYOUT_ENABLED + | PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END + | PF_PRUNE_CHILD | PF_SCROLL_ENABLED; private OnChildSelectedListener mChildSelectedListener = null; @@ -493,16 +576,6 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { private int mPrimaryScrollExtra; /** - * Force a full layout under certain situations. E.g. Rows change, jump to invisible child. - */ - private boolean mForceFullLayout; - - /** - * True if layout is enabled. - */ - private boolean mLayoutEnabled = true; - - /** * override child visibility */ @Visibility @@ -535,12 +608,6 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { private int[] mRowSizeSecondary; /** - * Flag controlling whether the current/next layout should - * be updating the secondary size of rows. - */ - private boolean mRowSecondarySizeRefresh; - - /** * The maximum measured size of the view. */ private int mMaxSizeSecondary; @@ -605,58 +672,11 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { private int mExtraLayoutSpace; /** - * Allow DPAD key to navigate out at the front of the View (where position = 0), - * default is false. - */ - private boolean mFocusOutFront; - - /** - * Allow DPAD key to navigate out at the end of the view, default is false. - */ - private boolean mFocusOutEnd; - - /** - * Allow DPAD key to navigate out of second axis. - * default is true. - */ - private boolean mFocusOutSideStart = true; - - /** - * Allow DPAD key to navigate out of second axis. - */ - private boolean mFocusOutSideEnd = true; - - /** - * True if focus search is disabled. - */ - private boolean mFocusSearchDisabled; - - /** - * True if prune child, might be disabled during transition. - */ - private boolean mPruneChild = true; - - /** - * True if scroll content, might be disabled during transition. - */ - private boolean mScrollEnabled = true; - - /** * Temporary variable: an int array of length=2. */ static int[] sTwoInts = new int[2]; /** - * Set to true for RTL layout in horizontal orientation - */ - boolean mReverseFlowPrimary = false; - - /** - * Set to true for RTL layout in vertical orientation - */ - private boolean mReverseFlowSecondary = false; - - /** * Temporaries used for measuring. */ private int[] mMeasuredDimension = new int[2]; @@ -685,24 +705,21 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); mWindowAlignment.setOrientation(orientation); mItemAlignment.setOrientation(orientation); - mForceFullLayout = true; + mFlag |= PF_FORCE_FULL_LAYOUT; } public void onRtlPropertiesChanged(int layoutDirection) { - boolean reversePrimary, reverseSecondary; + final int flags; if (mOrientation == HORIZONTAL) { - reversePrimary = layoutDirection == View.LAYOUT_DIRECTION_RTL; - reverseSecondary = false; + flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_PRIMARY : 0; } else { - reverseSecondary = layoutDirection == View.LAYOUT_DIRECTION_RTL; - reversePrimary = false; + flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_SECONDARY : 0; } - if (mReverseFlowPrimary == reversePrimary && mReverseFlowSecondary == reverseSecondary) { + if ((mFlag & PF_REVERSE_FLOW_MASK) == flags) { return; } - mReverseFlowPrimary = reversePrimary; - mReverseFlowSecondary = reverseSecondary; - mForceFullLayout = true; + mFlag = (mFlag & ~PF_REVERSE_FLOW_MASK) | flags; + mFlag |= PF_FORCE_FULL_LAYOUT; mWindowAlignment.horizontal.setReversedFlow(layoutDirection == View.LAYOUT_DIRECTION_RTL); } @@ -775,13 +792,15 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) { - mFocusOutFront = throughFront; - mFocusOutEnd = throughEnd; + mFlag = (mFlag & ~PF_FOCUS_OUT_MASKS) + | (throughFront ? PF_FOCUS_OUT_FRONT : 0) + | (throughEnd ? PF_FOCUS_OUT_END : 0); } public void setFocusOutSideAllowed(boolean throughStart, boolean throughEnd) { - mFocusOutSideStart = throughStart; - mFocusOutSideEnd = throughEnd; + mFlag = (mFlag & ~PF_FOCUS_OUT_SIDE_MASKS) + | (throughStart ? PF_FOCUS_OUT_SIDE_START : 0) + | (throughEnd ? PF_FOCUS_OUT_SIDE_END : 0); } public void setNumRows(int numRows) { @@ -971,7 +990,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { // layout warning. // If not in layout, we may be scrolling in which case the child layout request will be // eaten by recyclerview. Post a requestLayout. - if (!mInLayout && !mBaseGridView.isLayoutRequested()) { + if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && !mBaseGridView.isLayoutRequested()) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { if (getChildAt(i).isLayoutRequested()) { @@ -1177,19 +1196,19 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mSubFocusPosition = 0; } if (!mState.didStructureChange() && mGrid != null && mGrid.getFirstVisibleIndex() >= 0 - && !mForceFullLayout && mGrid.getNumRows() == mNumRows) { + && (mFlag & PF_FORCE_FULL_LAYOUT) == 0 && mGrid.getNumRows() == mNumRows) { updateScrollController(); updateSecondaryScrollLimits(); mGrid.setSpacing(mSpacingPrimary); return true; } else { - mForceFullLayout = false; + mFlag &= ~PF_FORCE_FULL_LAYOUT; if (mGrid == null || mNumRows != mGrid.getNumRows() - || mReverseFlowPrimary != mGrid.isReversedFlow()) { + || ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) != mGrid.isReversedFlow()) { mGrid = Grid.createGrid(mNumRows); mGrid.setProvider(mGridProvider); - mGrid.setReversedFlow(mReverseFlowPrimary); + mGrid.setReversedFlow((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0); } initScrollController(); updateSecondaryScrollLimits(); @@ -1216,7 +1235,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { int start = 0; // Iterate from left to right, which is a different index traversal // in RTL flow - if (mReverseFlowSecondary) { + if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) { for (int i = mNumRows-1; i > rowIndex; i--) { start += getRowSizeSecondary(i) + mSpacingSecondary; } @@ -1229,7 +1248,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } private int getSizeSecondary() { - int rightmostIndex = mReverseFlowSecondary ? 0 : mNumRows - 1; + int rightmostIndex = (mFlag & PF_REVERSE_FLOW_SECONDARY) != 0 ? 0 : mNumRows - 1; return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex); } @@ -1366,8 +1385,9 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { * Checks if we need to update row secondary sizes. */ private void updateRowSecondarySizeRefresh() { - mRowSecondarySizeRefresh = processRowSizeSecondary(false); - if (mRowSecondarySizeRefresh) { + mFlag = (mFlag & ~PF_ROW_SECONDARY_SIZE_REFRESH) + | (processRowSizeSecondary(false) ? PF_ROW_SECONDARY_SIZE_REFRESH : 0); + if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) { if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set"); forceRequestLayout(); } @@ -1599,7 +1619,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout(); } int subindex = getSubPositionByView(v, v.findFocus()); - if (!mInLayout) { + if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) { // when we are appending item during scroll pass and the item's position // matches the mFocusPosition, we should signal a childSelected event. // However if we are still running PendingMoveSmoothScroller, we defer and @@ -1610,20 +1630,20 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { && mPendingMoveSmoothScroller == null) { dispatchChildSelected(); } - } else if (!mInFastRelayout) { + } else if ((mFlag & PF_FAST_RELAYOUT) == 0) { // fastRelayout will dispatch event at end of onLayoutChildren(). // For full layout, two situations here: // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition. // 2. mInLayoutSearchFocus is true: dispatchChildSelected() on first child // equal to or after mFocusPosition that can take focus. - if (!mInLayoutSearchFocus && index == mFocusPosition + if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) == 0 && index == mFocusPosition && subindex == mSubFocusPosition) { dispatchChildSelected(); - } else if (mInLayoutSearchFocus && index >= mFocusPosition + } else if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) != 0 && index >= mFocusPosition && v.hasFocusable()) { mFocusPosition = index; mSubFocusPosition = subindex; - mInLayoutSearchFocus = false; + mFlag &= ~PF_IN_LAYOUT_SEARCH_FOCUS; dispatchChildSelected(); } } @@ -1663,7 +1683,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { if (!mState.isPreLayout()) { updateScrollLimits(); } - if (!mInLayout && mPendingMoveSmoothScroller != null) { + if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && mPendingMoveSmoothScroller != null) { mPendingMoveSmoothScroller.consumePendingMovesAfterLayout(); } if (mChildLaidOutListener != null) { @@ -1677,7 +1697,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { public void removeItem(int index) { if (TRACE) TraceCompat.beginSection("removeItem"); View v = findViewByPosition(index - mPositionDeltaInPreLayout); - if (mInLayout) { + if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) { detachAndScrapView(v, mRecycler); } else { removeAndRecycleView(v, mRecycler); @@ -1688,7 +1708,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { @Override public int getEdge(int index) { View v = findViewByPosition(index - mPositionDeltaInPreLayout); - return mReverseFlowPrimary ? getViewMax(v) : getViewMin(v); + return (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? getViewMax(v) : getViewMin(v); } @Override @@ -1705,7 +1725,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary); } final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; - final int horizontalGravity = (mReverseFlowPrimary || mReverseFlowSecondary) + final int horizontalGravity = (mFlag & PF_REVERSE_FLOW_MASK) != 0 ? Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, View.LAYOUT_DIRECTION_RTL) : mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; @@ -1781,16 +1801,16 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } private void removeInvisibleViewsAtEnd() { - if (mPruneChild && !mIsSlidingChildViews) { - mGrid.removeInvisibleItemsAtEnd(mFocusPosition, - mReverseFlowPrimary ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace); + if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) { + mGrid.removeInvisibleItemsAtEnd(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 + ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace); } } private void removeInvisibleViewsAtFront() { - if (mPruneChild && !mIsSlidingChildViews) { - mGrid.removeInvisibleItemsAtFront(mFocusPosition, - mReverseFlowPrimary ? mSizePrimary + mExtraLayoutSpace: -mExtraLayoutSpace); + if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) { + mGrid.removeInvisibleItemsAtFront(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 + ? mSizePrimary + mExtraLayoutSpace : -mExtraLayoutSpace); } } @@ -1799,16 +1819,16 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } void slideIn() { - if (mIsSlidingChildViews) { - mIsSlidingChildViews = false; + if ((mFlag & PF_SLIDING) != 0) { + mFlag &= ~PF_SLIDING; if (mFocusPosition >= 0) { scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra); } else { - mLayoutEatenInSliding = false; + mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING; requestLayout(); } - if (mLayoutEatenInSliding) { - mLayoutEatenInSliding = false; + if ((mFlag & PF_LAYOUT_EATEN_IN_SLIDING) != 0) { + mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING; if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) { mBaseGridView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -1838,7 +1858,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } } } else { - if (mReverseFlowPrimary) { + if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) { distance = getWidth(); if (getChildCount() > 0) { int start = getChildAt(0).getRight(); @@ -1861,14 +1881,18 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { return distance; } + boolean isSlidingChildViews() { + return (mFlag & PF_SLIDING) != 0; + } + /** * Temporarily slide out child and block layout and scroll requests. */ void slideOut() { - if (mIsSlidingChildViews) { + if ((mFlag & PF_SLIDING) != 0) { return; } - mIsSlidingChildViews = true; + mFlag |= PF_SLIDING; if (getChildCount() == 0) { return; } @@ -1886,13 +1910,13 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } private void appendVisibleItems() { - mGrid.appendVisibleItems(mReverseFlowPrimary + mGrid.appendVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout : mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout); } private void prependVisibleItems() { - mGrid.prependVisibleItems(mReverseFlowPrimary + mGrid.prependVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout : -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout); } @@ -1907,6 +1931,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { final int childCount = getChildCount(); int position = mGrid.getFirstVisibleIndex(); int index = 0; + mFlag &= ~PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION; for (; index < childCount; index++, position++) { View view = getChildAt(index); // We don't hit fastRelayout() if State.didStructure() is true, but prelayout may add @@ -1932,6 +1957,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (lp.viewNeedsUpdate()) { + mFlag |= PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION; detachAndScrapView(view, mRecycler); view = getViewForPosition(position); addView(view, index); @@ -1960,7 +1986,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { detachAndScrapView(v, mRecycler); } mGrid.invalidateItemsAfter(position); - if (mPruneChild) { + if ((mFlag & PF_PRUNE_CHILD) != 0) { // in regular prune child mode, we just append items up to edge limit appendVisibleItems(); if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) { @@ -2108,7 +2134,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " inPreLayout " + state.isPreLayout() + " didStructureChange " + state.didStructureChange() - + " mForceFullLayout " + mForceFullLayout); + + " mForceFullLayout " + ((mFlag & PF_FORCE_FULL_LAYOUT) != 0)); Log.v(getTag(), "width " + getWidth() + " height " + getHeight()); } @@ -2121,20 +2147,20 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { return; } - if (mIsSlidingChildViews) { + if ((mFlag & PF_SLIDING) != 0) { // if there is already children, delay the layout process until slideIn(), if it's // first time layout children: scroll them offscreen at end of onLayoutChildren() if (getChildCount() > 0) { - mLayoutEatenInSliding = true; + mFlag |= PF_LAYOUT_EATEN_IN_SLIDING; return; } } - if (!mLayoutEnabled) { + if ((mFlag & PF_LAYOUT_ENABLED) == 0) { discardLayoutInfo(); removeAndRecycleAllViews(recycler); return; } - mInLayout = true; + mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_LAYOUT; saveContext(recycler, state); if (state.isPreLayout()) { @@ -2172,7 +2198,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { appendVisibleItems(); prependVisibleItems(); } - mInLayout = false; + mFlag &= ~PF_STAGE_MASK; leaveContext(); if (DEBUG) Log.v(getTag(), "layoutChildren end"); return; @@ -2206,13 +2232,16 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { deltaSecondary = state.getRemainingScrollHorizontal(); deltaPrimary = state.getRemainingScrollVertical(); } - if (mInFastRelayout = layoutInit()) { + if (layoutInit()) { + mFlag |= PF_FAST_RELAYOUT; // If grid view is empty, we will start from mFocusPosition mGrid.setStart(mFocusPosition); fastRelayout(); } else { + mFlag &= ~PF_FAST_RELAYOUT; // layoutInit() has detached all views, so start from scratch - mInLayoutSearchFocus = hadFocus; + mFlag = (mFlag & ~PF_IN_LAYOUT_SEARCH_FOCUS) + | (hadFocus ? PF_IN_LAYOUT_SEARCH_FOCUS : 0); int startFromPosition, endPos; if (scrollToFocus && (firstVisibleIndex < 0 || mFocusPosition > lastVisibleIndex || mFocusPosition < firstVisibleIndex)) { @@ -2270,27 +2299,30 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { Log.d(getTag(), sw.toString()); } - if (mRowSecondarySizeRefresh) { - mRowSecondarySizeRefresh = false; + if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) { + mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH; } else { updateRowSecondarySizeRefresh(); } - // For fastRelayout, only dispatch event when focus position changes. - if (mInFastRelayout && (mFocusPosition != savedFocusPos || mSubFocusPosition - != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView)) { + // For fastRelayout, only dispatch event when focus position changes or selected item + // being updated. + if ((mFlag & PF_FAST_RELAYOUT) != 0 && (mFocusPosition != savedFocusPos || mSubFocusPosition + != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView + || (mFlag & PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION) != 0)) { dispatchChildSelected(); - } else if (!mInFastRelayout && mInLayoutSearchFocus) { + } else if ((mFlag & (PF_FAST_RELAYOUT | PF_IN_LAYOUT_SEARCH_FOCUS)) + == PF_IN_LAYOUT_SEARCH_FOCUS) { // For full layout we dispatchChildSelected() in createItem() unless searched all // children and found none is focusable then dispatchChildSelected() here. dispatchChildSelected(); } dispatchChildSelectedAndPositioned(); - if (mIsSlidingChildViews) { + if ((mFlag & PF_SLIDING) != 0) { scrollDirectionPrimary(getSlideOutDistance()); } - mInLayout = false; + mFlag &= ~PF_STAGE_MASK; leaveContext(); if (DEBUG) Log.v(getTag(), "layoutChildren end"); } @@ -2324,11 +2356,11 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { @Override public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) { if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx); - if (!mLayoutEnabled || !hasDoneFirstLayout()) { + if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) { return 0; } saveContext(recycler, state); - mInScroll = true; + mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL; int result; if (mOrientation == HORIZONTAL) { result = scrollDirectionPrimary(dx); @@ -2336,17 +2368,17 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { result = scrollDirectionSecondary(dx); } leaveContext(); - mInScroll = false; + mFlag &= ~PF_STAGE_MASK; return result; } @Override public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) { if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy); - if (!mLayoutEnabled || !hasDoneFirstLayout()) { + if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) { return 0; } - mInScroll = true; + mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL; saveContext(recycler, state); int result; if (mOrientation == VERTICAL) { @@ -2355,7 +2387,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { result = scrollDirectionSecondary(dy); } leaveContext(); - mInScroll = false; + mFlag &= ~PF_STAGE_MASK; return result; } @@ -2367,7 +2399,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { // 2. During onLayoutChildren(), it may compensate the remaining scroll delta, // we should honor the request regardless if it goes over minScroll / maxScroll. // (see b/64931938 testScrollAndRemove and testScrollAndRemoveSample1) - if (!mIsSlidingChildViews && !mInLayout) { + if ((mFlag & PF_SLIDING) == 0 && (mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) { if (da > 0) { if (!mWindowAlignment.mainAxis().isMaxUnknown()) { int maxScroll = mWindowAlignment.mainAxis().getMaxScroll(); @@ -2389,7 +2421,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { return 0; } offsetChildrenPrimary(-da); - if (mInLayout) { + if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) { updateScrollLimits(); if (TRACE) TraceCompat.endSection(); return da; @@ -2398,7 +2430,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { int childCount = getChildCount(); boolean updated; - if (mReverseFlowPrimary ? da > 0 : da < 0) { + if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) { prependVisibleItems(); } else { appendVisibleItems(); @@ -2407,7 +2439,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { childCount = getChildCount(); if (TRACE) TraceCompat.beginSection("remove"); - if (mReverseFlowPrimary ? da > 0 : da < 0) { + if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) { removeInvisibleViewsAtEnd(); } else { removeInvisibleViewsAtFront(); @@ -2476,7 +2508,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } int highVisiblePos, lowVisiblePos; int highMaxPos, lowMinPos; - if (!mReverseFlowPrimary) { + if ((mFlag & PF_REVERSE_FLOW_PRIMARY) == 0) { highVisiblePos = mGrid.getLastVisibleIndex(); highMaxPos = mState.getItemCount() - 1; lowVisiblePos = mGrid.getFirstVisibleIndex(); @@ -2614,14 +2646,14 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { // scrollToView() is based on Adapter position. Only call scrollToView() when item // is still valid. if (view != null && getAdapterPositionByView(view) == position) { - mInSelection = true; + mFlag |= PF_IN_SELECTION; scrollToView(view, smooth); - mInSelection = false; + mFlag &= ~PF_IN_SELECTION; } else { mFocusPosition = position; mSubFocusPosition = subposition; mFocusPositionOffset = Integer.MIN_VALUE; - if (!mLayoutEnabled || mIsSlidingChildViews) { + if ((mFlag & PF_LAYOUT_ENABLED) == 0 || (mFlag & PF_SLIDING) != 0) { return; } if (smooth) { @@ -2637,7 +2669,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mSubFocusPosition = 0; } } else { - mForceFullLayout = true; + mFlag |= PF_FORCE_FULL_LAYOUT; requestLayout(); } } @@ -2654,7 +2686,8 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { final int firstChildPos = getPosition(getChildAt(0)); // TODO We should be able to deduce direction from bounds of current and target // focus, rather than making assumptions about positions and directionality - final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos + final boolean isStart = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 + ? targetPosition > firstChildPos : targetPosition < firstChildPos; final int direction = isStart ? -1 : 1; if (mOrientation == HORIZONTAL) { @@ -2788,14 +2821,14 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { @Override public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) { - if (mFocusSearchDisabled) { + if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) { return true; } if (getAdapterPositionByView(child) == NO_POSITION) { // This is could be the last view in DISAPPEARING animation. return true; } - if (!mInLayout && !mInSelection && !mInScroll) { + if ((mFlag & (PF_STAGE_MASK | PF_IN_SELECTION)) == 0) { scrollToView(child, focused, true); } return true; @@ -2865,7 +2898,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { */ private void scrollToView(View view, View childView, boolean smooth, int extraDelta, int extraDeltaSecondary) { - if (mIsSlidingChildViews) { + if ((mFlag & PF_SLIDING) != 0) { return; } int newFocusPosition = getAdapterPositionByView(view); @@ -2874,7 +2907,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mFocusPosition = newFocusPosition; mSubFocusPosition = newSubFocusPosition; mFocusPositionOffset = 0; - if (!mInLayout) { + if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) { dispatchChildSelected(); } if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) { @@ -2889,7 +2922,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { // by setSelection()) view.requestFocus(); } - if (!mScrollEnabled && smooth) { + if ((mFlag & PF_SCROLL_ENABLED) == 0 && smooth) { return; } if (getScrollPosition(view, childView, sTwoInts) @@ -3007,7 +3040,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) { - if (mInLayout) { + if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) { scrollDirectionPrimary(scrollPrimary); scrollDirectionSecondary(scrollSecondary); } else { @@ -3030,22 +3063,23 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } public void setPruneChild(boolean pruneChild) { - if (mPruneChild != pruneChild) { - mPruneChild = pruneChild; - if (mPruneChild) { + if (((mFlag & PF_PRUNE_CHILD) != 0) != pruneChild) { + mFlag = (mFlag & ~PF_PRUNE_CHILD) | (pruneChild ? PF_PRUNE_CHILD : 0); + if (pruneChild) { requestLayout(); } } } public boolean getPruneChild() { - return mPruneChild; + return (mFlag & PF_PRUNE_CHILD) != 0; } public void setScrollEnabled(boolean scrollEnabled) { - if (mScrollEnabled != scrollEnabled) { - mScrollEnabled = scrollEnabled; - if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED + if (((mFlag & PF_SCROLL_ENABLED) != 0) != scrollEnabled) { + mFlag = (mFlag & ~PF_SCROLL_ENABLED) | (scrollEnabled ? PF_SCROLL_ENABLED : 0); + if (((mFlag & PF_SCROLL_ENABLED) != 0) + && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED && mFocusPosition != NO_POSITION) { scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra); @@ -3054,7 +3088,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } public boolean isScrollEnabled() { - return mScrollEnabled; + return (mFlag & PF_SCROLL_ENABLED) != 0; } private int findImmediateChildIndex(View view) { @@ -3088,16 +3122,16 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } void setFocusSearchDisabled(boolean disabled) { - mFocusSearchDisabled = disabled; + mFlag = (mFlag & ~PF_FOCUS_SEARCH_DISABLED) | (disabled ? PF_FOCUS_SEARCH_DISABLED : 0); } boolean isFocusSearchDisabled() { - return mFocusSearchDisabled; + return (mFlag & PF_FOCUS_SEARCH_DISABLED) != 0; } @Override public View onInterceptFocusSearch(View focused, int direction) { - if (mFocusSearchDisabled) { + if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) { return focused; } @@ -3132,27 +3166,27 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { int movement = getMovement(direction); final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE; if (movement == NEXT_ITEM) { - if (isScroll || !mFocusOutEnd) { + if (isScroll || (mFlag & PF_FOCUS_OUT_END) == 0) { result = focused; } - if (mScrollEnabled && !hasCreatedLastItem()) { + if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedLastItem()) { processPendingMovement(true); result = focused; } } else if (movement == PREV_ITEM) { - if (isScroll || !mFocusOutFront) { + if (isScroll || (mFlag & PF_FOCUS_OUT_FRONT) == 0) { result = focused; } - if (mScrollEnabled && !hasCreatedFirstItem()) { + if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedFirstItem()) { processPendingMovement(false); result = focused; } } else if (movement == NEXT_ROW) { - if (isScroll || !mFocusOutSideEnd) { + if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_END) == 0) { result = focused; } } else if (movement == PREV_ROW) { - if (isScroll || !mFocusOutSideStart) { + if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_START) == 0) { result = focused; } } @@ -3191,7 +3225,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { @Override public boolean onAddFocusables(RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) { - if (mFocusSearchDisabled) { + if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) { return true; } // If this viewgroup or one of its children currently has focus then we @@ -3423,10 +3457,10 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { if (mOrientation == HORIZONTAL) { switch(direction) { case View.FOCUS_LEFT: - movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM; + movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? PREV_ITEM : NEXT_ITEM; break; case View.FOCUS_RIGHT: - movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM; + movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? NEXT_ITEM : PREV_ITEM; break; case View.FOCUS_UP: movement = PREV_ROW; @@ -3438,10 +3472,10 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { } else if (mOrientation == VERTICAL) { switch(direction) { case View.FOCUS_LEFT: - movement = (!mReverseFlowSecondary) ? PREV_ROW : NEXT_ROW; + movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? PREV_ROW : NEXT_ROW; break; case View.FOCUS_RIGHT: - movement = (!mReverseFlowSecondary) ? NEXT_ROW : PREV_ROW; + movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? NEXT_ROW : PREV_ROW; break; case View.FOCUS_UP: movement = PREV_ITEM; @@ -3497,12 +3531,12 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { private void discardLayoutInfo() { mGrid = null; mRowSizeSecondary = null; - mRowSecondarySizeRefresh = false; + mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH; } public void setLayoutEnabled(boolean layoutEnabled) { - if (mLayoutEnabled != layoutEnabled) { - mLayoutEnabled = layoutEnabled; + if (((mFlag & PF_LAYOUT_ENABLED) != 0) != layoutEnabled) { + mFlag = (mFlag & ~PF_LAYOUT_ENABLED) | (layoutEnabled ? PF_LAYOUT_ENABLED : 0); requestLayout(); } } @@ -3592,7 +3626,7 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { mFocusPosition = loadingState.index; mFocusPositionOffset = 0; mChildrenStates.loadFromBundle(loadingState.childStates); - mForceFullLayout = true; + mFlag |= PF_FORCE_FULL_LAYOUT; requestLayout(); if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition); } @@ -3699,9 +3733,9 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { if (newSelected != null) { if (preventScroll) { if (hasFocus()) { - mInSelection = true; + mFlag |= PF_IN_SELECTION; newSelected.requestFocus(); - mInSelection = false; + mFlag &= ~PF_IN_SELECTION; } mFocusPosition = focusPosition; mSubFocusPosition = 0; @@ -3717,11 +3751,11 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { AccessibilityNodeInfoCompat info) { saveContext(recycler, state); int count = state.getItemCount(); - if (mScrollEnabled && count > 1 && !isItemFullyVisible(0)) { + if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(0)) { info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); info.setScrollable(true); } - if (mScrollEnabled && count > 1 && !isItemFullyVisible(count - 1)) { + if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(count - 1)) { info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); info.setScrollable(true); } diff --git a/android/support/v17/leanback/widget/GuidedActionAdapter.java b/android/support/v17/leanback/widget/GuidedActionAdapter.java index 5b755f5e..51b29e21 100644 --- a/android/support/v17/leanback/widget/GuidedActionAdapter.java +++ b/android/support/v17/leanback/widget/GuidedActionAdapter.java @@ -15,7 +15,9 @@ package android.support.v17.leanback.widget; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; +import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; +import android.support.v7.util.DiffUtil; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ViewHolder; import android.util.Log; @@ -103,6 +105,7 @@ public class GuidedActionAdapter extends RecyclerView.Adapter { private ClickListener mClickListener; final GuidedActionsStylist mStylist; GuidedActionAdapterGroup mGroup; + DiffCallback<GuidedAction> mDiffCallback; private final View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override @@ -145,20 +148,78 @@ public class GuidedActionAdapter extends RecyclerView.Adapter { mActionOnFocusListener = new ActionOnFocusListener(focusListener); mActionEditListener = new ActionEditListener(); mIsSubAdapter = isSubAdapter; + if (!isSubAdapter) { + mDiffCallback = GuidedActionDiffCallback.getInstance(); + } + } + + /** + * Change DiffCallback used in {@link #setActions(List)}. Set to null for firing a + * general {@link #notifyDataSetChanged()}. + * + * @param diffCallback + */ + public void setDiffCallback(DiffCallback<GuidedAction> diffCallback) { + mDiffCallback = diffCallback; } /** - * Sets the list of actions managed by this adapter. + * Sets the list of actions managed by this adapter. Use {@link #setDiffCallback(DiffCallback)} + * to change DiffCallback. * @param actions The list of actions to be managed. */ - public void setActions(List<GuidedAction> actions) { + public void setActions(final List<GuidedAction> actions) { if (!mIsSubAdapter) { mStylist.collapseAction(false); } mActionOnFocusListener.unFocus(); - mActions.clear(); - mActions.addAll(actions); - notifyDataSetChanged(); + if (mDiffCallback != null) { + // temporary variable used for DiffCallback + final List<GuidedAction> oldActions = new ArrayList(); + oldActions.addAll(mActions); + + // update items. + mActions.clear(); + mActions.addAll(actions); + + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return oldActions.size(); + } + + @Override + public int getNewListSize() { + return mActions.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mDiffCallback.areItemsTheSame(oldActions.get(oldItemPosition), + mActions.get(newItemPosition)); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return mDiffCallback.areContentsTheSame(oldActions.get(oldItemPosition), + mActions.get(newItemPosition)); + } + + @Nullable + @Override + public Object getChangePayload(int oldItemPosition, int newItemPosition) { + return mDiffCallback.getChangePayload(oldActions.get(oldItemPosition), + mActions.get(newItemPosition)); + } + }); + + // dispatch diff result + diffResult.dispatchUpdatesTo(this); + } else { + mActions.clear(); + mActions.addAll(actions); + notifyDataSetChanged(); + } } /** diff --git a/android/support/v17/leanback/widget/GuidedActionDiffCallback.java b/android/support/v17/leanback/widget/GuidedActionDiffCallback.java new file mode 100644 index 00000000..d4d4d77a --- /dev/null +++ b/android/support/v17/leanback/widget/GuidedActionDiffCallback.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 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 android.support.v17.leanback.widget; + +import android.support.annotation.NonNull; +import android.text.TextUtils; + +/** + * DiffCallback used for GuidedActions, see {@link + * android.support.v17.leanback.app.GuidedStepSupportFragment#setActionsDiffCallback(DiffCallback)}. + */ +public class GuidedActionDiffCallback extends DiffCallback<GuidedAction> { + + static final GuidedActionDiffCallback sInstance = new GuidedActionDiffCallback(); + + /** + * Returns the singleton GuidedActionDiffCallback. + * @return The singleton GuidedActionDiffCallback. + */ + public static final GuidedActionDiffCallback getInstance() { + return sInstance; + } + + @Override + public boolean areItemsTheSame(@NonNull GuidedAction oldItem, @NonNull GuidedAction newItem) { + if (oldItem == null) { + return newItem == null; + } else if (newItem == null) { + return false; + } + return oldItem.getId() == newItem.getId(); + } + + @Override + public boolean areContentsTheSame(@NonNull GuidedAction oldItem, + @NonNull GuidedAction newItem) { + if (oldItem == null) { + return newItem == null; + } else if (newItem == null) { + return false; + } + return oldItem.getCheckSetId() == newItem.getCheckSetId() + && oldItem.mActionFlags == newItem.mActionFlags + && TextUtils.equals(oldItem.getTitle(), newItem.getTitle()) + && TextUtils.equals(oldItem.getDescription(), newItem.getDescription()) + && oldItem.getInputType() == newItem.getInputType() + && TextUtils.equals(oldItem.getEditTitle(), newItem.getEditTitle()) + && TextUtils.equals(oldItem.getEditDescription(), newItem.getEditDescription()) + && oldItem.getEditInputType() == newItem.getEditInputType() + && oldItem.getDescriptionEditInputType() == newItem.getDescriptionEditInputType(); + } +} diff --git a/android/support/v17/leanback/widget/ObjectAdapter.java b/android/support/v17/leanback/widget/ObjectAdapter.java index 535f81b4..d411f9e7 100644 --- a/android/support/v17/leanback/widget/ObjectAdapter.java +++ b/android/support/v17/leanback/widget/ObjectAdapter.java @@ -13,7 +13,10 @@ */ package android.support.v17.leanback.widget; +import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; + import android.database.Observable; +import android.support.annotation.RestrictTo; /** * Base class adapter to be used in leanback activities. Provides access to a data model and is @@ -132,6 +135,10 @@ public abstract class ObjectAdapter { mObservers.get(i).onItemMoved(positionStart, toPosition); } } + + boolean hasObserver() { + return mObservers.size() > 0; + } } private final DataObservable mObservable = new DataObservable(); @@ -207,6 +214,14 @@ public abstract class ObjectAdapter { } /** + * @hide + */ + @RestrictTo(LIBRARY_GROUP) + public final boolean hasObserver() { + return mObservable.hasObserver(); + } + + /** * Unregisters all DataObservers for this ObjectAdapter. */ public final void unregisterAllObservers() { diff --git a/android/support/v4/app/FragmentActivity.java b/android/support/v4/app/FragmentActivity.java index 614ff351..78161a87 100644 --- a/android/support/v4/app/FragmentActivity.java +++ b/android/support/v4/app/FragmentActivity.java @@ -536,7 +536,7 @@ public class FragmentActivity extends BaseFragmentActivityApi16 implements @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - markState(getSupportFragmentManager(), Lifecycle.State.CREATED); + markFragmentsCreated(); Parcelable p = mFragments.saveAllState(); if (p != null) { outState.putParcelable(FRAGMENTS_TAG, p); @@ -591,7 +591,7 @@ public class FragmentActivity extends BaseFragmentActivityApi16 implements super.onStop(); mStopped = true; - markState(getSupportFragmentManager(), Lifecycle.State.CREATED); + markFragmentsCreated(); mHandler.sendEmptyMessage(MSG_REALLY_STOPPED); mFragments.dispatchStop(); @@ -970,18 +970,30 @@ public class FragmentActivity extends BaseFragmentActivityApi16 implements } } - private static void markState(FragmentManager manager, Lifecycle.State state) { + private void markFragmentsCreated() { + boolean reiterate; + do { + reiterate = markState(getSupportFragmentManager(), Lifecycle.State.CREATED); + } while (reiterate); + } + + private static boolean markState(FragmentManager manager, Lifecycle.State state) { + boolean hadNotMarked = false; Collection<Fragment> fragments = manager.getFragments(); for (Fragment fragment : fragments) { if (fragment == null) { continue; } - fragment.mLifecycleRegistry.markState(state); + if (fragment.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { + fragment.mLifecycleRegistry.markState(state); + hadNotMarked = true; + } FragmentManager childFragmentManager = fragment.peekChildFragmentManager(); if (childFragmentManager != null) { - markState(childFragmentManager, state); + hadNotMarked |= markState(childFragmentManager, state); } } + return hadNotMarked; } } diff --git a/android/support/v4/graphics/TypefaceCompat.java b/android/support/v4/graphics/TypefaceCompat.java index 3c55df62..734f1837 100644 --- a/android/support/v4/graphics/TypefaceCompat.java +++ b/android/support/v4/graphics/TypefaceCompat.java @@ -35,7 +35,6 @@ import android.support.v4.content.res.ResourcesCompat; import android.support.v4.provider.FontsContractCompat; import android.support.v4.provider.FontsContractCompat.FontInfo; import android.support.v4.util.LruCache; - /** * Helper for accessing features in {@link Typeface}. * @hide @@ -46,7 +45,9 @@ public class TypefaceCompat { private static final TypefaceCompatImpl sTypefaceCompatImpl; static { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + sTypefaceCompatImpl = new TypefaceCompatApi28Impl(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { sTypefaceCompatImpl = new TypefaceCompatApi26Impl(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && TypefaceCompatApi24Impl.isUsable()) { diff --git a/android/support/v4/graphics/TypefaceCompatApi26Impl.java b/android/support/v4/graphics/TypefaceCompatApi26Impl.java index 1b55a2e0..00e31a1a 100644 --- a/android/support/v4/graphics/TypefaceCompatApi26Impl.java +++ b/android/support/v4/graphics/TypefaceCompatApi26Impl.java @@ -60,76 +60,69 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { "createFromFamiliesWithDefault"; private static final String FREEZE_METHOD = "freeze"; private static final String ABORT_CREATION_METHOD = "abortCreation"; - private static final Class sFontFamily; - private static final Constructor sFontFamilyCtor; - private static final Method sAddFontFromAssetManager; - private static final Method sAddFontFromBuffer; - private static final Method sFreeze; - private static final Method sAbortCreation; - private static final Method sCreateFromFamiliesWithDefault; private static final int RESOLVE_BY_FONT_TABLE = -1; - static { - Class fontFamilyClass; + protected final Class mFontFamily; + protected final Constructor mFontFamilyCtor; + protected final Method mAddFontFromAssetManager; + protected final Method mAddFontFromBuffer; + protected final Method mFreeze; + protected final Method mAbortCreation; + protected final Method mCreateFromFamiliesWithDefault; + + public TypefaceCompatApi26Impl() { + Class fontFamily; Constructor fontFamilyCtor; - Method addFontMethod; - Method addFromBufferMethod; - Method freezeMethod; - Method abortCreationMethod; - Method createFromFamiliesWithDefaultMethod; + Method addFontFromAssetManager; + Method addFontFromBuffer; + Method freeze; + Method abortCreation; + Method createFromFamiliesWithDefault; try { - fontFamilyClass = Class.forName(FONT_FAMILY_CLASS); - fontFamilyCtor = fontFamilyClass.getConstructor(); - addFontMethod = fontFamilyClass.getMethod(ADD_FONT_FROM_ASSET_MANAGER_METHOD, - AssetManager.class, String.class, Integer.TYPE, Boolean.TYPE, Integer.TYPE, - Integer.TYPE, Integer.TYPE, FontVariationAxis[].class); - addFromBufferMethod = fontFamilyClass.getMethod(ADD_FONT_FROM_BUFFER_METHOD, - ByteBuffer.class, Integer.TYPE, FontVariationAxis[].class, Integer.TYPE, - Integer.TYPE); - freezeMethod = fontFamilyClass.getMethod(FREEZE_METHOD); - abortCreationMethod = fontFamilyClass.getMethod(ABORT_CREATION_METHOD); - Object familyArray = Array.newInstance(fontFamilyClass, 1); - createFromFamiliesWithDefaultMethod = - Typeface.class.getDeclaredMethod(CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD, - familyArray.getClass(), Integer.TYPE, Integer.TYPE); - createFromFamiliesWithDefaultMethod.setAccessible(true); + fontFamily = obtainFontFamily(); + fontFamilyCtor = obtainFontFamilyCtor(fontFamily); + addFontFromAssetManager = obtainAddFontFromAssetManagerMethod(fontFamily); + addFontFromBuffer = obtainAddFontFromBufferMethod(fontFamily); + freeze = obtainFreezeMethod(fontFamily); + abortCreation = obtainAbortCreationMethod(fontFamily); + createFromFamiliesWithDefault = obtainCreateFromFamiliesWithDefaultMethod(fontFamily); } catch (ClassNotFoundException | NoSuchMethodException e) { Log.e(TAG, "Unable to collect necessary methods for class " + e.getClass().getName(), e); - fontFamilyClass = null; + fontFamily = null; fontFamilyCtor = null; - addFontMethod = null; - addFromBufferMethod = null; - freezeMethod = null; - abortCreationMethod = null; - createFromFamiliesWithDefaultMethod = null; + addFontFromAssetManager = null; + addFontFromBuffer = null; + freeze = null; + abortCreation = null; + createFromFamiliesWithDefault = null; } - sFontFamilyCtor = fontFamilyCtor; - sFontFamily = fontFamilyClass; - sAddFontFromAssetManager = addFontMethod; - sAddFontFromBuffer = addFromBufferMethod; - sFreeze = freezeMethod; - sAbortCreation = abortCreationMethod; - sCreateFromFamiliesWithDefault = createFromFamiliesWithDefaultMethod; + mFontFamily = fontFamily; + mFontFamilyCtor = fontFamilyCtor; + mAddFontFromAssetManager = addFontFromAssetManager; + mAddFontFromBuffer = addFontFromBuffer; + mFreeze = freeze; + mAbortCreation = abortCreation; + mCreateFromFamiliesWithDefault = createFromFamiliesWithDefault; } /** - * Returns true if API26 implementation is usable. + * Returns true if all the necessary methods were found. */ - private static boolean isFontFamilyPrivateAPIAvailable() { - if (sAddFontFromAssetManager == null) { + private boolean isFontFamilyPrivateAPIAvailable() { + if (mAddFontFromAssetManager == null) { Log.w(TAG, "Unable to collect necessary private methods. " + "Fallback to legacy implementation."); } - return sAddFontFromAssetManager != null; + return mAddFontFromAssetManager != null; } /** * Create a new FontFamily instance */ - private static Object newFamily() { + private Object newFamily() { try { - return sFontFamilyCtor.newInstance(); + return mFontFamilyCtor.newInstance(); } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { throw new RuntimeException(e); } @@ -139,10 +132,10 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { * Call FontFamily#addFontFromAssetManager(AssetManager mgr, String path, int cookie, * boolean isAsset, int ttcIndex, int weight, int isItalic, FontVariationAxis[] axes) */ - private static boolean addFontFromAssetManager(Context context, Object family, String fileName, + private boolean addFontFromAssetManager(Context context, Object family, String fileName, int ttcIndex, int weight, int style) { try { - final Boolean result = (Boolean) sAddFontFromAssetManager.invoke(family, + final Boolean result = (Boolean) mAddFontFromAssetManager.invoke(family, context.getAssets(), fileName, 0 /* cookie */, false /* isAsset */, ttcIndex, weight, style, null /* axes */); return result.booleanValue(); @@ -155,10 +148,10 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { * Call FontFamily#addFontFromBuffer(ByteBuffer font, int ttcIndex, FontVariationAxis[] axes, * int weight, int italic) */ - private static boolean addFontFromBuffer(Object family, ByteBuffer buffer, + private boolean addFontFromBuffer(Object family, ByteBuffer buffer, int ttcIndex, int weight, int style) { try { - final Boolean result = (Boolean) sAddFontFromBuffer.invoke(family, + final Boolean result = (Boolean) mAddFontFromBuffer.invoke(family, buffer, ttcIndex, null /* axes */, weight, style); return result.booleanValue(); } catch (IllegalAccessException | InvocationTargetException e) { @@ -167,14 +160,14 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { } /** - * Call static method Typeface#createFromFamiliesWithDefault( + * Call method Typeface#createFromFamiliesWithDefault( * FontFamily[] families, int weight, int italic) */ - private static Typeface createFromFamiliesWithDefault(Object family) { + protected Typeface createFromFamiliesWithDefault(Object family) { try { - Object familyArray = Array.newInstance(sFontFamily, 1); + Object familyArray = Array.newInstance(mFontFamily, 1); Array.set(familyArray, 0, family); - return (Typeface) sCreateFromFamiliesWithDefault.invoke(null /* static method */, + return (Typeface) mCreateFromFamiliesWithDefault.invoke(null /* static method */, familyArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); @@ -184,9 +177,9 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { /** * Call FontFamily#freeze() */ - private static boolean freeze(Object family) { + private boolean freeze(Object family) { try { - Boolean result = (Boolean) sFreeze.invoke(family); + Boolean result = (Boolean) mFreeze.invoke(family); return result.booleanValue(); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); @@ -196,9 +189,9 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { /** * Call FontFamily#abortCreation() */ - private static boolean abortCreation(Object family) { + private boolean abortCreation(Object family) { try { - Boolean result = (Boolean) sAbortCreation.invoke(family); + Boolean result = (Boolean) mAbortCreation.invoke(family); return result.booleanValue(); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); @@ -299,4 +292,47 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { } return createFromFamiliesWithDefault(fontFamily); } + + // The following getters retrieve by reflection the Typeface methods, belonging to the + // framework code, which will be invoked. Since the definitions of these methods can change + // across different API versions, inheriting classes should override these getters in order to + // reflect the method definitions in the API versions they represent. + //=========================================================================================== + protected Class obtainFontFamily() throws ClassNotFoundException { + return Class.forName(FONT_FAMILY_CLASS); + } + + protected Constructor obtainFontFamilyCtor(Class fontFamily) throws NoSuchMethodException { + return fontFamily.getConstructor(); + } + + protected Method obtainAddFontFromAssetManagerMethod(Class fontFamily) + throws NoSuchMethodException { + return fontFamily.getMethod(ADD_FONT_FROM_ASSET_MANAGER_METHOD, + AssetManager.class, String.class, Integer.TYPE, Boolean.TYPE, Integer.TYPE, + Integer.TYPE, Integer.TYPE, FontVariationAxis[].class); + } + + protected Method obtainAddFontFromBufferMethod(Class fontFamily) throws NoSuchMethodException { + return fontFamily.getMethod(ADD_FONT_FROM_BUFFER_METHOD, + ByteBuffer.class, Integer.TYPE, FontVariationAxis[].class, Integer.TYPE, + Integer.TYPE); + } + + protected Method obtainFreezeMethod(Class fontFamily) throws NoSuchMethodException { + return fontFamily.getMethod(FREEZE_METHOD); + } + + protected Method obtainAbortCreationMethod(Class fontFamily) throws NoSuchMethodException { + return fontFamily.getMethod(ABORT_CREATION_METHOD); + } + + protected Method obtainCreateFromFamiliesWithDefaultMethod(Class fontFamily) + throws NoSuchMethodException { + Object familyArray = Array.newInstance(fontFamily, 1); + Method m = Typeface.class.getDeclaredMethod(CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD, + familyArray.getClass(), Integer.TYPE, Integer.TYPE); + m.setAccessible(true); + return m; + } } diff --git a/android/support/v4/graphics/TypefaceCompatApi28Impl.java b/android/support/v4/graphics/TypefaceCompatApi28Impl.java new file mode 100644 index 00000000..baa2ce67 --- /dev/null +++ b/android/support/v4/graphics/TypefaceCompatApi28Impl.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 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 android.support.v4.graphics; + +import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; + +import android.graphics.Typeface; +import android.support.annotation.RequiresApi; +import android.support.annotation.RestrictTo; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Implementation of the Typeface compat methods for API 28 and above. + * @hide + */ +@RestrictTo(LIBRARY_GROUP) +@RequiresApi(28) +public class TypefaceCompatApi28Impl extends TypefaceCompatApi26Impl { + private static final String TAG = "TypefaceCompatApi28Impl"; + + private static final String CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD = + "createFromFamiliesWithDefault"; + private static final int RESOLVE_BY_FONT_TABLE = -1; + private static final String DEFAULT_FAMILY = "sans-serif"; + + /** + * Call method Typeface#createFromFamiliesWithDefault( + * FontFamily[] families, String fallbackName, int weight, int italic) + */ + @Override + protected Typeface createFromFamiliesWithDefault(Object family) { + try { + Object familyArray = Array.newInstance(mFontFamily, 1); + Array.set(familyArray, 0, family); + return (Typeface) mCreateFromFamiliesWithDefault.invoke(null /* static method */, + familyArray, DEFAULT_FAMILY, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + protected Method obtainCreateFromFamiliesWithDefaultMethod(Class fontFamily) + throws NoSuchMethodException { + Object familyArray = Array.newInstance(fontFamily, 1); + Method m = Typeface.class.getDeclaredMethod(CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD, + familyArray.getClass(), String.class, Integer.TYPE, Integer.TYPE); + m.setAccessible(true); + return m; + } +} diff --git a/android/support/v4/media/MediaBrowserCompat.java b/android/support/v4/media/MediaBrowserCompat.java index 85f5a512..7adf7d78 100644 --- a/android/support/v4/media/MediaBrowserCompat.java +++ b/android/support/v4/media/MediaBrowserCompat.java @@ -676,17 +676,15 @@ public final class MediaBrowserCompat { WeakReference<Subscription> mSubscriptionRef; public SubscriptionCallback() { + mToken = new Binder(); if (Build.VERSION.SDK_INT >= 26) { mSubscriptionCallbackObj = MediaBrowserCompatApi26.createSubscriptionCallback(new StubApi26()); - mToken = null; } else if (Build.VERSION.SDK_INT >= 21) { mSubscriptionCallbackObj = MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21()); - mToken = new Binder(); } else { mSubscriptionCallbackObj = null; - mToken = new Binder(); } } @@ -1958,22 +1956,30 @@ public final class MediaBrowserCompat { @Override public void subscribe(@NonNull String parentId, @Nullable Bundle options, @NonNull SubscriptionCallback callback) { - if (options == null) { - MediaBrowserCompatApi21.subscribe( - mBrowserObj, parentId, callback.mSubscriptionCallbackObj); + if (mServiceBinderWrapper == null) { + if (options == null) { + MediaBrowserCompatApi21.subscribe( + mBrowserObj, parentId, callback.mSubscriptionCallbackObj); + } else { + MediaBrowserCompatApi26.subscribe( + mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj); + } } else { - MediaBrowserCompatApi26.subscribe( - mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj); + super.subscribe(parentId, options, callback); } } @Override public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { - if (callback == null) { - MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); + if (mServiceBinderWrapper == null) { + if (callback == null) { + MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); + } else { + MediaBrowserCompatApi26.unsubscribe(mBrowserObj, parentId, + callback.mSubscriptionCallbackObj); + } } else { - MediaBrowserCompatApi26.unsubscribe(mBrowserObj, parentId, - callback.mSubscriptionCallbackObj); + super.unsubscribe(parentId, callback); } } } diff --git a/android/support/v4/media/MediaBrowserServiceCompat.java b/android/support/v4/media/MediaBrowserServiceCompat.java index 53b111ab..debc66e8 100644 --- a/android/support/v4/media/MediaBrowserServiceCompat.java +++ b/android/support/v4/media/MediaBrowserServiceCompat.java @@ -422,11 +422,15 @@ public abstract class MediaBrowserServiceCompat extends Service { @Override public void notifyChildrenChanged(final String parentId, final Bundle options) { - if (options == null) { - MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId); + if (mMessenger == null) { + if (options == null) { + MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId); + } else { + MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId, + options); + } } else { - MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId, - options); + super.notifyChildrenChanged(parentId, options); } } diff --git a/android/support/v4/media/MediaMetadataCompat.java b/android/support/v4/media/MediaMetadataCompat.java index 3ddf255c..00f16cb3 100644 --- a/android/support/v4/media/MediaMetadataCompat.java +++ b/android/support/v4/media/MediaMetadataCompat.java @@ -365,10 +365,12 @@ public final class MediaMetadataCompat implements Parcelable { MediaMetadataCompat(Bundle bundle) { mBundle = new Bundle(bundle); + mBundle.setClassLoader(MediaMetadataCompat.class.getClassLoader()); } MediaMetadataCompat(Parcel in) { mBundle = in.readBundle(); + mBundle.setClassLoader(MediaMetadataCompat.class.getClassLoader()); } /** diff --git a/android/support/v4/view/ViewCompat.java b/android/support/v4/view/ViewCompat.java index 34a198a1..204a1218 100644 --- a/android/support/v4/view/ViewCompat.java +++ b/android/support/v4/view/ViewCompat.java @@ -1356,7 +1356,7 @@ public class ViewCompat { // after applying the tint Drawable background = view.getBackground(); boolean hasTint = (view.getBackgroundTintList() != null) - && (view.getBackgroundTintMode() != null); + || (view.getBackgroundTintMode() != null); if ((background != null) && hasTint) { if (background.isStateful()) { background.setState(view.getDrawableState()); @@ -1375,7 +1375,7 @@ public class ViewCompat { // after applying the tint Drawable background = view.getBackground(); boolean hasTint = (view.getBackgroundTintList() != null) - && (view.getBackgroundTintMode() != null); + || (view.getBackgroundTintMode() != null); if ((background != null) && hasTint) { if (background.isStateful()) { background.setState(view.getDrawableState()); diff --git a/android/support/v7/app/AppCompatDelegateImplV9.java b/android/support/v7/app/AppCompatDelegateImplV9.java index 056e33e3..5b53401c 100644 --- a/android/support/v7/app/AppCompatDelegateImplV9.java +++ b/android/support/v7/app/AppCompatDelegateImplV9.java @@ -1001,7 +1001,26 @@ class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (mAppCompatViewInflater == null) { - mAppCompatViewInflater = new AppCompatViewInflater(); + TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); + String viewInflaterClassName = + a.getString(R.styleable.AppCompatTheme_viewInflaterClass); + if ((viewInflaterClassName == null) + || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) { + // Either default class name or set explicitly to null. In both cases + // create the base inflater (no reflection) + mAppCompatViewInflater = new AppCompatViewInflater(); + } else { + try { + Class viewInflaterClass = Class.forName(viewInflaterClassName); + mAppCompatViewInflater = + (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor() + .newInstance(); + } catch (Throwable t) { + Log.i(TAG, "Failed to instantiate custom view inflater " + + viewInflaterClassName + ". Falling back to default.", t); + mAppCompatViewInflater = new AppCompatViewInflater(); + } + } } boolean inheritContext = false; diff --git a/android/support/v7/app/AppCompatViewInflater.java b/android/support/v7/app/AppCompatViewInflater.java index 54d01bce..87a1a3c7 100644 --- a/android/support/v7/app/AppCompatViewInflater.java +++ b/android/support/v7/app/AppCompatViewInflater.java @@ -51,14 +51,12 @@ import java.lang.reflect.Method; import java.util.Map; /** - * This class is responsible for manually inflating our tinted widgets which are used on devices - * running {@link android.os.Build.VERSION_CODES#KITKAT KITKAT} or below. As such, this class - * should only be used when running on those devices. + * This class is responsible for manually inflating our tinted widgets. * <p>This class two main responsibilities: the first is to 'inject' our tinted views in place of * the framework versions in layout inflation; the second is backport the {@code android:theme} * functionality for any inflated widgets. This include theme inheritance from its parent. */ -class AppCompatViewInflater { +public class AppCompatViewInflater { private static final Class<?>[] sConstructorSignature = new Class[]{ Context.class, AttributeSet.class}; @@ -77,7 +75,7 @@ class AppCompatViewInflater { private final Object[] mConstructorArgs = new Object[2]; - public final View createView(View parent, final String name, @NonNull Context context, + final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { final Context originalContext = context; @@ -100,44 +98,63 @@ class AppCompatViewInflater { // We need to 'inject' our tint aware Views in place of the standard framework versions switch (name) { case "TextView": - view = new AppCompatTextView(context, attrs); + view = createTextView(context, attrs); + verifyNotNull(view, name); break; case "ImageView": - view = new AppCompatImageView(context, attrs); + view = createImageView(context, attrs); + verifyNotNull(view, name); break; case "Button": - view = new AppCompatButton(context, attrs); + view = createButton(context, attrs); + verifyNotNull(view, name); break; case "EditText": - view = new AppCompatEditText(context, attrs); + view = createEditText(context, attrs); + verifyNotNull(view, name); break; case "Spinner": - view = new AppCompatSpinner(context, attrs); + view = createSpinner(context, attrs); + verifyNotNull(view, name); break; case "ImageButton": - view = new AppCompatImageButton(context, attrs); + view = createImageButton(context, attrs); + verifyNotNull(view, name); break; case "CheckBox": - view = new AppCompatCheckBox(context, attrs); + view = createCheckBox(context, attrs); + verifyNotNull(view, name); break; case "RadioButton": - view = new AppCompatRadioButton(context, attrs); + view = createRadioButton(context, attrs); + verifyNotNull(view, name); break; case "CheckedTextView": - view = new AppCompatCheckedTextView(context, attrs); + view = createCheckedTextView(context, attrs); + verifyNotNull(view, name); break; case "AutoCompleteTextView": - view = new AppCompatAutoCompleteTextView(context, attrs); + view = createAutoCompleteTextView(context, attrs); + verifyNotNull(view, name); break; case "MultiAutoCompleteTextView": - view = new AppCompatMultiAutoCompleteTextView(context, attrs); + view = createMultiAutoCompleteTextView(context, attrs); + verifyNotNull(view, name); break; case "RatingBar": - view = new AppCompatRatingBar(context, attrs); + view = createRatingBar(context, attrs); + verifyNotNull(view, name); break; case "SeekBar": - view = new AppCompatSeekBar(context, attrs); + view = createSeekBar(context, attrs); + verifyNotNull(view, name); break; + default: + // The fallback that allows extending class to take over view inflation + // for other tags. Note that we don't check that the result is not-null. + // That allows the custom inflater path to fall back on the default one + // later in this method. + view = createView(context, name, attrs); } if (view == null && originalContext != context) { @@ -154,6 +171,85 @@ class AppCompatViewInflater { return view; } + @NonNull + protected AppCompatTextView createTextView(Context context, AttributeSet attrs) { + return new AppCompatTextView(context, attrs); + } + + @NonNull + protected AppCompatImageView createImageView(Context context, AttributeSet attrs) { + return new AppCompatImageView(context, attrs); + } + + @NonNull + protected AppCompatButton createButton(Context context, AttributeSet attrs) { + return new AppCompatButton(context, attrs); + } + + @NonNull + protected AppCompatEditText createEditText(Context context, AttributeSet attrs) { + return new AppCompatEditText(context, attrs); + } + + @NonNull + protected AppCompatSpinner createSpinner(Context context, AttributeSet attrs) { + return new AppCompatSpinner(context, attrs); + } + + @NonNull + protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) { + return new AppCompatImageButton(context, attrs); + } + + @NonNull + protected AppCompatCheckBox createCheckBox(Context context, AttributeSet attrs) { + return new AppCompatCheckBox(context, attrs); + } + + @NonNull + protected AppCompatRadioButton createRadioButton(Context context, AttributeSet attrs) { + return new AppCompatRadioButton(context, attrs); + } + + @NonNull + protected AppCompatCheckedTextView createCheckedTextView(Context context, AttributeSet attrs) { + return new AppCompatCheckedTextView(context, attrs); + } + + @NonNull + protected AppCompatAutoCompleteTextView createAutoCompleteTextView(Context context, + AttributeSet attrs) { + return new AppCompatAutoCompleteTextView(context, attrs); + } + + @NonNull + protected AppCompatMultiAutoCompleteTextView createMultiAutoCompleteTextView(Context context, + AttributeSet attrs) { + return new AppCompatMultiAutoCompleteTextView(context, attrs); + } + + @NonNull + protected AppCompatRatingBar createRatingBar(Context context, AttributeSet attrs) { + return new AppCompatRatingBar(context, attrs); + } + + @NonNull + protected AppCompatSeekBar createSeekBar(Context context, AttributeSet attrs) { + return new AppCompatSeekBar(context, attrs); + } + + private void verifyNotNull(View view, String name) { + if (view == null) { + throw new IllegalStateException(this.getClass().getName() + + " asked to inflate view for <" + name + ">, but returned null"); + } + } + + @Nullable + protected View createView(Context context, String name, AttributeSet attrs) { + return null; + } + private View createViewFromTag(Context context, String name, AttributeSet attrs) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); @@ -165,14 +261,14 @@ class AppCompatViewInflater { if (-1 == name.indexOf('.')) { for (int i = 0; i < sClassPrefixList.length; i++) { - final View view = createView(context, name, sClassPrefixList[i]); + final View view = createViewByPrefix(context, name, sClassPrefixList[i]); if (view != null) { return view; } } return null; } else { - return createView(context, name, null); + return createViewByPrefix(context, name, null); } } catch (Exception e) { // We do not want to catch these, lets return null and let the actual LayoutInflater @@ -209,7 +305,7 @@ class AppCompatViewInflater { a.recycle(); } - private View createView(Context context, String name, String prefix) + private View createViewByPrefix(Context context, String name, String prefix) throws ClassNotFoundException, InflateException { Constructor<? extends View> constructor = sConstructorMap.get(name); diff --git a/android/support/v7/util/SortedList.java b/android/support/v7/util/SortedList.java index c62d0ce8..af000a1e 100644 --- a/android/support/v7/util/SortedList.java +++ b/android/support/v7/util/SortedList.java @@ -16,6 +16,8 @@ package android.support.v7.util; +import android.support.annotation.Nullable; + import java.lang.reflect.Array; import java.util.Arrays; import java.util.Collection; @@ -315,7 +317,8 @@ public class SortedList<T> { newDataStart++; mOldDataStart++; if (!mCallback.areContentsTheSame(oldItem, newItem)) { - mCallback.onChanged(mMergedSize - 1, 1); + mCallback.onChanged(mMergedSize - 1, 1, + mCallback.getChangePayload(oldItem, newItem)); } } else { // Old item is lower than or equal to (but not the same as the new). Output it. @@ -401,7 +404,7 @@ public class SortedList<T> { return index; } else { mData[index] = item; - mCallback.onChanged(index, 1); + mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); return index; } } @@ -488,13 +491,13 @@ public class SortedList<T> { if (cmp == 0) { mData[index] = item; if (contentsChanged) { - mCallback.onChanged(index, 1); + mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); } return; } } if (contentsChanged) { - mCallback.onChanged(index, 1); + mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); } // TODO this done in 1 pass to avoid shifting twice. removeItemAtIndex(index, false); @@ -741,6 +744,28 @@ public class SortedList<T> { * @return True if the two items represent the same object or false if they are different. */ abstract public boolean areItemsTheSame(T2 item1, T2 item2); + + /** + * When {@link #areItemsTheSame(T2, T2)} returns {@code true} for two items and + * {@link #areContentsTheSame(T2, T2)} returns false for them, {@link Callback} calls this + * method to get a payload about the change. + * <p> + * For example, if you are using {@link Callback} with + * {@link android.support.v7.widget.RecyclerView}, you can return the particular field that + * changed in the item and your + * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that + * information to run the correct animation. + * <p> + * Default implementation returns {@code null}. + * + * @param item1 The first item to check. + * @param item2 The second item to check. + * @return A payload object that represents the changes between the two items. + */ + @Nullable + public Object getChangePayload(T2 item1, T2 item2) { + return null; + } } /** @@ -801,6 +826,11 @@ public class SortedList<T> { } @Override + public void onChanged(int position, int count, Object payload) { + mBatchingListUpdateCallback.onChanged(position, count, payload); + } + + @Override public boolean areContentsTheSame(T2 oldItem, T2 newItem) { return mWrappedCallback.areContentsTheSame(oldItem, newItem); } @@ -810,6 +840,12 @@ public class SortedList<T> { return mWrappedCallback.areItemsTheSame(item1, item2); } + @Nullable + @Override + public Object getChangePayload(T2 item1, T2 item2) { + return mWrappedCallback.getChangePayload(item1, item2); + } + /** * This method dispatches any pending event notifications to the wrapped Callback. * You <b>must</b> always call this method after you are done with editing the SortedList. diff --git a/android/support/v7/util/SortedListBatchedCallbackTest.java b/android/support/v7/util/SortedListBatchedCallbackTest.java index 3ace2178..bc50415d 100644 --- a/android/support/v7/util/SortedListBatchedCallbackTest.java +++ b/android/support/v7/util/SortedListBatchedCallbackTest.java @@ -50,6 +50,16 @@ public class SortedListBatchedCallbackTest { } @Test + public void onChangeWithPayload() { + final Object payload = 7; + mBatchedCallback.onChanged(1, 2, payload); + verifyZeroInteractions(mMockCallback); + mBatchedCallback.dispatchLastEvent(); + verify(mMockCallback).onChanged(1, 2, payload); + verifyNoMoreInteractions(mMockCallback); + } + + @Test public void onRemoved() { mBatchedCallback.onRemoved(2, 3); verifyZeroInteractions(mMockCallback); diff --git a/android/support/v7/util/SortedListTest.java b/android/support/v7/util/SortedListTest.java index da3c9572..47d2ac0f 100644 --- a/android/support/v7/util/SortedListTest.java +++ b/android/support/v7/util/SortedListTest.java @@ -16,6 +16,7 @@ package android.support.v7.util; +import android.support.annotation.Nullable; import android.support.test.filters.SmallTest; import junit.framework.TestCase; @@ -41,6 +42,8 @@ public class SortedListTest extends TestCase { List<Pair> mRemovals = new ArrayList<Pair>(); List<Pair> mMoves = new ArrayList<Pair>(); List<Pair> mUpdates = new ArrayList<Pair>(); + private boolean mPayloadChanges = false; + List<PayloadChange> mPayloadUpdates = new ArrayList<>(); private SortedList.Callback<Item> mCallback; InsertedCallback<Item> mInsertedCallback; ChangedCallback<Item> mChangedCallback; @@ -97,6 +100,15 @@ public class SortedListTest extends TestCase { } @Override + public void onChanged(int position, int count, Object payload) { + if (mPayloadChanges) { + mPayloadUpdates.add(new PayloadChange(position, count, payload)); + } else { + onChanged(position, count); + } + } + + @Override public boolean areContentsTheSame(Item oldItem, Item newItem) { return oldItem.cmpField == newItem.cmpField && oldItem.data == newItem.data; } @@ -105,6 +117,15 @@ public class SortedListTest extends TestCase { public boolean areItemsTheSame(Item item1, Item item2) { return item1.id == item2.id; } + + @Nullable + @Override + public Object getChangePayload(Item item1, Item item2) { + if (mPayloadChanges) { + return item2.data; + } + return null; + } }; mInsertedCallback = null; mChangedCallback = null; @@ -705,6 +726,76 @@ public class SortedListTest extends TestCase { assertTrue(mAdditions.contains(new Pair(0, 6))); } + @Test + public void testAddExistingItemCallsChangeWithPayload() { + mList.addAll( + new Item(1, 10), + new Item(2, 20), + new Item(3, 30) + ); + mPayloadChanges = true; + + // add an item with the same id but a new data field i.e. send an update + final Item twoUpdate = new Item(2, 20); + twoUpdate.data = 1337; + mList.add(twoUpdate); + assertEquals(1, mPayloadUpdates.size()); + final PayloadChange update = mPayloadUpdates.get(0); + assertEquals(1, update.position); + assertEquals(1, update.count); + assertEquals(1337, update.payload); + assertEquals(3, size()); + } + + @Test + public void testUpdateItemCallsChangeWithPayload() { + mList.addAll( + new Item(1, 10), + new Item(2, 20), + new Item(3, 30) + ); + mPayloadChanges = true; + + // add an item with the same id but a new data field i.e. send an update + final Item twoUpdate = new Item(2, 20); + twoUpdate.data = 1337; + mList.updateItemAt(1, twoUpdate); + assertEquals(1, mPayloadUpdates.size()); + final PayloadChange update = mPayloadUpdates.get(0); + assertEquals(1, update.position); + assertEquals(1, update.count); + assertEquals(1337, update.payload); + assertEquals(3, size()); + assertEquals(1337, mList.get(1).data); + } + + @Test + public void testAddMultipleExistingItemCallsChangeWithPayload() { + mList.addAll( + new Item(1, 10), + new Item(2, 20), + new Item(3, 30) + ); + mPayloadChanges = true; + + // add two items with the same ids but a new data fields i.e. send two updates + final Item twoUpdate = new Item(2, 20); + twoUpdate.data = 222; + final Item threeUpdate = new Item(3, 30); + threeUpdate.data = 333; + mList.addAll(twoUpdate, threeUpdate); + assertEquals(2, mPayloadUpdates.size()); + final PayloadChange update1 = mPayloadUpdates.get(0); + assertEquals(1, update1.position); + assertEquals(1, update1.count); + assertEquals(222, update1.payload); + final PayloadChange update2 = mPayloadUpdates.get(1); + assertEquals(2, update2.position); + assertEquals(1, update2.count); + assertEquals(333, update2.payload); + assertEquals(3, size()); + } + private int size() { return mList.size(); } @@ -821,4 +912,37 @@ public class SortedListTest extends TestCase { return result; } } + + private static final class PayloadChange { + public final int position; + public final int count; + public final Object payload; + + PayloadChange(int position, int count, Object payload) { + this.position = position; + this.count = count; + this.payload = payload; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PayloadChange payloadChange = (PayloadChange) o; + + if (position != payloadChange.position) return false; + if (count != payloadChange.count) return false; + return payload != null ? payload.equals(payloadChange.payload) + : payloadChange.payload == null; + } + + @Override + public int hashCode() { + int result = position; + result = 31 * result + count; + result = 31 * result + (payload != null ? payload.hashCode() : 0); + return result; + } + } }
\ No newline at end of file diff --git a/android/support/v7/widget/AppCompatTextHelper.java b/android/support/v7/widget/AppCompatTextHelper.java index fa6196f5..b8ce82a4 100644 --- a/android/support/v7/widget/AppCompatTextHelper.java +++ b/android/support/v7/widget/AppCompatTextHelper.java @@ -213,9 +213,9 @@ class AppCompatTextHelper { if (a.hasValue(R.styleable.TextAppearance_android_fontFamily) || a.hasValue(R.styleable.TextAppearance_fontFamily)) { mFontTypeface = null; - int fontFamilyId = a.hasValue(R.styleable.TextAppearance_android_fontFamily) - ? R.styleable.TextAppearance_android_fontFamily - : R.styleable.TextAppearance_fontFamily; + int fontFamilyId = a.hasValue(R.styleable.TextAppearance_fontFamily) + ? R.styleable.TextAppearance_fontFamily + : R.styleable.TextAppearance_android_fontFamily; if (!context.isRestricted()) { final WeakReference<TextView> textViewWeak = new WeakReference<>(mView); ResourcesCompat.FontCallback replyCallback = new ResourcesCompat.FontCallback() { diff --git a/android/support/v7/widget/TooltipCompatHandler.java b/android/support/v7/widget/TooltipCompatHandler.java index 5ce1f8b3..63a61982 100644 --- a/android/support/v7/widget/TooltipCompatHandler.java +++ b/android/support/v7/widget/TooltipCompatHandler.java @@ -66,6 +66,10 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList private TooltipPopup mPopup; private boolean mFromTouch; + // The handler currently scheduled to show a tooltip, triggered by a hover + // (there can be only one). + private static TooltipCompatHandler sPendingHandler; + // The handler currently showing a tooltip (there can be only one). private static TooltipCompatHandler sActiveHandler; @@ -76,6 +80,15 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList * @param tooltipText the tooltip text */ public static void setTooltipText(View view, CharSequence tooltipText) { + // The code below is not attempting to update the tooltip text + // for a pending or currently active tooltip, because it may lead + // to updating the wrong tooltip in in some rare cases (e.g. when + // action menu item views are recycled). Instead, the tooltip is + // canceled/hidden. This might still be the wrong tooltip, + // but hiding a wrong tooltip is less disruptive UX. + if (sPendingHandler != null && sPendingHandler.mAnchor == view) { + setPendingHandler(null); + } if (TextUtils.isEmpty(tooltipText)) { if (sActiveHandler != null && sActiveHandler.mAnchor == view) { sActiveHandler.hide(); @@ -119,8 +132,7 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList if (mAnchor.isEnabled() && mPopup == null) { mAnchorX = (int) event.getX(); mAnchorY = (int) event.getY(); - mAnchor.removeCallbacks(mShowRunnable); - mAnchor.postDelayed(mShowRunnable, ViewConfiguration.getLongPressTimeout()); + setPendingHandler(this); } break; case MotionEvent.ACTION_HOVER_EXIT: @@ -145,6 +157,7 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList if (!ViewCompat.isAttachedToWindow(mAnchor)) { return; } + setPendingHandler(null); if (sActiveHandler != null) { sActiveHandler.hide(); } @@ -180,7 +193,27 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList Log.e(TAG, "sActiveHandler.mPopup == null"); } } - mAnchor.removeCallbacks(mShowRunnable); + if (sPendingHandler == this) { + setPendingHandler(null); + } mAnchor.removeCallbacks(mHideRunnable); } + + private static void setPendingHandler(TooltipCompatHandler handler) { + if (sPendingHandler != null) { + sPendingHandler.cancelPendingShow(); + } + sPendingHandler = handler; + if (sPendingHandler != null) { + sPendingHandler.scheduleShow(); + } + } + + private void scheduleShow() { + mAnchor.postDelayed(mShowRunnable, ViewConfiguration.getLongPressTimeout()); + } + + private void cancelPendingShow() { + mAnchor.removeCallbacks(mShowRunnable); + } } diff --git a/android/support/v7/widget/util/SortedListAdapterCallback.java b/android/support/v7/widget/util/SortedListAdapterCallback.java index 4921541b..a1203a69 100644 --- a/android/support/v7/widget/util/SortedListAdapterCallback.java +++ b/android/support/v7/widget/util/SortedListAdapterCallback.java @@ -56,4 +56,9 @@ public abstract class SortedListAdapterCallback<T2> extends SortedList.Callback< public void onChanged(int position, int count) { mAdapter.notifyItemRangeChanged(position, count); } + + @Override + public void onChanged(int position, int count, Object payload) { + mAdapter.notifyItemRangeChanged(position, count, payload); + } } diff --git a/android/support/wear/ambient/AmbientDelegate.java b/android/support/wear/ambient/AmbientDelegate.java index 49012908..8e96a020 100644 --- a/android/support/wear/ambient/AmbientDelegate.java +++ b/android/support/wear/ambient/AmbientDelegate.java @@ -19,14 +19,12 @@ import android.app.Activity; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; import com.google.android.wearable.compat.WearableActivityController; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; -import java.lang.reflect.Method; /** * Provides compatibility for ambient mode. @@ -146,19 +144,6 @@ final class AmbientDelegate { } /** - * Sets whether this activity's task should be moved to the front when the system exits ambient - * mode. If true, the activity's task may be moved to the front if it was the last activity to - * be running when ambient started, depending on how much time the system spent in ambient mode. - */ - void setAutoResumeEnabled(boolean enabled) { - if (mWearableController != null) { - if (hasSetAutoResumeEnabledMethod()) { - mWearableController.setAutoResumeEnabled(enabled); - } - } - } - - /** * @return {@code true} if the activity is currently in ambient. */ boolean isAmbient() { @@ -177,31 +162,4 @@ final class AmbientDelegate { mWearableController.dump(prefix, fd, writer, args); } } - - private boolean hasSetAutoResumeEnabledMethod() { - if (!sInitAutoResumeEnabledMethod) { - sInitAutoResumeEnabledMethod = true; - try { - Method method = - WearableActivityController.class - .getDeclaredMethod("setAutoResumeEnabled", boolean.class); - // Proguard is sneaky -- it will actually rewrite strings it finds in addition to - // function names. Therefore add a "." prefix to the method name check to ensure the - // function was not renamed by proguard. - if (!(".setAutoResumeEnabled".equals("." + method.getName()))) { - throw new NoSuchMethodException(); - } - sHasAutoResumeEnabledMethod = true; - } catch (NoSuchMethodException e) { - Log.w( - "WearableActivity", - "Could not find a required method for auto-resume " - + "support, likely due to proguard optimization. Please add " - + "com.google.android.wearable:wearable jar to the list of library " - + "jars for your project"); - sHasAutoResumeEnabledMethod = false; - } - } - return sHasAutoResumeEnabledMethod; - } } diff --git a/android/support/wear/ambient/AmbientMode.java b/android/support/wear/ambient/AmbientMode.java index db53dfc1..5db93830 100644 --- a/android/support/wear/ambient/AmbientMode.java +++ b/android/support/wear/ambient/AmbientMode.java @@ -38,13 +38,13 @@ import java.io.PrintWriter; * It should be called with an {@link Activity} as an argument and that {@link Activity} will then * be able to receive ambient lifecycle events through an {@link AmbientCallback}. The * {@link Activity} will also receive a {@link AmbientController} object from the attachment which - * can be used to query the current status of the ambient mode, or toggle simple settings. + * can be used to query the current status of the ambient mode. * An example of how to attach {@link AmbientMode} to your {@link Activity} and use * the {@link AmbientController} can be found below: * <p> * <pre class="prettyprint">{@code * AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this); - * controller.setAutoResumeEnabled(true); + * boolean isAmbient = controller.isAmbient(); * }</pre> */ public final class AmbientMode extends Fragment { @@ -117,7 +117,7 @@ public final class AmbientMode extends Fragment { * Called when the system is updating the display for ambient mode. Activities may use this * opportunity to update or invalidate views. */ - public void onUpdateAmbient() {}; + public void onUpdateAmbient() {} /** * Called when an activity should exit ambient mode. This event is sent while an activity is @@ -126,7 +126,7 @@ public final class AmbientMode extends Fragment { * <p><em>Derived classes must call through to the super class's implementation of this * method. If they do not, an exception will be thrown.</em> */ - public void onExitAmbient() {}; + public void onExitAmbient() {} } private final AmbientDelegate.AmbientCallback mCallback = @@ -220,7 +220,7 @@ public final class AmbientMode extends Fragment { * @param activity the activity to attach ambient support to. This activity has to also * implement {@link AmbientCallbackProvider} * @return the associated {@link AmbientController} which can be used to query the state of - * ambient mode and toggle simple settings related to it. + * ambient mode. */ public static <T extends Activity & AmbientCallbackProvider> AmbientController attachAmbientSupport(T activity) { @@ -251,9 +251,8 @@ public final class AmbientMode extends Fragment { /** * A class for interacting with the ambient mode on a wearable device. This class can be used to - * query the current state of ambient mode and to enable or disable certain settings. - * An instance of this class is returned to the user when they attach their {@link Activity} - * to {@link AmbientMode}. + * query the current state of ambient mode. An instance of this class is returned to the user + * when they attach their {@link Activity} to {@link AmbientMode}. */ public final class AmbientController { private static final String TAG = "AmbientController"; diff --git a/android/support/wear/ambient/SharedLibraryVersion.java b/android/support/wear/ambient/SharedLibraryVersion.java index cd90a3b7..9421d9ee 100644 --- a/android/support/wear/ambient/SharedLibraryVersion.java +++ b/android/support/wear/ambient/SharedLibraryVersion.java @@ -16,7 +16,6 @@ package android.support.wear.ambient; import android.os.Build; -import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; import com.google.android.wearable.WearableSharedLib; @@ -24,10 +23,7 @@ import com.google.android.wearable.WearableSharedLib; /** * Internal class which can be used to determine the version of the wearable shared library that is * available on the current device. - * - * @hide */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) final class SharedLibraryVersion { private SharedLibraryVersion() { diff --git a/android/support/wear/ambient/WearableControllerProvider.java b/android/support/wear/ambient/WearableControllerProvider.java index 1682dc0e..4b6ae8ee 100644 --- a/android/support/wear/ambient/WearableControllerProvider.java +++ b/android/support/wear/ambient/WearableControllerProvider.java @@ -28,7 +28,7 @@ import java.lang.reflect.Method; * * @hide */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@RestrictTo(RestrictTo.Scope.LIBRARY) public class WearableControllerProvider { private static final String TAG = "WearableControllerProvider"; diff --git a/android/support/wear/internal/widget/ResourcesUtil.java b/android/support/wear/internal/widget/ResourcesUtil.java index f23a6889..8ba3adf0 100644 --- a/android/support/wear/internal/widget/ResourcesUtil.java +++ b/android/support/wear/internal/widget/ResourcesUtil.java @@ -26,7 +26,7 @@ import android.support.annotation.RestrictTo.Scope; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public final class ResourcesUtil { /** diff --git a/android/support/wear/internal/widget/drawer/MultiPagePresenter.java b/android/support/wear/internal/widget/drawer/MultiPagePresenter.java index ad56048b..4a7ce667 100644 --- a/android/support/wear/internal/widget/drawer/MultiPagePresenter.java +++ b/android/support/wear/internal/widget/drawer/MultiPagePresenter.java @@ -28,7 +28,7 @@ import android.support.wear.widget.drawer.WearableNavigationDrawerView.WearableN * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public class MultiPagePresenter extends WearableNavigationDrawerPresenter { private final Ui mUi; diff --git a/android/support/wear/internal/widget/drawer/MultiPageUi.java b/android/support/wear/internal/widget/drawer/MultiPageUi.java index 90568451..0ba2f5d1 100644 --- a/android/support/wear/internal/widget/drawer/MultiPageUi.java +++ b/android/support/wear/internal/widget/drawer/MultiPageUi.java @@ -16,6 +16,7 @@ package android.support.wear.internal.widget.drawer; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.annotation.RestrictTo.Scope; @@ -37,7 +38,7 @@ import android.widget.TextView; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public class MultiPageUi implements MultiPagePresenter.Ui { private static final String TAG = "MultiPageUi"; @@ -62,13 +63,8 @@ public class MultiPageUi implements MultiPagePresenter.Ui { final View content = inflater.inflate(R.layout.ws_navigation_drawer_view, drawer, false /* attachToRoot */); - mNavigationPager = - (ViewPager) content - .findViewById(R.id.ws_navigation_drawer_view_pager); - mPageIndicatorView = - (PageIndicatorView) - content.findViewById( - R.id.ws_navigation_drawer_page_indicator); + mNavigationPager = content.findViewById(R.id.ws_navigation_drawer_view_pager); + mPageIndicatorView = content.findViewById(R.id.ws_navigation_drawer_page_indicator); drawer.setDrawerContent(content); } @@ -132,8 +128,9 @@ public class MultiPageUi implements MultiPagePresenter.Ui { mAdapter = adapter; } + @NonNull @Override - public Object instantiateItem(ViewGroup container, int position) { + public Object instantiateItem(@NonNull ViewGroup container, int position) { // Do not attach to root in the inflate method. The view needs to returned at the end // of this method. Attaching to root will cause view to point to container instead. final View view = @@ -141,17 +138,17 @@ public class MultiPageUi implements MultiPagePresenter.Ui { .inflate(R.layout.ws_navigation_drawer_item_view, container, false); container.addView(view); final ImageView iconView = - (ImageView) view - .findViewById(R.id.ws_navigation_drawer_item_icon); + view.findViewById(R.id.ws_navigation_drawer_item_icon); final TextView textView = - (TextView) view.findViewById(R.id.ws_navigation_drawer_item_text); + view.findViewById(R.id.ws_navigation_drawer_item_text); iconView.setImageDrawable(mAdapter.getItemDrawable(position)); textView.setText(mAdapter.getItemText(position)); return view; } @Override - public void destroyItem(ViewGroup container, int position, Object object) { + public void destroyItem(@NonNull ViewGroup container, int position, + @NonNull Object object) { container.removeView((View) object); } @@ -161,12 +158,12 @@ public class MultiPageUi implements MultiPagePresenter.Ui { } @Override - public int getItemPosition(Object object) { + public int getItemPosition(@NonNull Object object) { return POSITION_NONE; } @Override - public boolean isViewFromObject(View view, Object object) { + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view == object; } } diff --git a/android/support/wear/internal/widget/drawer/SinglePagePresenter.java b/android/support/wear/internal/widget/drawer/SinglePagePresenter.java index d90b5891..42cc7d06 100644 --- a/android/support/wear/internal/widget/drawer/SinglePagePresenter.java +++ b/android/support/wear/internal/widget/drawer/SinglePagePresenter.java @@ -29,7 +29,7 @@ import android.support.wear.widget.drawer.WearableNavigationDrawerView.WearableN * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public class SinglePagePresenter extends WearableNavigationDrawerPresenter { private static final long DRAWER_CLOSE_DELAY_MS = 500; diff --git a/android/support/wear/internal/widget/drawer/SinglePageUi.java b/android/support/wear/internal/widget/drawer/SinglePageUi.java index f3a42904..ffc966f0 100644 --- a/android/support/wear/internal/widget/drawer/SinglePageUi.java +++ b/android/support/wear/internal/widget/drawer/SinglePageUi.java @@ -38,7 +38,7 @@ import android.widget.Toast; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public class SinglePageUi implements SinglePagePresenter.Ui { @IdRes @@ -111,11 +111,10 @@ public class SinglePageUi implements SinglePagePresenter.Ui { R.layout.ws_single_page_nav_drawer_peek_view, mDrawer, false /* attachToRoot */); - mTextView = (TextView) content.findViewById(R.id.ws_nav_drawer_text); + mTextView = content.findViewById(R.id.ws_nav_drawer_text); mSinglePageImageViews = new CircledImageView[count]; for (int i = 0; i < count; i++) { - mSinglePageImageViews[i] = (CircledImageView) content - .findViewById(SINGLE_PAGE_BUTTON_IDS[i]); + mSinglePageImageViews[i] = content.findViewById(SINGLE_PAGE_BUTTON_IDS[i]); mSinglePageImageViews[i].setOnClickListener(new OnSelectedClickHandler(i, mPresenter)); mSinglePageImageViews[i].setCircleHidden(true); } diff --git a/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java b/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java index 1c8c4fb7..df108aac 100644 --- a/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java +++ b/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java @@ -30,7 +30,7 @@ import java.util.Set; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public abstract class WearableNavigationDrawerPresenter { private final Set<OnItemSelectedListener> mOnItemSelectedListeners = new HashSet<>(); diff --git a/android/support/wear/utils/MetadataConstants.java b/android/support/wear/utils/MetadataConstants.java index 5be9c523..c7335c2d 100644 --- a/android/support/wear/utils/MetadataConstants.java +++ b/android/support/wear/utils/MetadataConstants.java @@ -15,16 +15,13 @@ */ package android.support.wear.utils; -import android.annotation.TargetApi; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.os.Build; /** * Constants for android wear apps which are related to manifest meta-data. */ -@TargetApi(Build.VERSION_CODES.N) public class MetadataConstants { // Constants for standalone apps. // diff --git a/android/support/wear/widget/BezierSCurveInterpolator.java b/android/support/wear/widget/BezierSCurveInterpolator.java index 131bae84..9c56a83d 100644 --- a/android/support/wear/widget/BezierSCurveInterpolator.java +++ b/android/support/wear/widget/BezierSCurveInterpolator.java @@ -17,8 +17,6 @@ package android.support.wear.widget; import android.animation.TimeInterpolator; -import android.annotation.TargetApi; -import android.os.Build; import android.support.annotation.RestrictTo; import android.support.annotation.RestrictTo.Scope; @@ -27,8 +25,7 @@ import android.support.annotation.RestrictTo.Scope; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) -@TargetApi(Build.VERSION_CODES.KITKAT_WATCH) +@RestrictTo(Scope.LIBRARY) class BezierSCurveInterpolator implements TimeInterpolator { /** diff --git a/android/support/wear/widget/BoxInsetLayout.java b/android/support/wear/widget/BoxInsetLayout.java index ba35f2c9..a8b13814 100644 --- a/android/support/wear/widget/BoxInsetLayout.java +++ b/android/support/wear/widget/BoxInsetLayout.java @@ -20,7 +20,6 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.os.Build; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -111,21 +110,6 @@ public class BoxInsetLayout extends ViewGroup { } @Override - public WindowInsets onApplyWindowInsets(WindowInsets insets) { - insets = super.onApplyWindowInsets(insets); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - final boolean round = insets.isRound(); - if (round != mIsRound) { - mIsRound = round; - requestLayout(); - } - mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), - insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); - } - return insets; - } - - @Override public void setForeground(Drawable drawable) { super.setForeground(drawable); mForegroundDrawable = drawable; @@ -145,14 +129,10 @@ public class BoxInsetLayout extends ViewGroup { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - requestApplyInsets(); - } else { - mIsRound = getResources().getConfiguration().isScreenRound(); - WindowInsets insets = getRootWindowInsets(); - mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), - insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); - } + mIsRound = getResources().getConfiguration().isScreenRound(); + WindowInsets insets = getRootWindowInsets(); + mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), + insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); } @Override @@ -413,7 +393,7 @@ public class BoxInsetLayout extends ViewGroup { public static class LayoutParams extends FrameLayout.LayoutParams { /** @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @RestrictTo(RestrictTo.Scope.LIBRARY) @IntDef({BOX_NONE, BOX_LEFT, BOX_TOP, BOX_RIGHT, BOX_BOTTOM, BOX_ALL}) @Retention(RetentionPolicy.SOURCE) public @interface BoxedEdges {} diff --git a/android/support/wear/widget/CircledImageView.java b/android/support/wear/widget/CircledImageView.java index 03ed8c98..c441dd57 100644 --- a/android/support/wear/widget/CircledImageView.java +++ b/android/support/wear/widget/CircledImageView.java @@ -19,7 +19,6 @@ package android.support.wear.widget; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; @@ -32,7 +31,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; -import android.os.Build; import android.support.annotation.Px; import android.support.annotation.RestrictTo; import android.support.annotation.RestrictTo.Scope; @@ -47,8 +45,7 @@ import java.util.Objects; * * @hide */ -@TargetApi(Build.VERSION_CODES.M) -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public class CircledImageView extends View { private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator(); @@ -133,13 +130,9 @@ public class CircledImageView extends View { if (mDrawable != null && mDrawable.getConstantState() != null) { // The provided Drawable may be used elsewhere, so make a mutable clone before setTint() // or setAlpha() is called on it. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mDrawable = - mDrawable.getConstantState() - .newDrawable(context.getResources(), context.getTheme()); - } else { - mDrawable = mDrawable.getConstantState().newDrawable(context.getResources()); - } + mDrawable = + mDrawable.getConstantState() + .newDrawable(context.getResources(), context.getTheme()); mDrawable = mDrawable.mutate(); } diff --git a/android/support/wear/widget/CurvingLayoutCallback.java b/android/support/wear/widget/CurvingLayoutCallback.java index 275f1f8b..5e88a8c4 100644 --- a/android/support/wear/widget/CurvingLayoutCallback.java +++ b/android/support/wear/widget/CurvingLayoutCallback.java @@ -113,7 +113,7 @@ public class CurvingLayoutCallback extends WearableLinearLayoutManager.LayoutCal */ public void adjustAnchorOffsetXY(View child, float[] anchorOffsetXY) { return; - }; + } @VisibleForTesting void setRound(boolean isScreenRound) { diff --git a/android/support/wear/widget/ProgressDrawable.java b/android/support/wear/widget/ProgressDrawable.java index 08e8ec2e..28e05708 100644 --- a/android/support/wear/widget/ProgressDrawable.java +++ b/android/support/wear/widget/ProgressDrawable.java @@ -19,14 +19,12 @@ package android.support.wear.widget; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; -import android.annotation.TargetApi; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.RectF; import android.graphics.drawable.Drawable; -import android.os.Build; import android.support.annotation.RestrictTo; import android.support.annotation.RestrictTo.Scope; import android.util.Property; @@ -37,8 +35,7 @@ import android.view.animation.LinearInterpolator; * * @hide */ -@TargetApi(Build.VERSION_CODES.KITKAT_WATCH) -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) class ProgressDrawable extends Drawable { private static final Property<ProgressDrawable, Integer> LEVEL = diff --git a/android/support/wear/widget/RoundedDrawable.java b/android/support/wear/widget/RoundedDrawable.java index fd09a878..300b6dd6 100644 --- a/android/support/wear/widget/RoundedDrawable.java +++ b/android/support/wear/widget/RoundedDrawable.java @@ -15,7 +15,6 @@ */ package android.support.wear.widget; -import android.annotation.TargetApi; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -29,7 +28,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; -import android.os.Build; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -76,7 +74,6 @@ import java.util.Objects; * app:radius="dimension" * app:clipEnabled="boolean" /></pre> */ -@TargetApi(Build.VERSION_CODES.N) public class RoundedDrawable extends Drawable { @VisibleForTesting diff --git a/android/support/wear/widget/ScrollManager.java b/android/support/wear/widget/ScrollManager.java index 8155f62d..e01a2710 100644 --- a/android/support/wear/widget/ScrollManager.java +++ b/android/support/wear/widget/ScrollManager.java @@ -16,11 +16,8 @@ package android.support.wear.widget; -import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; - -import android.annotation.TargetApi; -import android.os.Build; import android.support.annotation.RestrictTo; +import android.support.annotation.RestrictTo.Scope; import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -30,8 +27,7 @@ import android.view.VelocityTracker; * * @hide */ -@TargetApi(Build.VERSION_CODES.M) -@RestrictTo(LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) class ScrollManager { // One second in milliseconds. private static final int ONE_SEC_IN_MS = 1000; diff --git a/android/support/wear/widget/SimpleAnimatorListener.java b/android/support/wear/widget/SimpleAnimatorListener.java index a60b0bd2..3a1e56b9 100644 --- a/android/support/wear/widget/SimpleAnimatorListener.java +++ b/android/support/wear/widget/SimpleAnimatorListener.java @@ -29,7 +29,7 @@ import android.support.annotation.RestrictTo.Scope; * @hide Hidden until this goes through review */ @RequiresApi(Build.VERSION_CODES.KITKAT_WATCH) -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public class SimpleAnimatorListener implements Animator.AnimatorListener { private boolean mWasCanceled; diff --git a/android/support/wear/widget/SwipeDismissLayout.java b/android/support/wear/widget/SwipeDismissLayout.java index 6e7a6f36..33da79c2 100644 --- a/android/support/wear/widget/SwipeDismissLayout.java +++ b/android/support/wear/widget/SwipeDismissLayout.java @@ -16,12 +16,11 @@ package android.support.wear.widget; -import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; - import android.content.Context; import android.content.res.Resources; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; +import android.support.annotation.RestrictTo.Scope; import android.support.annotation.UiThread; import android.util.AttributeSet; import android.util.Log; @@ -40,7 +39,7 @@ import android.widget.FrameLayout; * * @hide */ -@RestrictTo(LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) @UiThread class SwipeDismissLayout extends FrameLayout { private static final String TAG = "SwipeDismissLayout"; diff --git a/android/support/wear/widget/WearableRecyclerView.java b/android/support/wear/widget/WearableRecyclerView.java index 5cacdfcb..1425e68b 100644 --- a/android/support/wear/widget/WearableRecyclerView.java +++ b/android/support/wear/widget/WearableRecyclerView.java @@ -16,11 +16,9 @@ package android.support.wear.widget; -import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Point; -import android.os.Build; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.support.wear.R; @@ -35,7 +33,6 @@ import android.view.ViewTreeObserver; * * @see #setCircularScrollingGestureEnabled(boolean) */ -@TargetApi(Build.VERSION_CODES.M) public class WearableRecyclerView extends RecyclerView { private static final String TAG = "WearableRecyclerView"; diff --git a/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java b/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java index f1cb640d..e9b2a40a 100644 --- a/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java +++ b/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java @@ -32,7 +32,7 @@ import java.lang.ref.WeakReference; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) class AbsListViewFlingWatcher implements FlingWatcher, OnScrollListener { private final FlingListener mListener; diff --git a/android/support/wear/widget/drawer/FlingWatcherFactory.java b/android/support/wear/widget/drawer/FlingWatcherFactory.java index 3fe84c63..2fdfa13c 100644 --- a/android/support/wear/widget/drawer/FlingWatcherFactory.java +++ b/android/support/wear/widget/drawer/FlingWatcherFactory.java @@ -33,7 +33,7 @@ import java.util.WeakHashMap; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) class FlingWatcherFactory { /** diff --git a/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java b/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java index ca95ab22..4c0e5c8c 100644 --- a/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java +++ b/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java @@ -38,7 +38,7 @@ import java.lang.ref.WeakReference; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) class NestedScrollViewFlingWatcher implements FlingWatcher, OnScrollChangeListener { static final int MAX_WAIT_TIME_MS = 100; diff --git a/android/support/wear/widget/drawer/PageIndicatorView.java b/android/support/wear/widget/drawer/PageIndicatorView.java index 99c7c09f..1285f725 100644 --- a/android/support/wear/widget/drawer/PageIndicatorView.java +++ b/android/support/wear/widget/drawer/PageIndicatorView.java @@ -54,7 +54,7 @@ import java.util.concurrent.TimeUnit; * @hide */ @RequiresApi(Build.VERSION_CODES.M) -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) public class PageIndicatorView extends View implements OnPageChangeListener { private static final String TAG = "Dots"; diff --git a/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java b/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java index 7570fae5..7916875e 100644 --- a/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java +++ b/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java @@ -31,7 +31,7 @@ import java.lang.ref.WeakReference; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) class RecyclerViewFlingWatcher extends OnScrollListener implements FlingWatcher { private final FlingListener mListener; diff --git a/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java b/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java index f0b973be..5154e7bb 100644 --- a/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java +++ b/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java @@ -38,7 +38,7 @@ import java.lang.ref.WeakReference; * * @hide */ -@RestrictTo(Scope.LIBRARY_GROUP) +@RestrictTo(Scope.LIBRARY) class ScrollViewFlingWatcher implements FlingWatcher, OnScrollChangeListener { static final int MAX_WAIT_TIME_MS = 100; diff --git a/android/support/wear/widget/drawer/WearableActionDrawerMenu.java b/android/support/wear/widget/drawer/WearableActionDrawerMenu.java index 158467dc..092ac72d 100644 --- a/android/support/wear/widget/drawer/WearableActionDrawerMenu.java +++ b/android/support/wear/widget/drawer/WearableActionDrawerMenu.java @@ -16,12 +16,10 @@ package android.support.wear.widget.drawer; -import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; -import android.os.Build; import android.support.annotation.Nullable; import android.view.ActionProvider; import android.view.ContextMenu; @@ -34,7 +32,6 @@ import android.view.View; import java.util.ArrayList; import java.util.List; -@TargetApi(Build.VERSION_CODES.M) /* package */ class WearableActionDrawerMenu implements Menu { private final Context mContext; diff --git a/android/support/wear/widget/drawer/WearableActionDrawerView.java b/android/support/wear/widget/drawer/WearableActionDrawerView.java index 8154e6b9..99cd4ff6 100644 --- a/android/support/wear/widget/drawer/WearableActionDrawerView.java +++ b/android/support/wear/widget/drawer/WearableActionDrawerView.java @@ -16,12 +16,10 @@ package android.support.wear.widget.drawer; -import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.os.Build; import android.support.annotation.Nullable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -38,6 +36,7 @@ import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.ImageView; import android.widget.LinearLayout; @@ -75,7 +74,6 @@ import java.util.Objects; * <p>For {@link MenuItem}, setting and getting the title and icon, {@link MenuItem#getItemId}, and * {@link MenuItem#setOnMenuItemClickListener} are implemented. */ -@TargetApi(Build.VERSION_CODES.M) public class WearableActionDrawerView extends WearableDrawerView { private static final String TAG = "WearableActionDrawer"; @@ -140,12 +138,8 @@ public class WearableActionDrawerView extends WearableDrawerView { View peekView = layoutInflater.inflate(R.layout.ws_action_drawer_peek_view, getPeekContainer(), false /* attachToRoot */); setPeekContent(peekView); - mPeekActionIcon = - (ImageView) peekView - .findViewById(R.id.ws_action_drawer_peek_action_icon); - mPeekExpandIcon = - (ImageView) peekView - .findViewById(R.id.ws_action_drawer_expand_icon); + mPeekActionIcon = peekView.findViewById(R.id.ws_action_drawer_peek_action_icon); + mPeekExpandIcon = peekView.findViewById(R.id.ws_action_drawer_expand_icon); } else { mPeekActionIcon = null; mPeekExpandIcon = null; @@ -193,6 +187,16 @@ public class WearableActionDrawerView extends WearableDrawerView { } @Override + public void onDrawerOpened() { + if (mActionListAdapter.getItemCount() > 0) { + RecyclerView.ViewHolder holder = mActionList.findViewHolderForAdapterPosition(0); + if (holder != null && holder.itemView != null) { + holder.itemView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + } + } + + @Override public boolean canScrollHorizontally(int direction) { // Prevent the window from being swiped closed while it is open by saying that it can scroll // horizontally. @@ -412,7 +416,6 @@ public class WearableActionDrawerView extends WearableDrawerView { CharSequence title = mActionMenu.getItem(titleAwarePosition).getTitle(); holder.textView.setText(title); holder.textView.setContentDescription(title); - holder.iconView.setContentDescription(title); holder.iconView.setImageDrawable(icon); } else if (viewHolder instanceof TitleViewHolder) { TitleViewHolder holder = (TitleViewHolder) viewHolder; diff --git a/android/support/wear/widget/drawer/WearableDrawerLayout.java b/android/support/wear/widget/drawer/WearableDrawerLayout.java index 6d270647..e100a467 100644 --- a/android/support/wear/widget/drawer/WearableDrawerLayout.java +++ b/android/support/wear/widget/drawer/WearableDrawerLayout.java @@ -19,11 +19,10 @@ package android.support.wear.widget.drawer; import static android.support.wear.widget.drawer.WearableDrawerView.STATE_IDLE; import static android.support.wear.widget.drawer.WearableDrawerView.STATE_SETTLING; -import android.annotation.TargetApi; import android.content.Context; -import android.os.Build; import android.os.Handler; import android.os.Looper; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.view.NestedScrollingParent; @@ -98,7 +97,6 @@ import android.widget.FrameLayout; * </android.support.wear.widget.drawer.WearableDrawerView> * </android.support.wear.widget.drawer.WearableDrawerLayout></pre> */ -@TargetApi(Build.VERSION_CODES.M) public class WearableDrawerLayout extends FrameLayout implements View.OnLayoutChangeListener, NestedScrollingParent, FlingListener { @@ -654,12 +652,13 @@ public class WearableDrawerLayout extends FrameLayout } @Override // NestedScrollingParent - public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, + boolean consumed) { return false; } @Override // NestedScrollingParent - public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) { maybeUpdateScrollingContentView(target); mLastScrollWasFling = true; @@ -674,13 +673,13 @@ public class WearableDrawerLayout extends FrameLayout } @Override // NestedScrollingParent - public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) { maybeUpdateScrollingContentView(target); } @Override // NestedScrollingParent - public void onNestedScroll( - View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { boolean scrolledUp = dyConsumed < 0; boolean scrolledDown = dyConsumed > 0; @@ -873,18 +872,20 @@ public class WearableDrawerLayout extends FrameLayout } @Override // NestedScrollingParent - public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, + int nestedScrollAxes) { mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); } @Override // NestedScrollingParent - public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, + int nestedScrollAxes) { mCurrentNestedScrollSlopTracker = 0; return true; } @Override // NestedScrollingParent - public void onStopNestedScroll(View target) { + public void onStopNestedScroll(@NonNull View target) { mNestedScrollingParentHelper.onStopNestedScroll(target); } @@ -961,7 +962,7 @@ public class WearableDrawerLayout extends FrameLayout public abstract WearableDrawerView getDrawerView(); @Override - public boolean tryCaptureView(View child, int pointerId) { + public boolean tryCaptureView(@NonNull View child, int pointerId) { WearableDrawerView drawerView = getDrawerView(); // Returns true if the dragger is dragging the drawer. return child == drawerView && !drawerView.isLocked() @@ -969,13 +970,13 @@ public class WearableDrawerLayout extends FrameLayout } @Override - public int getViewVerticalDragRange(View child) { + public int getViewVerticalDragRange(@NonNull View child) { // Defines the vertical drag range of the drawer. return child == getDrawerView() ? child.getHeight() : 0; } @Override - public void onViewCaptured(View capturedChild, int activePointerId) { + public void onViewCaptured(@NonNull View capturedChild, int activePointerId) { showDrawerContentMaybeAnimate((WearableDrawerView) capturedChild); } @@ -1036,7 +1037,7 @@ public class WearableDrawerLayout extends FrameLayout private class TopDrawerDraggerCallback extends DrawerDraggerCallback { @Override - public int clampViewPositionVertical(View child, int top, int dy) { + public int clampViewPositionVertical(@NonNull View child, int top, int dy) { if (mTopDrawerView == child) { int peekHeight = mTopDrawerView.getPeekContainer().getHeight(); // The top drawer can be dragged vertically from peekHeight - height to 0. @@ -1063,7 +1064,7 @@ public class WearableDrawerLayout extends FrameLayout } @Override - public void onViewReleased(View releasedChild, float xvel, float yvel) { + public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) { if (releasedChild == mTopDrawerView) { // Settle to final position. Either swipe open or close. final float openedPercent = mTopDrawerView.getOpenedPercent(); @@ -1085,7 +1086,8 @@ public class WearableDrawerLayout extends FrameLayout } @Override - public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, + int dy) { if (changedView == mTopDrawerView) { // Compute the offset and invalidate will move the drawer during layout. final int height = changedView.getHeight(); @@ -1106,7 +1108,7 @@ public class WearableDrawerLayout extends FrameLayout private class BottomDrawerDraggerCallback extends DrawerDraggerCallback { @Override - public int clampViewPositionVertical(View child, int top, int dy) { + public int clampViewPositionVertical(@NonNull View child, int top, int dy) { if (mBottomDrawerView == child) { // The bottom drawer can be dragged vertically from (parentHeight - height) to // (parentHeight - peekHeight). @@ -1131,7 +1133,7 @@ public class WearableDrawerLayout extends FrameLayout } @Override - public void onViewReleased(View releasedChild, float xvel, float yvel) { + public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) { if (releasedChild == mBottomDrawerView) { // Settle to final position. Either swipe open or close. final int parentHeight = getHeight(); @@ -1151,7 +1153,8 @@ public class WearableDrawerLayout extends FrameLayout } @Override - public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, + int dy) { if (changedView == mBottomDrawerView) { // Compute the offset and invalidate will move the drawer during layout. final int height = changedView.getHeight(); diff --git a/android/support/wear/widget/drawer/WearableDrawerView.java b/android/support/wear/widget/drawer/WearableDrawerView.java index dafac39b..2462cba8 100644 --- a/android/support/wear/widget/drawer/WearableDrawerView.java +++ b/android/support/wear/widget/drawer/WearableDrawerView.java @@ -16,11 +16,9 @@ package android.support.wear.widget.drawer; -import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.os.Build; import android.support.annotation.IdRes; import android.support.annotation.IntDef; import android.support.annotation.Nullable; @@ -87,7 +85,6 @@ import java.lang.annotation.RetentionPolicy; * </LinearLayout> * </android.support.wear.widget.drawer.WearableDrawerView></pre> */ -@TargetApi(Build.VERSION_CODES.M) public class WearableDrawerView extends FrameLayout { /** * Indicates that the drawer is in an idle, settled state. No animation is in progress. @@ -109,7 +106,7 @@ public class WearableDrawerView extends FrameLayout { * @hide */ @Retention(RetentionPolicy.SOURCE) - @RestrictTo(Scope.LIBRARY_GROUP) + @RestrictTo(Scope.LIBRARY) @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING}) public @interface DrawerState {} @@ -155,8 +152,8 @@ public class WearableDrawerView extends FrameLayout { setElevation(context.getResources() .getDimension(R.dimen.ws_wearable_drawer_view_elevation)); - mPeekContainer = (ViewGroup) findViewById(R.id.ws_drawer_view_peek_container); - mPeekIcon = (ImageView) findViewById(R.id.ws_drawer_view_peek_icon); + mPeekContainer = findViewById(R.id.ws_drawer_view_peek_container); + mPeekIcon = findViewById(R.id.ws_drawer_view_peek_icon); mPeekContainer.setOnClickListener( new OnClickListener() { diff --git a/android/support/wear/widget/drawer/WearableNavigationDrawerView.java b/android/support/wear/widget/drawer/WearableNavigationDrawerView.java index 480812b8..c5c49fe3 100644 --- a/android/support/wear/widget/drawer/WearableNavigationDrawerView.java +++ b/android/support/wear/widget/drawer/WearableNavigationDrawerView.java @@ -16,11 +16,9 @@ package android.support.wear.widget.drawer; -import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.annotation.IntDef; @@ -58,7 +56,6 @@ import java.util.concurrent.TimeUnit; * <p>The developer may specify which style to use with the {@code app:navigationStyle} custom * attribute. If not specified, {@link #SINGLE_PAGE singlePage} will be used as the default. */ -@TargetApi(Build.VERSION_CODES.M) public class WearableNavigationDrawerView extends WearableDrawerView { private static final String TAG = "WearableNavDrawer"; @@ -79,7 +76,7 @@ public class WearableNavigationDrawerView extends WearableDrawerView { * @hide */ @Retention(RetentionPolicy.SOURCE) - @RestrictTo(Scope.LIBRARY_GROUP) + @RestrictTo(Scope.LIBRARY) @IntDef({SINGLE_PAGE, MULTI_PAGE}) public @interface NavigationStyle {} @@ -282,7 +279,7 @@ public class WearableNavigationDrawerView extends WearableDrawerView { /** * @hide */ - @RestrictTo(Scope.LIBRARY_GROUP) + @RestrictTo(Scope.LIBRARY) public void setPresenter(WearableNavigationDrawerPresenter presenter) { mPresenter = presenter; } diff --git a/android/support/wearable/watchface/decomposition/package-info.java b/android/support/wearable/watchface/decomposition/package-info.java new file mode 100644 index 00000000..dbd815e7 --- /dev/null +++ b/android/support/wearable/watchface/decomposition/package-info.java @@ -0,0 +1,2 @@ +/** @hide */ +package android.support.wearable.watchface.decomposition; diff --git a/android/system/Int32Ref.java b/android/system/Int32Ref.java new file mode 100644 index 00000000..25a818da --- /dev/null +++ b/android/system/Int32Ref.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 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 android.system; + +/** + * A signed 32bit integer reference suitable for passing to lower-level system calls. + */ +public class Int32Ref { + public int value; + + public Int32Ref(int value) { + this.value = value; + } +} diff --git a/android/system/Int64Ref.java b/android/system/Int64Ref.java new file mode 100644 index 00000000..f42450d8 --- /dev/null +++ b/android/system/Int64Ref.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 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 android.system; + +/** + * A signed 64bit integer reference suitable for passing to lower-level system calls. + */ +public class Int64Ref { + public long value; + + public Int64Ref(long value) { + this.value = value; + } +} diff --git a/android/system/Os.java b/android/system/Os.java index 2dabae2f..fe8f7d45 100644 --- a/android/system/Os.java +++ b/android/system/Os.java @@ -188,6 +188,17 @@ public final class Os { public static int getgid() { return Libcore.os.getgid(); } /** + * See <a href="http://man7.org/linux/man-pages/man2/getgroups.2.html">getgroups(2)</a>. + * + * <p>Should the number of groups change during the execution of this call, the call may + * return an arbitrary subset. This may be worth reconsidering should this be exposed + * as public API. + * + * @hide + */ + public static int[] getgroups() throws ErrnoException { return Libcore.os.getgroups(); } + + /** * See <a href="http://man7.org/linux/man-pages/man3/getenv.3.html">getenv(3)</a>. */ public static String getenv(String name) { return Libcore.os.getenv(name); } @@ -268,7 +279,16 @@ public final class Os { public static InetAddress inet_pton(int family, String address) { return Libcore.os.inet_pton(family, address); } /** @hide */ public static InetAddress ioctlInetAddress(FileDescriptor fd, int cmd, String interfaceName) throws ErrnoException { return Libcore.os.ioctlInetAddress(fd, cmd, interfaceName); } - /** @hide */ public static int ioctlInt(FileDescriptor fd, int cmd, MutableInt arg) throws ErrnoException { return Libcore.os.ioctlInt(fd, cmd, arg); } + + + /** @hide */ public static int ioctlInt(FileDescriptor fd, int cmd, Int32Ref arg) throws ErrnoException { + libcore.util.MutableInt internalArg = new libcore.util.MutableInt(arg.value); + try { + return Libcore.os.ioctlInt(fd, cmd, internalArg); + } finally { + arg.value = internalArg.value; + } + } /** * See <a href="http://man7.org/linux/man-pages/man3/isatty.3.html">isatty(3)</a>. @@ -453,8 +473,41 @@ public final class Os { /** * See <a href="http://man7.org/linux/man-pages/man2/sendfile.2.html">sendfile(2)</a>. + * + * @deprecated This method will be removed in a future version of Android. Use + * {@link #sendfile(FileDescriptor, FileDescriptor, Int64Ref, long)} instead. + */ + @Deprecated + public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, MutableLong inOffset, long byteCount) throws ErrnoException { + if (inOffset == null) { + return Libcore.os.sendfile(outFd, inFd, null, byteCount); + } else { + libcore.util.MutableLong internalInOffset = new libcore.util.MutableLong( + inOffset.value); + try { + return Libcore.os.sendfile(outFd, inFd, internalInOffset, byteCount); + } finally { + inOffset.value = internalInOffset.value; + } + } + } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/sendfile.2.html">sendfile(2)</a>. */ - public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, MutableLong inOffset, long byteCount) throws ErrnoException { return Libcore.os.sendfile(outFd, inFd, inOffset, byteCount); } + public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, Int64Ref inOffset, long byteCount) throws ErrnoException { + if (inOffset == null) { + return Libcore.os.sendfile(outFd, inFd, null, byteCount); + } else { + libcore.util.MutableLong internalInOffset = new libcore.util.MutableLong( + inOffset.value); + try { + return Libcore.os.sendfile(outFd, inFd, internalInOffset, byteCount); + } finally { + inOffset.value = internalInOffset.value; + } + } + } /** * See <a href="http://man7.org/linux/man-pages/man2/sendto.2.html">sendto(2)</a>. @@ -492,6 +545,13 @@ public final class Os { public static void setgid(int gid) throws ErrnoException { Libcore.os.setgid(gid); } /** + * See <a href="http://man7.org/linux/man-pages/man2/setgroups.2.html">setgroups(2)</a>. + * + * @hide + */ + public static void setgroups(int[] gids) throws ErrnoException { Libcore.os.setgroups(gids); } + + /** * See <a href="http://man7.org/linux/man-pages/man2/setpgid.2.html">setpgid(2)</a>. */ /** @hide */ public static void setpgid(int pid, int pgid) throws ErrnoException { Libcore.os.setpgid(pid, pgid); } @@ -611,8 +671,41 @@ public final class Os { /** * See <a href="http://man7.org/linux/man-pages/man2/waitpid.2.html">waitpid(2)</a>. - */ - public static int waitpid(int pid, MutableInt status, int options) throws ErrnoException { return Libcore.os.waitpid(pid, status, options); } + * + * @deprecated This method will be removed in a future version of Android. Use + * {@link #waitpid(int, Int32Ref, int)} instead. + */ + @Deprecated + public static int waitpid(int pid, MutableInt status, int options) throws ErrnoException { + if (status == null) { + return Libcore.os.waitpid(pid, null, options); + } else { + libcore.util.MutableInt internalStatus = new libcore.util.MutableInt(status.value); + try { + return Libcore.os.waitpid(pid, internalStatus, options); + } finally { + status.value = internalStatus.value; + } + } + } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/waitpid.2.html">waitpid(2)</a>. + * + * @throws IllegalArgumentException if {@code status != null && status.length != 1} + */ + public static int waitpid(int pid, Int32Ref status, int options) throws ErrnoException { + if (status == null) { + return Libcore.os.waitpid(pid, null, options); + } else { + libcore.util.MutableInt internalStatus = new libcore.util.MutableInt(status.value); + try { + return Libcore.os.waitpid(pid, internalStatus, options); + } finally { + status.value = internalStatus.value; + } + } + } /** * See <a href="http://man7.org/linux/man-pages/man2/write.2.html">write(2)</a>. diff --git a/android/system/UnixSocketAddressTest.java b/android/system/UnixSocketAddressTest.java deleted file mode 100644 index f1b7fc23..00000000 --- a/android/system/UnixSocketAddressTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 android.system; - -import junit.framework.TestCase; - -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -public class UnixSocketAddressTest extends TestCase { - - public void testFilesystemSunPath() throws Exception { - String path = "/foo/bar"; - UnixSocketAddress sa = UnixSocketAddress.createFileSystem(path); - - byte[] abstractNameBytes = path.getBytes(StandardCharsets.UTF_8); - byte[] expected = new byte[abstractNameBytes.length + 1]; - // See unix(7) - System.arraycopy(abstractNameBytes, 0, expected, 0, abstractNameBytes.length); - assertTrue(Arrays.equals(expected, sa.getSunPath())); - } - - public void testUnnamedSunPath() throws Exception { - UnixSocketAddress sa = UnixSocketAddress.createUnnamed(); - assertEquals(0, sa.getSunPath().length); - } - - public void testAbstractSunPath() throws Exception { - String abstractName = "abstract"; - UnixSocketAddress sa = UnixSocketAddress.createAbstract(abstractName); - byte[] abstractNameBytes = abstractName.getBytes(StandardCharsets.UTF_8); - byte[] expected = new byte[abstractNameBytes.length + 1]; - // See unix(7) - System.arraycopy(abstractNameBytes, 0, expected, 1, abstractNameBytes.length); - assertTrue(Arrays.equals(expected, sa.getSunPath())); - } -} diff --git a/android/telecom/Call.java b/android/telecom/Call.java index e13bd619..a07f2bbf 100644 --- a/android/telecom/Call.java +++ b/android/telecom/Call.java @@ -855,6 +855,39 @@ public final class Call { */ public static abstract class Callback { /** + * @hide + */ + @IntDef({HANDOVER_FAILURE_DEST_APP_REJECTED, HANDOVER_FAILURE_DEST_NOT_SUPPORTED, + HANDOVER_FAILURE_DEST_INVALID_PERM, HANDOVER_FAILURE_DEST_USER_REJECTED}) + @Retention(RetentionPolicy.SOURCE) + public @interface HandoverFailureErrors {} + + /** + * Handover failure reason returned via {@link #onHandoverFailed(Call, int)} when the app + * to handover the call rejects handover. + */ + public static final int HANDOVER_FAILURE_DEST_APP_REJECTED = 1; + + /** + * Handover failure reason returned via {@link #onHandoverFailed(Call, int)} when there is + * an error associated with unsupported handover. + */ + public static final int HANDOVER_FAILURE_DEST_NOT_SUPPORTED = 2; + + /** + * Handover failure reason returned via {@link #onHandoverFailed(Call, int)} when there + * are some permission errors associated with APIs doing handover. + */ + public static final int HANDOVER_FAILURE_DEST_INVALID_PERM = 3; + + /** + * Handover failure reason returned via {@link #onHandoverFailed(Call, int)} when user + * rejects handover. + */ + public static final int HANDOVER_FAILURE_DEST_USER_REJECTED = 4; + + + /** * Invoked when the state of this {@code Call} has changed. See {@link #getState()}. * * @param call The {@code Call} invoking this method. @@ -989,6 +1022,21 @@ public final class Call { * {@link android.telecom.Connection.RttModifyStatus#SESSION_MODIFY_REQUEST_SUCCESS}. */ public void onRttInitiationFailure(Call call, int reason) {} + + /** + * Invoked when Call handover from one {@link PhoneAccount} to other {@link PhoneAccount} + * has completed successfully. + * @param call The call which had initiated handover. + */ + public void onHandoverComplete(Call call) {} + + /** + * Invoked when Call handover from one {@link PhoneAccount} to other {@link PhoneAccount} + * has failed. + * @param call The call which had initiated handover. + * @param failureReason Error reason for failure + */ + public void onHandoverFailed(Call call, @HandoverFailureErrors int failureReason) {} } /** @@ -1367,6 +1415,24 @@ public final class Call { } /** + * Initiates a handover of this {@link Call} to the {@link ConnectionService} identified + * by {@code toHandle}. The videoState specified indicates the desired video state after the + * handover. + * <p> + * A handover request is initiated by the user from one app to indicate a desire + * to handover a call to another. + * + * @param toHandle {@link PhoneAccountHandle} of the {@link ConnectionService} to handover + * this call to. + * @param videoState Indicates the video state desired after the handover. + * @param extras Bundle containing extra information to be passed to the + * {@link ConnectionService} + */ + public void handoverTo(PhoneAccountHandle toHandle, int videoState, Bundle extras) { + mInCallAdapter.handoverTo(mTelecomCallId, toHandle, videoState, extras); + } + + /** * Terminate the RTT session on this call. The resulting state change will be notified via * the {@link Callback#onRttStatusChanged(Call, boolean, RttCall)} callback. */ diff --git a/android/telecom/CallAudioState.java b/android/telecom/CallAudioState.java index f601d8b5..4b827d2e 100644 --- a/android/telecom/CallAudioState.java +++ b/android/telecom/CallAudioState.java @@ -16,16 +16,35 @@ package android.telecom; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.bluetooth.BluetoothDevice; import android.os.Parcel; import android.os.Parcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; /** * Encapsulates the telecom audio state, including the current audio routing, supported audio * routing and mute. */ public final class CallAudioState implements Parcelable { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value={ROUTE_EARPIECE, ROUTE_BLUETOOTH, ROUTE_WIRED_HEADSET, ROUTE_SPEAKER}, + flag=true) + public @interface CallAudioRoute {} + /** Direct the audio stream through the device's earpiece. */ public static final int ROUTE_EARPIECE = 0x00000001; @@ -55,6 +74,8 @@ public final class CallAudioState implements Parcelable { private final boolean isMuted; private final int route; private final int supportedRouteMask; + private final BluetoothDevice activeBluetoothDevice; + private final Collection<BluetoothDevice> supportedBluetoothDevices; /** * Constructor for a {@link CallAudioState} object. @@ -73,10 +94,21 @@ public final class CallAudioState implements Parcelable { * {@link #ROUTE_WIRED_HEADSET} * {@link #ROUTE_SPEAKER} */ - public CallAudioState(boolean muted, int route, int supportedRouteMask) { - this.isMuted = muted; + public CallAudioState(boolean muted, @CallAudioRoute int route, + @CallAudioRoute int supportedRouteMask) { + this(muted, route, supportedRouteMask, null, Collections.emptyList()); + } + + /** @hide */ + public CallAudioState(boolean isMuted, @CallAudioRoute int route, + @CallAudioRoute int supportedRouteMask, + @Nullable BluetoothDevice activeBluetoothDevice, + @NonNull Collection<BluetoothDevice> supportedBluetoothDevices) { + this.isMuted = isMuted; this.route = route; this.supportedRouteMask = supportedRouteMask; + this.activeBluetoothDevice = activeBluetoothDevice; + this.supportedBluetoothDevices = supportedBluetoothDevices; } /** @hide */ @@ -84,6 +116,8 @@ public final class CallAudioState implements Parcelable { isMuted = state.isMuted(); route = state.getRoute(); supportedRouteMask = state.getSupportedRouteMask(); + activeBluetoothDevice = state.activeBluetoothDevice; + supportedBluetoothDevices = state.getSupportedBluetoothDevices(); } /** @hide */ @@ -92,6 +126,8 @@ public final class CallAudioState implements Parcelable { isMuted = state.isMuted(); route = state.getRoute(); supportedRouteMask = state.getSupportedRouteMask(); + activeBluetoothDevice = null; + supportedBluetoothDevices = Collections.emptyList(); } @Override @@ -103,17 +139,32 @@ public final class CallAudioState implements Parcelable { return false; } CallAudioState state = (CallAudioState) obj; - return isMuted() == state.isMuted() && getRoute() == state.getRoute() && - getSupportedRouteMask() == state.getSupportedRouteMask(); + if (supportedBluetoothDevices.size() != state.supportedBluetoothDevices.size()) { + return false; + } + for (BluetoothDevice device : supportedBluetoothDevices) { + if (!state.supportedBluetoothDevices.contains(device)) { + return false; + } + } + return Objects.equals(activeBluetoothDevice, state.activeBluetoothDevice) && isMuted() == + state.isMuted() && getRoute() == state.getRoute() && getSupportedRouteMask() == + state.getSupportedRouteMask(); } @Override public String toString() { + String bluetoothDeviceList = supportedBluetoothDevices.stream() + .map(BluetoothDevice::getAddress).collect(Collectors.joining(", ")); + return String.format(Locale.US, - "[AudioState isMuted: %b, route: %s, supportedRouteMask: %s]", + "[AudioState isMuted: %b, route: %s, supportedRouteMask: %s, " + + "activeBluetoothDevice: [%s], supportedBluetoothDevices: [%s]]", isMuted, audioRouteToString(route), - audioRouteToString(supportedRouteMask)); + audioRouteToString(supportedRouteMask), + activeBluetoothDevice, + bluetoothDeviceList); } /** @@ -126,6 +177,7 @@ public final class CallAudioState implements Parcelable { /** * @return The current audio route being used. */ + @CallAudioRoute public int getRoute() { return route; } @@ -133,11 +185,27 @@ public final class CallAudioState implements Parcelable { /** * @return Bit mask of all routes supported by this call. */ + @CallAudioRoute public int getSupportedRouteMask() { return supportedRouteMask; } /** + * @return The {@link BluetoothDevice} through which audio is being routed. + * Will not be {@code null} if {@link #getRoute()} returns {@link #ROUTE_BLUETOOTH}. + */ + public BluetoothDevice getActiveBluetoothDevice() { + return activeBluetoothDevice; + } + + /** + * @return {@link List} of {@link BluetoothDevice}s that can be used for this call. + */ + public Collection<BluetoothDevice> getSupportedBluetoothDevices() { + return supportedBluetoothDevices; + } + + /** * Converts the provided audio route into a human readable string representation. * * @param route to convert into a string. @@ -177,7 +245,13 @@ public final class CallAudioState implements Parcelable { boolean isMuted = source.readByte() == 0 ? false : true; int route = source.readInt(); int supportedRouteMask = source.readInt(); - return new CallAudioState(isMuted, route, supportedRouteMask); + BluetoothDevice activeBluetoothDevice = source.readParcelable( + ClassLoader.getSystemClassLoader()); + List<BluetoothDevice> supportedBluetoothDevices = new ArrayList<>(); + source.readParcelableList(supportedBluetoothDevices, + ClassLoader.getSystemClassLoader()); + return new CallAudioState(isMuted, route, + supportedRouteMask, activeBluetoothDevice, supportedBluetoothDevices); } @Override @@ -202,6 +276,8 @@ public final class CallAudioState implements Parcelable { destination.writeByte((byte) (isMuted ? 1 : 0)); destination.writeInt(route); destination.writeInt(supportedRouteMask); + destination.writeParcelable(activeBluetoothDevice, 0); + destination.writeParcelableList(new ArrayList<>(supportedBluetoothDevices), 0); } private static void listAppend(StringBuffer buffer, String str) { diff --git a/android/telecom/Connection.java b/android/telecom/Connection.java index 8ba934cc..2bb1c4ed 100644 --- a/android/telecom/Connection.java +++ b/android/telecom/Connection.java @@ -25,6 +25,7 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.Notification; +import android.bluetooth.BluetoothDevice; import android.content.Intent; import android.hardware.camera2.CameraManager; import android.net.Uri; @@ -819,7 +820,7 @@ public abstract class Connection extends Conferenceable { public void onConnectionEvent(Connection c, String event, Bundle extras) {} /** @hide */ public void onConferenceSupportedChanged(Connection c, boolean isConferenceSupported) {} - public void onAudioRouteChanged(Connection c, int audioRoute) {} + public void onAudioRouteChanged(Connection c, int audioRoute, String bluetoothAddress) {} public void onRttInitiationSuccess(Connection c) {} public void onRttInitiationFailure(Connection c, int reason) {} public void onRttSessionRemotelyTerminated(Connection c) {} @@ -1683,6 +1684,8 @@ public abstract class Connection extends Conferenceable { // The internal telecom call ID associated with this connection. private String mTelecomCallId; + // The PhoneAccountHandle associated with this connection. + private PhoneAccountHandle mPhoneAccountHandle; private int mState = STATE_NEW; private CallAudioState mCallAudioState; private Uri mAddress; @@ -2576,7 +2579,29 @@ public abstract class Connection extends Conferenceable { */ public final void setAudioRoute(int route) { for (Listener l : mListeners) { - l.onAudioRouteChanged(this, route); + l.onAudioRouteChanged(this, route, null); + } + } + + /** + * + * Request audio routing to a specific bluetooth device. Calling this method may result in + * the device routing audio to a different bluetooth device than the one specified if the + * bluetooth stack is unable to route audio to the requested device. + * A list of available devices can be obtained via + * {@link CallAudioState#getSupportedBluetoothDevices()} + * + * <p> + * Used by self-managed {@link ConnectionService}s which wish to use bluetooth audio for a + * self-managed {@link Connection} (see {@link PhoneAccount#CAPABILITY_SELF_MANAGED}.) + * <p> + * See also {@link InCallService#requestBluetoothAudio(String)} + * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by + * {@link BluetoothDevice#getAddress()}. + */ + public void requestBluetoothAudio(@NonNull String bluetoothAddress) { + for (Listener l : mListeners) { + l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH, bluetoothAddress); } } @@ -3076,6 +3101,27 @@ public abstract class Connection extends Conferenceable { } /** + * Sets the {@link PhoneAccountHandle} associated with this connection. + * + * @hide + */ + public void setPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) { + if (mPhoneAccountHandle != phoneAccountHandle) { + mPhoneAccountHandle = phoneAccountHandle; + notifyPhoneAccountChanged(phoneAccountHandle); + } + } + + /** + * Returns the {@link PhoneAccountHandle} associated with this connection. + * + * @hide + */ + public PhoneAccountHandle getPhoneAccountHandle() { + return mPhoneAccountHandle; + } + + /** * Sends an event associated with this {@code Connection} with associated event extras to the * {@link InCallService}. * <p> diff --git a/android/telecom/ConnectionService.java b/android/telecom/ConnectionService.java index a81fba95..7e833066 100644 --- a/android/telecom/ConnectionService.java +++ b/android/telecom/ConnectionService.java @@ -1294,10 +1294,10 @@ public abstract class ConnectionService extends Service { } @Override - public void onAudioRouteChanged(Connection c, int audioRoute) { + public void onAudioRouteChanged(Connection c, int audioRoute, String bluetoothAddress) { String id = mIdByConnection.get(c); if (id != null) { - mAdapter.setAudioRoute(id, audioRoute); + mAdapter.setAudioRoute(id, audioRoute, bluetoothAddress); } } @@ -1382,7 +1382,7 @@ public abstract class ConnectionService extends Service { connection.setTelecomCallId(callId); if (connection.getState() != Connection.STATE_DISCONNECTED) { - addConnection(callId, connection); + addConnection(request.getAccountHandle(), callId, connection); } Uri address = connection.getAddress(); @@ -1846,6 +1846,7 @@ public abstract class ConnectionService extends Service { mAdapter.setIsConferenced(connectionId, id); } } + onConferenceAdded(conference); } } @@ -2033,6 +2034,43 @@ public abstract class ConnectionService extends Service { } /** + * Called by Telecom on the initiating side of the handover to create an instance of a + * handover connection. + * @param fromPhoneAccountHandle {@link PhoneAccountHandle} associated with the + * ConnectionService which needs to handover the call. + * @param request Details about the call which needs to be handover. + * @return Connection object corresponding to the handover call. + */ + public Connection onCreateOutgoingHandoverConnection(PhoneAccountHandle fromPhoneAccountHandle, + ConnectionRequest request) { + return null; + } + + /** + * Called by Telecom on the receiving side of the handover to request the + * {@link ConnectionService} to create an instance of a handover connection. + * @param fromPhoneAccountHandle {@link PhoneAccountHandle} associated with the + * ConnectionService which needs to handover the call. + * @param request Details about the call which needs to be handover. + * @return {@link Connection} object corresponding to the handover call. + */ + public Connection onCreateIncomingHandoverConnection(PhoneAccountHandle fromPhoneAccountHandle, + ConnectionRequest request) { + return null; + } + + /** + * Called by Telecom in response to a {@code TelecomManager#acceptHandover()} + * invocation which failed. + * @param request Details about the call which needs to be handover. + * @param error Reason for handover failure as defined in + * {@link android.telecom.Call.Callback#HANDOVER_FAILURE_DEST_INVALID_PERM} + */ + public void onHandoverFailed(ConnectionRequest request, int error) { + return; + } + + /** * Create a {@code Connection} for a new unknown call. An unknown call is a call originating * from the ConnectionService that was neither a user-initiated outgoing call, nor an incoming * call created using @@ -2056,6 +2094,30 @@ public abstract class ConnectionService extends Service { public void onConference(Connection connection1, Connection connection2) {} /** + * Called when a connection is added. + * @hide + */ + public void onConnectionAdded(Connection connection) {} + + /** + * Called when a connection is removed. + * @hide + */ + public void onConnectionRemoved(Connection connection) {} + + /** + * Called when a conference is added. + * @hide + */ + public void onConferenceAdded(Conference conference) {} + + /** + * Called when a conference is removed. + * @hide + */ + public void onConferenceRemoved(Conference conference) {} + + /** * Indicates that a remote conference has been created for existing {@link RemoteConnection}s. * When this method is invoked, this {@link ConnectionService} should create its own * representation of the conference call and send it to telecom using {@link #addConference}. @@ -2122,16 +2184,18 @@ public abstract class ConnectionService extends Service { // prefix for a unique incremental call ID. id = handle.getComponentName().getClassName() + "@" + getNextCallId(); } - addConnection(id, connection); + addConnection(handle, id, connection); return id; } - private void addConnection(String callId, Connection connection) { + private void addConnection(PhoneAccountHandle handle, String callId, Connection connection) { connection.setTelecomCallId(callId); mConnectionById.put(callId, connection); mIdByConnection.put(connection, callId); connection.addConnectionListener(mConnectionListener); connection.setConnectionService(this); + connection.setPhoneAccountHandle(handle); + onConnectionAdded(connection); } /** {@hide} */ @@ -2143,6 +2207,7 @@ public abstract class ConnectionService extends Service { mConnectionById.remove(id); mIdByConnection.remove(connection); mAdapter.removeCall(id); + onConnectionRemoved(connection); } } @@ -2179,6 +2244,8 @@ public abstract class ConnectionService extends Service { mConferenceById.remove(id); mIdByConference.remove(conference); mAdapter.removeCall(id); + + onConferenceRemoved(conference); } } diff --git a/android/telecom/ConnectionServiceAdapter.java b/android/telecom/ConnectionServiceAdapter.java index 111fcc78..92a9dc23 100644 --- a/android/telecom/ConnectionServiceAdapter.java +++ b/android/telecom/ConnectionServiceAdapter.java @@ -520,11 +520,14 @@ final class ConnectionServiceAdapter implements DeathRecipient { * @param callId The unique ID of the call. * @param audioRoute The new audio route (see {@code CallAudioState#ROUTE_*}). */ - void setAudioRoute(String callId, int audioRoute) { - Log.v(this, "setAudioRoute: %s %s", callId, CallAudioState.audioRouteToString(audioRoute)); + void setAudioRoute(String callId, int audioRoute, String bluetoothAddress) { + Log.v(this, "setAudioRoute: %s %s %s", callId, + CallAudioState.audioRouteToString(audioRoute), + bluetoothAddress); for (IConnectionServiceAdapter adapter : mAdapters) { try { - adapter.setAudioRoute(callId, audioRoute, Log.getExternalSession()); + adapter.setAudioRoute(callId, audioRoute, + bluetoothAddress, Log.getExternalSession()); } catch (RemoteException ignored) { } } diff --git a/android/telecom/ConnectionServiceAdapterServant.java b/android/telecom/ConnectionServiceAdapterServant.java index b1617f4d..3fbdeb1e 100644 --- a/android/telecom/ConnectionServiceAdapterServant.java +++ b/android/telecom/ConnectionServiceAdapterServant.java @@ -298,8 +298,8 @@ final class ConnectionServiceAdapterServant { case MSG_SET_AUDIO_ROUTE: { SomeArgs args = (SomeArgs) msg.obj; try { - mDelegate.setAudioRoute((String) args.arg1, args.argi1, - (Session.Info) args.arg2); + mDelegate.setAudioRoute((String) args.arg1, args.argi1, (String) args.arg2, + (Session.Info) args.arg3); } finally { args.recycle(); } @@ -548,12 +548,12 @@ final class ConnectionServiceAdapterServant { @Override public final void setAudioRoute(String connectionId, int audioRoute, - Session.Info sessionInfo) { - + String bluetoothAddress, Session.Info sessionInfo) { SomeArgs args = SomeArgs.obtain(); args.arg1 = connectionId; args.argi1 = audioRoute; - args.arg2 = sessionInfo; + args.arg2 = bluetoothAddress; + args.arg3 = sessionInfo; mHandler.obtainMessage(MSG_SET_AUDIO_ROUTE, args).sendToTarget(); } diff --git a/android/telecom/InCallAdapter.java b/android/telecom/InCallAdapter.java index 9559a28c..4bc2a9b1 100644 --- a/android/telecom/InCallAdapter.java +++ b/android/telecom/InCallAdapter.java @@ -16,6 +16,7 @@ package android.telecom; +import android.bluetooth.BluetoothDevice; import android.os.Bundle; import android.os.RemoteException; @@ -128,7 +129,22 @@ public final class InCallAdapter { */ public void setAudioRoute(int route) { try { - mAdapter.setAudioRoute(route); + mAdapter.setAudioRoute(route, null); + } catch (RemoteException e) { + } + } + + /** + * Request audio routing to a specific bluetooth device. Calling this method may result in + * the device routing audio to a different bluetooth device than the one specified. A list of + * available devices can be obtained via {@link CallAudioState#getSupportedBluetoothDevices()} + * + * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by + * {@link BluetoothDevice#getAddress()}, or {@code null} if no device is preferred. + */ + public void requestBluetoothAudio(String bluetoothAddress) { + try { + mAdapter.setAudioRoute(CallAudioState.ROUTE_BLUETOOTH, bluetoothAddress); } catch (RemoteException e) { } } @@ -419,4 +435,21 @@ public final class InCallAdapter { } catch (RemoteException ignored) { } } + + + /** + * Initiates a handover of this {@link Call} to the {@link ConnectionService} identified + * by destAcct. + * @param callId The callId of the Call which calls this function. + * @param destAcct ConnectionService to which the call should be handed over. + * @param videoState The video state desired after the handover. + * @param extras Extra information to be passed to ConnectionService + */ + public void handoverTo(String callId, PhoneAccountHandle destAcct, int videoState, + Bundle extras) { + try { + mAdapter.handoverTo(callId, destAcct, videoState, extras); + } catch (RemoteException ignored) { + } + } } diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java index e384d469..d558bbae 100644 --- a/android/telecom/InCallService.java +++ b/android/telecom/InCallService.java @@ -16,9 +16,11 @@ package android.telecom; +import android.annotation.NonNull; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.app.Service; +import android.bluetooth.BluetoothDevice; import android.content.Intent; import android.hardware.camera2.CameraManager; import android.net.Uri; @@ -377,6 +379,22 @@ public abstract class InCallService extends Service { } /** + * Request audio routing to a specific bluetooth device. Calling this method may result in + * the device routing audio to a different bluetooth device than the one specified if the + * bluetooth stack is unable to route audio to the requested device. + * A list of available devices can be obtained via + * {@link CallAudioState#getSupportedBluetoothDevices()} + * + * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by + * {@link BluetoothDevice#getAddress()}. + */ + public final void requestBluetoothAudio(@NonNull String bluetoothAddress) { + if (mPhone != null) { + mPhone.requestBluetoothAudio(bluetoothAddress); + } + } + + /** * Invoked when the {@code Phone} has been created. This is a signal to the in-call experience * to start displaying in-call information to the user. Each instance of {@code InCallService} * will have only one {@code Phone}, and this method will be called exactly once in the lifetime diff --git a/android/telecom/Phone.java b/android/telecom/Phone.java index 066f6c26..421b1a4b 100644 --- a/android/telecom/Phone.java +++ b/android/telecom/Phone.java @@ -17,7 +17,9 @@ package android.telecom; import android.annotation.SystemApi; +import android.bluetooth.BluetoothDevice; import android.os.Bundle; +import android.os.RemoteException; import android.util.ArrayMap; import java.util.Collections; @@ -295,6 +297,18 @@ public final class Phone { } /** + * Request audio routing to a specific bluetooth device. Calling this method may result in + * the device routing audio to a different bluetooth device than the one specified. A list of + * available devices can be obtained via {@link CallAudioState#getSupportedBluetoothDevices()} + * + * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by + * {@link BluetoothDevice#getAddress()}, or {@code null} if no device is preferred. + */ + public void requestBluetoothAudio(String bluetoothAddress) { + mInCallAdapter.requestBluetoothAudio(bluetoothAddress); + } + + /** * Turns the proximity sensor on. When this request is made, the proximity sensor will * become active, and the touch screen and display will be turned off when the user's face * is detected to be in close proximity to the screen. This operation is a no-op on devices diff --git a/android/telecom/PhoneAccount.java b/android/telecom/PhoneAccount.java index 691e7cf1..74b94650 100644 --- a/android/telecom/PhoneAccount.java +++ b/android/telecom/PhoneAccount.java @@ -86,13 +86,11 @@ public final class PhoneAccount implements Parcelable { /** * Boolean {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which * indicates whether this {@link PhoneAccount} is capable of supporting a request to handover a - * connection (see {@link android.telecom.Call#EVENT_REQUEST_HANDOVER}) to this - * {@link PhoneAccount} from a {@link PhoneAccount} specifying - * {@link #EXTRA_SUPPORTS_HANDOVER_FROM}. + * connection (see {@code android.telecom.Call#handoverTo()}) to this {@link PhoneAccount} from + * a {@link PhoneAccount} specifying {@link #EXTRA_SUPPORTS_HANDOVER_FROM}. * <p> * A handover request is initiated by the user from the default dialer app to indicate a desire * to handover a call from one {@link PhoneAccount}/{@link ConnectionService} to another. - * @hide */ public static final String EXTRA_SUPPORTS_HANDOVER_TO = "android.telecom.extra.SUPPORTS_HANDOVER_TO"; @@ -113,12 +111,11 @@ public final class PhoneAccount implements Parcelable { * Boolean {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which * indicates whether this {@link PhoneAccount} is capable of supporting a request to handover a * connection from this {@link PhoneAccount} to another {@link PhoneAccount}. - * (see {@link android.telecom.Call#EVENT_REQUEST_HANDOVER}) which specifies + * (see {@code android.telecom.Call#handoverTo()}) which specifies * {@link #EXTRA_SUPPORTS_HANDOVER_TO}. * <p> * A handover request is initiated by the user from the default dialer app to indicate a desire * to handover a call from one {@link PhoneAccount}/{@link ConnectionService} to another. - * @hide */ public static final String EXTRA_SUPPORTS_HANDOVER_FROM = "android.telecom.extra.SUPPORTS_HANDOVER_FROM"; @@ -132,7 +129,6 @@ public final class PhoneAccount implements Parcelable { * <p> * By default, Self-Managed {@link PhoneAccount}s do not log their calls to the call log. * Setting this extra to {@code true} provides a means for them to log their calls. - * @hide */ public static final String EXTRA_LOG_SELF_MANAGED_CALLS = "android.telecom.extra.LOG_SELF_MANAGED_CALLS"; diff --git a/android/telecom/RemoteConnectionService.java b/android/telecom/RemoteConnectionService.java index 2cc43143..85906ad1 100644 --- a/android/telecom/RemoteConnectionService.java +++ b/android/telecom/RemoteConnectionService.java @@ -398,7 +398,8 @@ final class RemoteConnectionService { } @Override - public void setAudioRoute(String callId, int audioRoute, Session.Info sessionInfo) { + public void setAudioRoute(String callId, int audioRoute, String bluetoothAddress, + Session.Info sessionInfo) { if (hasConnection(callId)) { // TODO(3pcalls): handle this for remote connections. // Likely we don't want to do anything since it doesn't make sense for self-managed diff --git a/android/telecom/TelecomManager.java b/android/telecom/TelecomManager.java index 9e52c71b..da32e0be 100644 --- a/android/telecom/TelecomManager.java +++ b/android/telecom/TelecomManager.java @@ -1750,6 +1750,41 @@ public class TelecomManager { return false; } + /** + * Called from the recipient side of a handover to indicate a desire to accept the handover + * of an ongoing call to another {@link ConnectionService} identified by + * {@link PhoneAccountHandle} destAcct. For managed {@link ConnectionService}s, the specified + * {@link PhoneAccountHandle} must have been registered with {@link #registerPhoneAccount} and + * the user must have enabled the corresponding {@link PhoneAccount}. This can be checked using + * {@link #getPhoneAccount}. Self-managed {@link ConnectionService}s must have + * {@link android.Manifest.permission#MANAGE_OWN_CALLS} to handover a call to it. + * <p> + * Once invoked, this method will cause the system to bind to the {@link ConnectionService} + * associated with the {@link PhoneAccountHandle} destAcct and call + * (See {@link ConnectionService#onCreateIncomingHandoverConnection}). + * <p> + * For a managed {@link ConnectionService}, a {@link SecurityException} will be thrown if either + * the {@link PhoneAccountHandle} destAcct does not correspond to a registered + * {@link PhoneAccount} or the associated {@link PhoneAccount} is not currently enabled by the + * user. + * <p> + * For a self-managed {@link ConnectionService}, a {@link SecurityException} will be thrown if + * the calling app does not have {@link android.Manifest.permission#MANAGE_OWN_CALLS}. + * + * @param srcAddr The {@link android.net.Uri} of the ongoing call to handover to the caller’s + * {@link ConnectionService}. + * @param videoState Video state after the handover. + * @param destAcct The {@link PhoneAccountHandle} registered to the calling package. + */ + public void acceptHandover(Uri srcAddr, int videoState, PhoneAccountHandle destAcct) { + try { + if (isServiceConnected()) { + getTelecomService().acceptHandover(srcAddr, videoState, destAcct); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException acceptHandover: " + e); + } + } private ITelecomService getTelecomService() { if (mTelecomServiceOverride != null) { diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java index 6fc7d23a..1db6ef7b 100644 --- a/android/telephony/CarrierConfigManager.java +++ b/android/telephony/CarrierConfigManager.java @@ -352,6 +352,17 @@ public class CarrierConfigManager { public static final String KEY_DEFAULT_VM_NUMBER_STRING = "default_vm_number_string"; /** + * Flag that specifies to use the user's own phone number as the voicemail number when there is + * no pre-loaded voicemail number on the SIM card. + * <p> + * {@link #KEY_DEFAULT_VM_NUMBER_STRING} takes precedence over this flag. + * <p> + * If false, the system default (*86) will be used instead. + */ + public static final String KEY_CONFIG_TELEPHONY_USE_OWN_NUMBER_FOR_VOICEMAIL_BOOL = + "config_telephony_use_own_number_for_voicemail_bool"; + + /** * When {@code true}, changes to the mobile data enabled switch will not cause the VT * registration state to change. That is, turning on or off mobile data will not cause VT to be * enabled or disabled. @@ -844,6 +855,14 @@ public class CarrierConfigManager { public static final String KEY_HIDE_ENHANCED_4G_LTE_BOOL = "hide_enhanced_4g_lte_bool"; /** + * Default Enhanced 4G LTE mode enabled. When this is {@code true}, Enhanced 4G LTE mode by + * default is on, otherwise if {@code false}, Enhanced 4G LTE mode by default is off. + * @hide + */ + public static final String KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL = + "enhanced_4g_lte_on_by_default_bool"; + + /** * Determine whether IMS apn can be shown. */ public static final String KEY_HIDE_IMS_APN_BOOL = "hide_ims_apn_bool"; @@ -1187,24 +1206,21 @@ public class CarrierConfigManager { */ public static final String KEY_CDMA_3WAYCALL_FLASH_DELAY_INT = "cdma_3waycall_flash_delay_int"; - /** - * @hide - * The default value for preferred CDMA roaming mode (aka CDMA system select.) - * CDMA_ROAMING_MODE_RADIO_DEFAULT = the default roaming mode from the radio - * CDMA_ROAMING_MODE_HOME = Home Networks - * CDMA_ROAMING_MODE_AFFILIATED = Roaming on Affiliated networks - * CDMA_ROAMING_MODE_ANY = Roaming on any networks + * The CDMA roaming mode (aka CDMA system select). + * + * <p>The value should be one of the CDMA_ROAMING_MODE_ constants in {@link TelephonyManager}. + * Values other than {@link TelephonyManager#CDMA_ROAMING_MODE_RADIO_DEFAULT} (which is the + * default) will take precedence over user selection. + * + * @see TelephonyManager#CDMA_ROAMING_MODE_RADIO_DEFAULT + * @see TelephonyManager#CDMA_ROAMING_MODE_HOME + * @see TelephonyManager#CDMA_ROAMING_MODE_AFFILIATED + * @see TelephonyManager#CDMA_ROAMING_MODE_ANY */ public static final String KEY_CDMA_ROAMING_MODE_INT = "cdma_roaming_mode_int"; - /** @hide */ - public static final int CDMA_ROAMING_MODE_RADIO_DEFAULT = -1; - /** @hide */ - public static final int CDMA_ROAMING_MODE_HOME = 0; - /** @hide */ - public static final int CDMA_ROAMING_MODE_AFFILIATED = 1; - /** @hide */ - public static final int CDMA_ROAMING_MODE_ANY = 2; + + /** * Boolean indicating if support is provided for directly dialing FDN number from FDN list. * If false, this feature is not supported. @@ -1535,6 +1551,13 @@ public class CarrierConfigManager { "boosted_lte_earfcns_string_array"; /** + * Determine whether to use only RSRP for the number of LTE signal bars. + * @hide + */ + public static final String KEY_USE_ONLY_RSRP_FOR_LTE_SIGNAL_BAR_BOOL = + "use_only_rsrp_for_lte_signal_bar_bool"; + + /** * Key identifying if voice call barring notification is required to be shown to the user. * @hide */ @@ -1628,6 +1651,33 @@ public class CarrierConfigManager { public static final String KEY_FEATURE_ACCESS_CODES_STRING_ARRAY = "feature_access_codes_string_array"; + /** + * Determines if the carrier wants to identify high definition calls in the call log. + * @hide + */ + public static final String KEY_IDENTIFY_HIGH_DEFINITION_CALLS_IN_CALL_LOG_BOOL = + "identify_high_definition_calls_in_call_log_bool"; + + /** + * Flag specifying whether to use the {@link ServiceState} roaming status, which can be + * affected by other carrier configs (e.g. + * {@link #KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY}), when setting the SPN display. + * <p> + * If {@code true}, the SPN display uses {@link ServiceState#getRoaming}. + * If {@code false} the SPN display checks if the current MCC/MNC is different from the + * SIM card's MCC/MNC. + * + * @see KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY + * @see KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY + * @see KEY_NON_ROAMING_OPERATOR_STRING_ARRAY + * @see KEY_ROAMING_OPERATOR_STRING_ARRAY + * @see KEY_FORCE_HOME_NETWORK_BOOL + * + * @hide + */ + public static final String KEY_SPN_DISPLAY_RULE_USE_ROAMING_FROM_SERVICE_STATE_BOOL = + "spn_display_rule_use_roaming_from_service_state_bool"; + /** The default value for every variable. */ private final static PersistableBundle sDefaults; @@ -1645,6 +1695,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_NOTIFY_HANDOVER_VIDEO_FROM_WIFI_TO_LTE_BOOL, false); sDefaults.putBoolean(KEY_SUPPORT_DOWNGRADE_VT_TO_AUDIO_BOOL, true); sDefaults.putString(KEY_DEFAULT_VM_NUMBER_STRING, ""); + sDefaults.putBoolean(KEY_CONFIG_TELEPHONY_USE_OWN_NUMBER_FOR_VOICEMAIL_BOOL, false); sDefaults.putBoolean(KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS, true); sDefaults.putBoolean(KEY_VILTE_DATA_IS_METERED_BOOL, true); sDefaults.putBoolean(KEY_CARRIER_WFC_IMS_AVAILABLE_BOOL, false); @@ -1769,6 +1820,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_DISPLAY_HD_AUDIO_PROPERTY_BOOL, true); sDefaults.putBoolean(KEY_EDITABLE_ENHANCED_4G_LTE_BOOL, true); sDefaults.putBoolean(KEY_HIDE_ENHANCED_4G_LTE_BOOL, false); + sDefaults.putBoolean(KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL, true); sDefaults.putBoolean(KEY_HIDE_IMS_APN_BOOL, false); sDefaults.putBoolean(KEY_HIDE_PREFERRED_NETWORK_TYPE_BOOL, false); sDefaults.putBoolean(KEY_ALLOW_EMERGENCY_VIDEO_CALLS_BOOL, false); @@ -1821,7 +1873,8 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_ALLOW_NON_EMERGENCY_CALLS_IN_ECM_BOOL, true); sDefaults.putBoolean(KEY_USE_RCS_PRESENCE_BOOL, false); sDefaults.putBoolean(KEY_FORCE_IMEI_BOOL, false); - sDefaults.putInt(KEY_CDMA_ROAMING_MODE_INT, CDMA_ROAMING_MODE_RADIO_DEFAULT); + sDefaults.putInt( + KEY_CDMA_ROAMING_MODE_INT, TelephonyManager.CDMA_ROAMING_MODE_RADIO_DEFAULT); sDefaults.putString(KEY_RCS_CONFIG_SERVER_URL_STRING, ""); // Carrier Signalling Receivers @@ -1892,6 +1945,7 @@ public class CarrierConfigManager { null); sDefaults.putInt(KEY_LTE_EARFCNS_RSRP_BOOST_INT, 0); sDefaults.putStringArray(KEY_BOOSTED_LTE_EARFCNS_STRING_ARRAY, null); + sDefaults.putBoolean(KEY_USE_ONLY_RSRP_FOR_LTE_SIGNAL_BAR_BOOL, false); sDefaults.putBoolean(KEY_DISABLE_VOICE_BARRING_NOTIFICATION_BOOL, false); sDefaults.putInt(IMSI_KEY_AVAILABILITY_INT, 0); sDefaults.putString(IMSI_KEY_DOWNLOAD_URL_STRING, null); @@ -1902,6 +1956,8 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false); sDefaults.putBoolean(KEY_DISABLE_CHARGE_INDICATION_BOOL, false); sDefaults.putStringArray(KEY_FEATURE_ACCESS_CODES_STRING_ARRAY, null); + sDefaults.putBoolean(KEY_IDENTIFY_HIGH_DEFINITION_CALLS_IN_CALL_LOG_BOOL, false); + sDefaults.putBoolean(KEY_SPN_DISPLAY_RULE_USE_ROAMING_FROM_SERVICE_STATE_BOOL, false); } /** diff --git a/android/telephony/PhoneStateListener.java b/android/telephony/PhoneStateListener.java index afff6d54..9ccfa942 100644 --- a/android/telephony/PhoneStateListener.java +++ b/android/telephony/PhoneStateListener.java @@ -204,16 +204,6 @@ public class PhoneStateListener { public static final int LISTEN_VOLTE_STATE = 0x00004000; /** - * Listen for OEM hook raw event - * - * @see #onOemHookRawEvent - * @hide - * @deprecated OEM needs a vendor-extension hal and their apps should use that instead - */ - @Deprecated - public static final int LISTEN_OEM_HOOK_RAW_EVENT = 0x00008000; - - /** * Listen for carrier network changes indicated by a carrier app. * * @see #onCarrierNetworkRequest @@ -359,9 +349,6 @@ public class PhoneStateListener { case LISTEN_DATA_ACTIVATION_STATE: PhoneStateListener.this.onDataActivationStateChanged((int)msg.obj); break; - case LISTEN_OEM_HOOK_RAW_EVENT: - PhoneStateListener.this.onOemHookRawEvent((byte[])msg.obj); - break; case LISTEN_CARRIER_NETWORK_CHANGE: PhoneStateListener.this.onCarrierNetworkChange((boolean)msg.obj); break; @@ -556,16 +543,6 @@ public class PhoneStateListener { } /** - * Callback invoked when OEM hook raw event is received. Requires - * the READ_PRIVILEGED_PHONE_STATE permission. - * @param rawData is the byte array of the OEM hook raw data. - * @hide - */ - public void onOemHookRawEvent(byte[] rawData) { - // default implementation empty - } - - /** * Callback invoked when telephony has received notice from a carrier * app that a network action that could result in connectivity loss * has been requested by an app using @@ -677,10 +654,6 @@ public class PhoneStateListener { send(LISTEN_DATA_ACTIVATION_STATE, 0, 0, activationState); } - public void onOemHookRawEvent(byte[] rawData) { - send(LISTEN_OEM_HOOK_RAW_EVENT, 0, 0, rawData); - } - public void onCarrierNetworkChange(boolean active) { send(LISTEN_CARRIER_NETWORK_CHANGE, 0, 0, active); } diff --git a/android/telephony/SignalStrength.java b/android/telephony/SignalStrength.java index c8b47765..de02de7b 100644 --- a/android/telephony/SignalStrength.java +++ b/android/telephony/SignalStrength.java @@ -68,6 +68,7 @@ public class SignalStrength implements Parcelable { private int mTdScdmaRscp; private boolean isGsm; // This value is set by the ServiceStateTracker onSignalStrengthResult + private boolean mUseOnlyRsrpForLteLevel; // Use only RSRP for the number of LTE signal bar. /** * Create a new SignalStrength from a intent notifier Bundle @@ -108,6 +109,7 @@ public class SignalStrength implements Parcelable { mLteRsrpBoost = 0; mTdScdmaRscp = INVALID; isGsm = true; + mUseOnlyRsrpForLteLevel = false; } /** @@ -134,6 +136,7 @@ public class SignalStrength implements Parcelable { mLteRsrpBoost = 0; mTdScdmaRscp = INVALID; isGsm = gsmFlag; + mUseOnlyRsrpForLteLevel = false; } /** @@ -145,10 +148,10 @@ public class SignalStrength implements Parcelable { int cdmaDbm, int cdmaEcio, int evdoDbm, int evdoEcio, int evdoSnr, int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi, - int lteRsrpBoost, int tdScdmaRscp, boolean gsmFlag) { + int lteRsrpBoost, int tdScdmaRscp, boolean gsmFlag, boolean lteLevelBaseOnRsrp) { initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio, evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp, - lteRsrq, lteRssnr, lteCqi, lteRsrpBoost, gsmFlag); + lteRsrq, lteRssnr, lteCqi, lteRsrpBoost, gsmFlag, lteLevelBaseOnRsrp); mTdScdmaRscp = tdScdmaRscp; } @@ -164,7 +167,7 @@ public class SignalStrength implements Parcelable { int tdScdmaRscp, boolean gsmFlag) { initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio, evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp, - lteRsrq, lteRssnr, lteCqi, 0, gsmFlag); + lteRsrq, lteRssnr, lteCqi, 0, gsmFlag, false); mTdScdmaRscp = tdScdmaRscp; } @@ -180,7 +183,7 @@ public class SignalStrength implements Parcelable { boolean gsmFlag) { initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio, evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp, - lteRsrq, lteRssnr, lteCqi, 0, gsmFlag); + lteRsrq, lteRssnr, lteCqi, 0, gsmFlag, false); } /** @@ -194,7 +197,7 @@ public class SignalStrength implements Parcelable { boolean gsmFlag) { initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio, evdoDbm, evdoEcio, evdoSnr, 99, INVALID, - INVALID, INVALID, INVALID, 0, gsmFlag); + INVALID, INVALID, INVALID, 0, gsmFlag, false); } /** @@ -228,7 +231,7 @@ public class SignalStrength implements Parcelable { boolean gsm) { initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio, evdoDbm, evdoEcio, evdoSnr, 99, INVALID, - INVALID, INVALID, INVALID, 0, gsm); + INVALID, INVALID, INVALID, 0, gsm, false); } /** @@ -248,6 +251,7 @@ public class SignalStrength implements Parcelable { * @param lteCqi * @param lteRsrpBoost * @param gsm + * @param useOnlyRsrpForLteLevel * * @hide */ @@ -255,7 +259,7 @@ public class SignalStrength implements Parcelable { int cdmaDbm, int cdmaEcio, int evdoDbm, int evdoEcio, int evdoSnr, int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi, - int lteRsrpBoost, boolean gsm) { + int lteRsrpBoost, boolean gsm, boolean useOnlyRsrpForLteLevel) { mGsmSignalStrength = gsmSignalStrength; mGsmBitErrorRate = gsmBitErrorRate; mCdmaDbm = cdmaDbm; @@ -271,6 +275,7 @@ public class SignalStrength implements Parcelable { mLteRsrpBoost = lteRsrpBoost; mTdScdmaRscp = INVALID; isGsm = gsm; + mUseOnlyRsrpForLteLevel = useOnlyRsrpForLteLevel; if (DBG) log("initialize: " + toString()); } @@ -293,6 +298,7 @@ public class SignalStrength implements Parcelable { mLteRsrpBoost = s.mLteRsrpBoost; mTdScdmaRscp = s.mTdScdmaRscp; isGsm = s.isGsm; + mUseOnlyRsrpForLteLevel = s.mUseOnlyRsrpForLteLevel; } /** @@ -318,6 +324,7 @@ public class SignalStrength implements Parcelable { mLteRsrpBoost = in.readInt(); mTdScdmaRscp = in.readInt(); isGsm = (in.readInt() != 0); + mUseOnlyRsrpForLteLevel = (in.readInt() != 0); } /** @@ -366,6 +373,7 @@ public class SignalStrength implements Parcelable { out.writeInt(mLteRsrpBoost); out.writeInt(mTdScdmaRscp); out.writeInt(isGsm ? 1 : 0); + out.writeInt(mUseOnlyRsrpForLteLevel ? 1 : 0); } /** @@ -449,6 +457,17 @@ public class SignalStrength implements Parcelable { } /** + * @param useOnlyRsrpForLteLevel true if it uses only RSRP for the number of LTE signal bar, + * otherwise false. + * + * Used by phone to use only RSRP or not for the number of LTE signal bar. + * @hide + */ + public void setUseOnlyRsrpForLteLevel(boolean useOnlyRsrpForLteLevel) { + mUseOnlyRsrpForLteLevel = useOnlyRsrpForLteLevel; + } + + /** * @param lteRsrpBoost - signal strength offset * * Used by phone to set the lte signal strength offset which will be @@ -835,6 +854,13 @@ public class SignalStrength implements Parcelable { } } + if (useOnlyRsrpForLteLevel()) { + log("getLTELevel - rsrp = " + rsrpIconLevel); + if (rsrpIconLevel != -1) { + return rsrpIconLevel; + } + } + /* * Values are -200 dB to +300 (SNR*10dB) RS_SNR >= 13.0 dB =>4 bars 4.5 * dB <= RS_SNR < 13.0 dB => 3 bars 1.0 dB <= RS_SNR < 4.5 dB => 2 bars @@ -915,6 +941,15 @@ public class SignalStrength implements Parcelable { } /** + * @return true if it uses only RSRP for the number of LTE signal bar, otherwise false. + * + * @hide + */ + public boolean useOnlyRsrpForLteLevel() { + return this.mUseOnlyRsrpForLteLevel; + } + + /** * @return get TD_SCDMA dbm * * @hide @@ -974,7 +1009,8 @@ public class SignalStrength implements Parcelable { + (mEvdoDbm * primeNum) + (mEvdoEcio * primeNum) + (mEvdoSnr * primeNum) + (mLteSignalStrength * primeNum) + (mLteRsrp * primeNum) + (mLteRsrq * primeNum) + (mLteRssnr * primeNum) + (mLteCqi * primeNum) - + (mLteRsrpBoost * primeNum) + (mTdScdmaRscp * primeNum) + (isGsm ? 1 : 0)); + + (mLteRsrpBoost * primeNum) + (mTdScdmaRscp * primeNum) + (isGsm ? 1 : 0) + + (mUseOnlyRsrpForLteLevel ? 1 : 0)); } /** @@ -1008,7 +1044,8 @@ public class SignalStrength implements Parcelable { && mLteCqi == s.mLteCqi && mLteRsrpBoost == s.mLteRsrpBoost && mTdScdmaRscp == s.mTdScdmaRscp - && isGsm == s.isGsm); + && isGsm == s.isGsm + && mUseOnlyRsrpForLteLevel == s.mUseOnlyRsrpForLteLevel); } /** @@ -1031,7 +1068,9 @@ public class SignalStrength implements Parcelable { + " " + mLteCqi + " " + mLteRsrpBoost + " " + mTdScdmaRscp - + " " + (isGsm ? "gsm|lte" : "cdma")); + + " " + (isGsm ? "gsm|lte" : "cdma") + + " " + (mUseOnlyRsrpForLteLevel ? "use_only_rsrp_for_lte_level" : + "use_rsrp_and_rssnr_for_lte_level")); } /** Returns the signal strength related to GSM. */ @@ -1086,6 +1125,7 @@ public class SignalStrength implements Parcelable { mLteRsrpBoost = m.getInt("lteRsrpBoost"); mTdScdmaRscp = m.getInt("TdScdma"); isGsm = m.getBoolean("isGsm"); + mUseOnlyRsrpForLteLevel = m.getBoolean("useOnlyRsrpForLteLevel"); } /** @@ -1110,6 +1150,7 @@ public class SignalStrength implements Parcelable { m.putInt("lteRsrpBoost", mLteRsrpBoost); m.putInt("TdScdma", mTdScdmaRscp); m.putBoolean("isGsm", isGsm); + m.putBoolean("useOnlyRsrpForLteLevel", mUseOnlyRsrpForLteLevel); } /** diff --git a/android/telephony/SmsManager.java b/android/telephony/SmsManager.java index 98195ada..5d039268 100644 --- a/android/telephony/SmsManager.java +++ b/android/telephony/SmsManager.java @@ -338,16 +338,18 @@ public final class SmsManager { /** * Send a text based SMS without writing it into the SMS Provider. * + * <p> + * The message will be sent directly over the network and will not be visible in SMS + * applications. Intended for internal carrier use only. + * </p> + * * <p>Requires Permission: * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier * privileges. * </p> * * @see #sendTextMessage(String, String, String, PendingIntent, PendingIntent) - * @hide */ - @SystemApi - @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void sendTextMessageWithoutPersisting( String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent) { @@ -387,6 +389,112 @@ public final class SmsManager { } /** + * Send a text based SMS with messaging options. + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param text the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * @param priority Priority level of the message + * Refer specification See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1 + * --------------------------------- + * PRIORITY | Level of Priority + * --------------------------------- + * '00' | Normal + * '01' | Interactive + * '10' | Urgent + * '11' | Emergency + * ---------------------------------- + * Any Other values included Negative considered as Invalid Priority Indicator of the message. + * @param expectMore is a boolean to indicate the sending messages through same link or not. + * @param validityPeriod Validity Period of the message in mins. + * Refer specification 3GPP TS 23.040 V6.8.1 section 9.2.3.12.1. + * Validity Period(Minimum) -> 5 mins + * Validity Period(Maximum) -> 635040 mins(i.e.63 weeks). + * Any Other values included Negative considered as Invalid Validity Period of the message. + * + * @throws IllegalArgumentException if destinationAddress or text are empty + * {@hide} + */ + public void sendTextMessage( + String destinationAddress, String scAddress, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent, + int priority, boolean expectMore, int validityPeriod) { + sendTextMessageInternal(destinationAddress, scAddress, text, sentIntent, deliveryIntent, + true /* persistMessage*/, priority, expectMore, validityPeriod); + } + + private void sendTextMessageInternal( + String destinationAddress, String scAddress, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent, boolean persistMessage, + int priority, boolean expectMore, int validityPeriod) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + + if (TextUtils.isEmpty(text)) { + throw new IllegalArgumentException("Invalid message body"); + } + + if (priority < 0x00 || priority > 0x03) { + throw new IllegalArgumentException("Invalid priority"); + } + + if (validityPeriod < 0x05 || validityPeriod > 0x09b0a0) { + throw new IllegalArgumentException("Invalid validity period"); + } + + try { + ISms iccISms = getISmsServiceOrThrow(); + if (iccISms != null) { + iccISms.sendTextForSubscriberWithOptions(getSubscriptionId(), + ActivityThread.currentPackageName(), destinationAddress, scAddress, text, + sentIntent, deliveryIntent, persistMessage, priority, expectMore, + validityPeriod); + } + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Send a text based SMS without writing it into the SMS Provider. + * + * <p>Requires Permission: + * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier + * privileges. + * </p> + * + * @see #sendTextMessage(String, String, String, PendingIntent, + * PendingIntent, int, boolean, int) + * @hide + */ + public void sendTextMessageWithoutPersisting( + String destinationAddress, String scAddress, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent, int priority, + boolean expectMore, int validityPeriod) { + sendTextMessageInternal(destinationAddress, scAddress, text, sentIntent, deliveryIntent, + false /* persistMessage */, priority, expectMore, validityPeriod); + } + + /** + * * Inject an SMS PDU into the android application framework. * * <p>Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or carrier @@ -544,6 +652,140 @@ public final class SmsManager { } /** + * Send a multi-part text based SMS with messaging options. The callee should have already + * divided the message into correctly sized parts by calling + * <code>divideMessage</code>. + * + * <p class="note"><strong>Note:</strong> Using this method requires that your app has the + * {@link android.Manifest.permission#SEND_SMS} permission.</p> + * + * <p class="note"><strong>Note:</strong> Beginning with Android 4.4 (API level 19), if + * <em>and only if</em> an app is not selected as the default SMS app, the system automatically + * writes messages sent using this method to the SMS Provider (the default SMS app is always + * responsible for writing its sent messages to the SMS Provider). For information about + * how to behave as the default SMS app, see {@link android.provider.Telephony}.</p> + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param parts an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * @param sentIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been sent. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> each sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been delivered + * to the recipient. The raw pdu of the status report is in the + * extended data ("pdu"). + * @param priority Priority level of the message + * Refer specification See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1 + * --------------------------------- + * PRIORITY | Level of Priority + * --------------------------------- + * '00' | Normal + * '01' | Interactive + * '10' | Urgent + * '11' | Emergency + * ---------------------------------- + * Any Other values included Negative considered as Invalid Priority Indicator of the message. + * @param expectMore is a boolean to indicate the sending messages through same link or not. + * @param validityPeriod Validity Period of the message in mins. + * Refer specification 3GPP TS 23.040 V6.8.1 section 9.2.3.12.1. + * Validity Period(Minimum) -> 5 mins + * Validity Period(Maximum) -> 635040 mins(i.e.63 weeks). + * Any Other values included Negative considered as Invalid Validity Period of the message. + * + * @throws IllegalArgumentException if destinationAddress or data are empty + * {@hide} + */ + public void sendMultipartTextMessage( + String destinationAddress, String scAddress, ArrayList<String> parts, + ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents, + int priority, boolean expectMore, int validityPeriod) { + sendMultipartTextMessageInternal(destinationAddress, scAddress, parts, sentIntents, + deliveryIntents, true /* persistMessage*/); + } + + private void sendMultipartTextMessageInternal( + String destinationAddress, String scAddress, List<String> parts, + List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents, + boolean persistMessage, int priority, boolean expectMore, int validityPeriod) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + if (parts == null || parts.size() < 1) { + throw new IllegalArgumentException("Invalid message body"); + } + + if (priority < 0x00 || priority > 0x03) { + throw new IllegalArgumentException("Invalid priority"); + } + + if (validityPeriod < 0x05 || validityPeriod > 0x09b0a0) { + throw new IllegalArgumentException("Invalid validity period"); + } + + if (parts.size() > 1) { + try { + ISms iccISms = getISmsServiceOrThrow(); + if (iccISms != null) { + iccISms.sendMultipartTextForSubscriberWithOptions(getSubscriptionId(), + ActivityThread.currentPackageName(), destinationAddress, scAddress, + parts, sentIntents, deliveryIntents, persistMessage, priority, + expectMore, validityPeriod); + } + } catch (RemoteException ex) { + // ignore it + } + } else { + PendingIntent sentIntent = null; + PendingIntent deliveryIntent = null; + if (sentIntents != null && sentIntents.size() > 0) { + sentIntent = sentIntents.get(0); + } + if (deliveryIntents != null && deliveryIntents.size() > 0) { + deliveryIntent = deliveryIntents.get(0); + } + sendTextMessageInternal(destinationAddress, scAddress, parts.get(0), + sentIntent, deliveryIntent, persistMessage, priority, expectMore, + validityPeriod); + } + } + + /** + * Send a multi-part text based SMS without writing it into the SMS Provider. + * + * <p>Requires Permission: + * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier + * privileges. + * </p> + * + * @see #sendMultipartTextMessage(String, String, ArrayList, ArrayList, + * ArrayList, int, boolean, int) + * @hide + **/ + public void sendMultipartTextMessageWithoutPersisting( + String destinationAddress, String scAddress, List<String> parts, + List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents, + int priority, boolean expectMore, int validityPeriod) { + sendMultipartTextMessageInternal(destinationAddress, scAddress, parts, sentIntents, + deliveryIntents, false /* persistMessage*/, priority, expectMore, + validityPeriod); + } + + /** * Send a data based SMS to a specific application port. * * <p class="note"><strong>Note:</strong> Using this method requires that your app has the @@ -1006,7 +1248,7 @@ public final class SmsManager { * <code>getAllMessagesFromIcc</code> * @return <code>ArrayList</code> of <code>SmsMessage</code> objects. */ - private static ArrayList<SmsMessage> createMessageListFromRawRecords(List<SmsRawData> records) { + private ArrayList<SmsMessage> createMessageListFromRawRecords(List<SmsRawData> records) { ArrayList<SmsMessage> messages = new ArrayList<SmsMessage>(); if (records != null) { int count = records.size(); @@ -1014,7 +1256,8 @@ public final class SmsManager { SmsRawData data = records.get(i); // List contains all records, including "free" records (null) if (data != null) { - SmsMessage sms = SmsMessage.createFromEfRecord(i+1, data.getBytes()); + SmsMessage sms = SmsMessage.createFromEfRecord(i+1, data.getBytes(), + getSubscriptionId()); if (sms != null) { messages.add(sms); } diff --git a/android/telephony/SmsMessage.java b/android/telephony/SmsMessage.java index df412335..a5d67c60 100644 --- a/android/telephony/SmsMessage.java +++ b/android/telephony/SmsMessage.java @@ -271,6 +271,31 @@ public class SmsMessage { } /** + * Create an SmsMessage from an SMS EF record. + * + * @param index Index of SMS record. This should be index in ArrayList + * returned by SmsManager.getAllMessagesFromSim + 1. + * @param data Record data. + * @param subId Subscription Id of the SMS + * @return An SmsMessage representing the record. + * + * @hide + */ + public static SmsMessage createFromEfRecord(int index, byte[] data, int subId) { + SmsMessageBase wrappedMessage; + + if (isCdmaVoice(subId)) { + wrappedMessage = com.android.internal.telephony.cdma.SmsMessage.createFromEfRecord( + index, data); + } else { + wrappedMessage = com.android.internal.telephony.gsm.SmsMessage.createFromEfRecord( + index, data); + } + + return wrappedMessage != null ? new SmsMessage(wrappedMessage) : null; + } + + /** * Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the * length in bytes (not hex chars) less the SMSC header * @@ -822,6 +847,7 @@ public class SmsMessage { int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(subId); return (PHONE_TYPE_CDMA == activePhone); } + /** * Decide if the carrier supports long SMS. * {@hide} diff --git a/android/telephony/SubscriptionManager.java b/android/telephony/SubscriptionManager.java index 88f4880a..2f39ddb1 100644 --- a/android/telephony/SubscriptionManager.java +++ b/android/telephony/SubscriptionManager.java @@ -360,6 +360,42 @@ public class SubscriptionManager { public static final String CB_OPT_OUT_DIALOG = "show_cmas_opt_out_dialog"; /** + * TelephonyProvider column name for enable Volte. + *@hide + */ + public static final String ENHANCED_4G_MODE_ENABLED = "volte_vt_enabled"; + + /** + * TelephonyProvider column name for enable VT (Video Telephony over IMS) + *@hide + */ + public static final String VT_IMS_ENABLED = "vt_ims_enabled"; + + /** + * TelephonyProvider column name for enable Wifi calling + *@hide + */ + public static final String WFC_IMS_ENABLED = "wfc_ims_enabled"; + + /** + * TelephonyProvider column name for Wifi calling mode + *@hide + */ + public static final String WFC_IMS_MODE = "wfc_ims_mode"; + + /** + * TelephonyProvider column name for Wifi calling mode in roaming + *@hide + */ + public static final String WFC_IMS_ROAMING_MODE = "wfc_ims_roaming_mode"; + + /** + * TelephonyProvider column name for enable Wifi calling in roaming + *@hide + */ + public static final String WFC_IMS_ROAMING_ENABLED = "wfc_ims_roaming_enabled"; + + /** * Broadcast Action: The user has changed one of the default subs related to * data, phone calls, or sms</p> * diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java index c0564c55..4ffb3c32 100644 --- a/android/telephony/TelephonyManager.java +++ b/android/telephony/TelephonyManager.java @@ -953,6 +953,27 @@ public class TelephonyManager { */ public static final int USSD_ERROR_SERVICE_UNAVAIL = -2; + /** + * Value for {@link CarrierConfigManager#KEY_CDMA_ROAMING_MODE_INT} which leaves the roaming + * mode set to the radio default or to the user's preference if they've indicated one. + */ + public static final int CDMA_ROAMING_MODE_RADIO_DEFAULT = -1; + /** + * Value for {@link CarrierConfigManager#KEY_CDMA_ROAMING_MODE_INT} which only permits + * connections on home networks. + */ + public static final int CDMA_ROAMING_MODE_HOME = 0; + /** + * Value for {@link CarrierConfigManager#KEY_CDMA_ROAMING_MODE_INT} which permits roaming on + * affiliated networks. + */ + public static final int CDMA_ROAMING_MODE_AFFILIATED = 1; + /** + * Value for {@link CarrierConfigManager#KEY_CDMA_ROAMING_MODE_INT} which permits roaming on + * any network. + */ + public static final int CDMA_ROAMING_MODE_ANY = 2; + // // // Device Info @@ -2145,13 +2166,16 @@ public class TelephonyManager { * @hide */ public String getSimOperatorNumeric() { - int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + int subId = mSubId; if (!SubscriptionManager.isUsableSubIdValue(subId)) { - subId = SubscriptionManager.getDefaultSmsSubscriptionId(); + subId = SubscriptionManager.getDefaultDataSubscriptionId(); if (!SubscriptionManager.isUsableSubIdValue(subId)) { - subId = SubscriptionManager.getDefaultVoiceSubscriptionId(); + subId = SubscriptionManager.getDefaultSmsSubscriptionId(); if (!SubscriptionManager.isUsableSubIdValue(subId)) { - subId = SubscriptionManager.getDefaultSubscriptionId(); + subId = SubscriptionManager.getDefaultVoiceSubscriptionId(); + if (!SubscriptionManager.isUsableSubIdValue(subId)) { + subId = SubscriptionManager.getDefaultSubscriptionId(); + } } } } @@ -5685,29 +5709,6 @@ public class TelephonyManager { return retVal; } - /** - * Returns the result and response from RIL for oem request - * - * @param oemReq the data is sent to ril. - * @param oemResp the respose data from RIL. - * @return negative value request was not handled or get error - * 0 request was handled succesfully, but no response data - * positive value success, data length of response - * @hide - * @deprecated OEM needs a vendor-extension hal and their apps should use that instead - */ - @Deprecated - public int invokeOemRilRequestRaw(byte[] oemReq, byte[] oemResp) { - try { - ITelephony telephony = getITelephony(); - if (telephony != null) - return telephony.invokeOemRilRequestRaw(oemReq, oemResp); - } catch (RemoteException ex) { - } catch (NullPointerException ex) { - } - return -1; - } - /** @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) diff --git a/android/telephony/TelephonyScanManager.java b/android/telephony/TelephonyScanManager.java index 92a21b6f..7bcdcdcc 100644 --- a/android/telephony/TelephonyScanManager.java +++ b/android/telephony/TelephonyScanManager.java @@ -73,8 +73,8 @@ public final class TelephonyScanManager { /** * Informs the user that there is some error about the scan. * - * This callback will be called whenever there is any error about the scan, but the scan - * won't stop unless the onComplete() callback is called. + * This callback will be called whenever there is any error about the scan, and the scan + * will be terminated. onComplete() will NOT be called. */ public void onError(int error) {} } diff --git a/android/telephony/euicc/DownloadableSubscription.java b/android/telephony/euicc/DownloadableSubscription.java index b5484e34..01041c8b 100644 --- a/android/telephony/euicc/DownloadableSubscription.java +++ b/android/telephony/euicc/DownloadableSubscription.java @@ -53,6 +53,8 @@ public final class DownloadableSubscription implements Parcelable { @Nullable public final String encodedActivationCode; + @Nullable private String confirmationCode; + // see getCarrierName and setCarrierName @Nullable private String carrierName; @@ -66,6 +68,7 @@ public final class DownloadableSubscription implements Parcelable { private DownloadableSubscription(Parcel in) { encodedActivationCode = in.readString(); + confirmationCode = in.readString(); carrierName = in.readString(); accessRules = in.createTypedArray(UiccAccessRule.CREATOR); } @@ -83,6 +86,21 @@ public final class DownloadableSubscription implements Parcelable { } /** + * Sets the confirmation code. + */ + public void setConfirmationCode(String confirmationCode) { + this.confirmationCode = confirmationCode; + } + + /** + * Returns the confirmation code. + */ + @Nullable + public String getConfirmationCode() { + return confirmationCode; + } + + /** * Set the user-visible carrier name. * @hide * @@ -134,6 +152,7 @@ public final class DownloadableSubscription implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(encodedActivationCode); + dest.writeString(confirmationCode); dest.writeString(carrierName); dest.writeTypedArray(accessRules, flags); } diff --git a/android/telephony/ims/ImsServiceProxy.java b/android/telephony/ims/ImsServiceProxy.java deleted file mode 100644 index 038e295d..00000000 --- a/android/telephony/ims/ImsServiceProxy.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright (C) 2017 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 android.telephony.ims; - -import android.app.PendingIntent; -import android.os.IBinder; -import android.os.Message; -import android.os.RemoteException; -import android.telephony.ims.feature.IRcsFeature; -import android.telephony.ims.feature.ImsFeature; -import android.util.Log; - -import com.android.ims.ImsCallProfile; -import com.android.ims.internal.IImsCallSession; -import com.android.ims.internal.IImsCallSessionListener; -import com.android.ims.internal.IImsConfig; -import com.android.ims.internal.IImsEcbm; -import com.android.ims.internal.IImsMultiEndpoint; -import com.android.ims.internal.IImsRegistrationListener; -import com.android.ims.internal.IImsServiceController; -import com.android.ims.internal.IImsServiceFeatureListener; -import com.android.ims.internal.IImsUt; - -/** - * A container of the IImsServiceController binder, which implements all of the ImsFeatures that - * the platform currently supports: MMTel and RCS. - * @hide - */ - -public class ImsServiceProxy extends ImsServiceProxyCompat implements IRcsFeature { - - protected String LOG_TAG = "ImsServiceProxy"; - private final int mSupportedFeature; - - // Start by assuming the proxy is available for usage. - private boolean mIsAvailable = true; - // ImsFeature Status from the ImsService. Cached. - private Integer mFeatureStatusCached = null; - private ImsServiceProxy.INotifyStatusChanged mStatusCallback; - private final Object mLock = new Object(); - - public interface INotifyStatusChanged { - void notifyStatusChanged(); - } - - private final IImsServiceFeatureListener mListenerBinder = - new IImsServiceFeatureListener.Stub() { - - @Override - public void imsFeatureCreated(int slotId, int feature) throws RemoteException { - // The feature has been re-enabled. This may happen when the service crashes. - synchronized (mLock) { - if (!mIsAvailable && mSlotId == slotId && feature == mSupportedFeature) { - Log.i(LOG_TAG, "Feature enabled on slotId: " + slotId + " for feature: " + - feature); - mIsAvailable = true; - } - } - } - - @Override - public void imsFeatureRemoved(int slotId, int feature) throws RemoteException { - synchronized (mLock) { - if (mIsAvailable && mSlotId == slotId && feature == mSupportedFeature) { - Log.i(LOG_TAG, "Feature disabled on slotId: " + slotId + " for feature: " + - feature); - mIsAvailable = false; - } - } - } - - @Override - public void imsStatusChanged(int slotId, int feature, int status) throws RemoteException { - synchronized (mLock) { - Log.i(LOG_TAG, "imsStatusChanged: slot: " + slotId + " feature: " + feature + - " status: " + status); - if (mSlotId == slotId && feature == mSupportedFeature) { - mFeatureStatusCached = status; - if (mStatusCallback != null) { - mStatusCallback.notifyStatusChanged(); - } - } - } - } - }; - - public ImsServiceProxy(int slotId, IBinder binder, int featureType) { - super(slotId, binder); - mSupportedFeature = featureType; - } - - public ImsServiceProxy(int slotId, int featureType) { - super(slotId, null /*IBinder*/); - mSupportedFeature = featureType; - } - - public IImsServiceFeatureListener getListener() { - return mListenerBinder; - } - - public void setBinder(IBinder binder) { - mBinder = binder; - } - - @Override - public int startSession(PendingIntent incomingCallIntent, IImsRegistrationListener listener) - throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).startSession(mSlotId, mSupportedFeature, - incomingCallIntent, listener); - } - } - - @Override - public void endSession(int sessionId) throws RemoteException { - synchronized (mLock) { - // Only check to make sure the binder connection still exists. This method should - // still be able to be called when the state is STATE_NOT_AVAILABLE. - checkBinderConnection(); - getServiceInterface(mBinder).endSession(mSlotId, mSupportedFeature, sessionId); - } - } - - @Override - public boolean isConnected(int callServiceType, int callType) - throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).isConnected(mSlotId, mSupportedFeature, - callServiceType, callType); - } - } - - @Override - public boolean isOpened() throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).isOpened(mSlotId, mSupportedFeature); - } - } - - @Override - public void addRegistrationListener(IImsRegistrationListener listener) - throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - getServiceInterface(mBinder).addRegistrationListener(mSlotId, mSupportedFeature, - listener); - } - } - - @Override - public void removeRegistrationListener(IImsRegistrationListener listener) - throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - getServiceInterface(mBinder).removeRegistrationListener(mSlotId, mSupportedFeature, - listener); - } - } - - @Override - public ImsCallProfile createCallProfile(int sessionId, int callServiceType, int callType) - throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).createCallProfile(mSlotId, mSupportedFeature, - sessionId, callServiceType, callType); - } - } - - @Override - public IImsCallSession createCallSession(int sessionId, ImsCallProfile profile, - IImsCallSessionListener listener) throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).createCallSession(mSlotId, mSupportedFeature, - sessionId, profile, listener); - } - } - - @Override - public IImsCallSession getPendingCallSession(int sessionId, String callId) - throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).getPendingCallSession(mSlotId, mSupportedFeature, - sessionId, callId); - } - } - - @Override - public IImsUt getUtInterface() throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).getUtInterface(mSlotId, mSupportedFeature); - } - } - - @Override - public IImsConfig getConfigInterface() throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).getConfigInterface(mSlotId, mSupportedFeature); - } - } - - @Override - public void turnOnIms() throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - getServiceInterface(mBinder).turnOnIms(mSlotId, mSupportedFeature); - } - } - - @Override - public void turnOffIms() throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - getServiceInterface(mBinder).turnOffIms(mSlotId, mSupportedFeature); - } - } - - @Override - public IImsEcbm getEcbmInterface() throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).getEcbmInterface(mSlotId, mSupportedFeature); - } - } - - @Override - public void setUiTTYMode(int uiTtyMode, Message onComplete) - throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - getServiceInterface(mBinder).setUiTTYMode(mSlotId, mSupportedFeature, uiTtyMode, - onComplete); - } - } - - @Override - public IImsMultiEndpoint getMultiEndpointInterface() throws RemoteException { - synchronized (mLock) { - checkServiceIsReady(); - return getServiceInterface(mBinder).getMultiEndpointInterface(mSlotId, - mSupportedFeature); - } - } - - @Override - public int getFeatureStatus() { - synchronized (mLock) { - if (isBinderAlive() && mFeatureStatusCached != null) { - Log.i(LOG_TAG, "getFeatureStatus - returning cached: " + mFeatureStatusCached); - return mFeatureStatusCached; - } - } - // Don't synchronize on Binder call. - Integer status = retrieveFeatureStatus(); - synchronized (mLock) { - if (status == null) { - return ImsFeature.STATE_NOT_AVAILABLE; - } - // Cache only non-null value for feature status. - mFeatureStatusCached = status; - } - Log.i(LOG_TAG, "getFeatureStatus - returning " + status); - return status; - } - - /** - * Internal method used to retrieve the feature status from the corresponding ImsService. - */ - private Integer retrieveFeatureStatus() { - if (mBinder != null) { - try { - return getServiceInterface(mBinder).getFeatureStatus(mSlotId, mSupportedFeature); - } catch (RemoteException e) { - // Status check failed, don't update cache - } - } - return null; - } - - /** - * @param c Callback that will fire when the feature status has changed. - */ - public void setStatusCallback(INotifyStatusChanged c) { - mStatusCallback = c; - } - - /** - * @return Returns true if the ImsService is ready to take commands, false otherwise. If this - * method returns false, it doesn't mean that the Binder connection is not available (use - * {@link #isBinderReady()} to check that), but that the ImsService is not accepting commands - * at this time. - * - * For example, for DSDS devices, only one slot can be {@link ImsFeature#STATE_READY} to take - * commands at a time, so the other slot must stay at {@link ImsFeature#STATE_NOT_AVAILABLE}. - */ - public boolean isBinderReady() { - return isBinderAlive() && getFeatureStatus() == ImsFeature.STATE_READY; - } - - @Override - public boolean isBinderAlive() { - return mIsAvailable && mBinder != null && mBinder.isBinderAlive(); - } - - protected void checkServiceIsReady() throws RemoteException { - if (!isBinderReady()) { - throw new RemoteException("ImsServiceProxy is not ready to accept commands."); - } - } - - private IImsServiceController getServiceInterface(IBinder b) { - return IImsServiceController.Stub.asInterface(b); - } -} diff --git a/android/telephony/ims/ImsServiceProxyCompat.java b/android/telephony/ims/ImsServiceProxyCompat.java deleted file mode 100644 index bbd5f027..00000000 --- a/android/telephony/ims/ImsServiceProxyCompat.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2017 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 android.telephony.ims; - -import android.app.PendingIntent; -import android.os.IBinder; -import android.os.Message; -import android.os.RemoteException; -import android.telephony.ims.feature.IMMTelFeature; -import android.telephony.ims.feature.ImsFeature; - -import com.android.ims.ImsCallProfile; -import com.android.ims.internal.IImsCallSession; -import com.android.ims.internal.IImsCallSessionListener; -import com.android.ims.internal.IImsConfig; -import com.android.ims.internal.IImsEcbm; -import com.android.ims.internal.IImsMultiEndpoint; -import com.android.ims.internal.IImsRegistrationListener; -import com.android.ims.internal.IImsService; -import com.android.ims.internal.IImsUt; - -/** - * Compatibility class that implements the new ImsService IMMTelFeature interface, but - * uses the old IImsService interface to support older devices that implement the deprecated - * opt/net/ims interface. - * @hide - */ - -public class ImsServiceProxyCompat implements IMMTelFeature { - - private static final int SERVICE_ID = ImsFeature.MMTEL; - - protected final int mSlotId; - protected IBinder mBinder; - - public ImsServiceProxyCompat(int slotId, IBinder binder) { - mSlotId = slotId; - mBinder = binder; - } - - @Override - public int startSession(PendingIntent incomingCallIntent, IImsRegistrationListener listener) - throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).open(mSlotId, ImsFeature.MMTEL, incomingCallIntent, - listener); - } - - @Override - public void endSession(int sessionId) throws RemoteException { - checkBinderConnection(); - getServiceInterface(mBinder).close(sessionId); - } - - @Override - public boolean isConnected(int callServiceType, int callType) - throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).isConnected(SERVICE_ID, callServiceType, callType); - } - - @Override - public boolean isOpened() throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).isOpened(SERVICE_ID); - } - - @Override - public void addRegistrationListener(IImsRegistrationListener listener) - throws RemoteException { - checkBinderConnection(); - getServiceInterface(mBinder).addRegistrationListener(mSlotId, ImsFeature.MMTEL, listener); - } - - @Override - public void removeRegistrationListener(IImsRegistrationListener listener) - throws RemoteException { - // Not Implemented in old ImsService. If the registration listener becomes invalid, the - // ImsService will remove. - } - - @Override - public ImsCallProfile createCallProfile(int sessionId, int callServiceType, int callType) - throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).createCallProfile(sessionId, callServiceType, callType); - } - - @Override - public IImsCallSession createCallSession(int sessionId, ImsCallProfile profile, - IImsCallSessionListener listener) throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).createCallSession(sessionId, profile, listener); - } - - @Override - public IImsCallSession getPendingCallSession(int sessionId, String callId) - throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).getPendingCallSession(sessionId, callId); - } - - @Override - public IImsUt getUtInterface() throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).getUtInterface(SERVICE_ID); - } - - @Override - public IImsConfig getConfigInterface() throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).getConfigInterface(mSlotId); - } - - @Override - public void turnOnIms() throws RemoteException { - checkBinderConnection(); - getServiceInterface(mBinder).turnOnIms(mSlotId); - } - - @Override - public void turnOffIms() throws RemoteException { - checkBinderConnection(); - getServiceInterface(mBinder).turnOffIms(mSlotId); - } - - @Override - public IImsEcbm getEcbmInterface() throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).getEcbmInterface(SERVICE_ID); - } - - @Override - public void setUiTTYMode(int uiTtyMode, Message onComplete) - throws RemoteException { - checkBinderConnection(); - getServiceInterface(mBinder).setUiTTYMode(SERVICE_ID, uiTtyMode, onComplete); - } - - @Override - public IImsMultiEndpoint getMultiEndpointInterface() throws RemoteException { - checkBinderConnection(); - return getServiceInterface(mBinder).getMultiEndpointInterface(SERVICE_ID); - } - - /** - * Base implementation, always returns READY for compatibility with old ImsService. - */ - public int getFeatureStatus() { - return ImsFeature.STATE_READY; - } - - /** - * @return false if the binder connection is no longer alive. - */ - public boolean isBinderAlive() { - return mBinder != null && mBinder.isBinderAlive(); - } - - private IImsService getServiceInterface(IBinder b) { - return IImsService.Stub.asInterface(b); - } - - protected void checkBinderConnection() throws RemoteException { - if (!isBinderAlive()) { - throw new RemoteException("ImsServiceProxy is not available for that feature."); - } - } -} diff --git a/android/telephony/ims/feature/IMMTelFeature.java b/android/telephony/ims/feature/IMMTelFeature.java deleted file mode 100644 index d65e27eb..00000000 --- a/android/telephony/ims/feature/IMMTelFeature.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2017 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 android.telephony.ims.feature; - -import android.app.PendingIntent; -import android.os.Message; -import android.os.RemoteException; - -import com.android.ims.ImsCallProfile; -import com.android.ims.internal.IImsCallSession; -import com.android.ims.internal.IImsCallSessionListener; -import com.android.ims.internal.IImsConfig; -import com.android.ims.internal.IImsEcbm; -import com.android.ims.internal.IImsMultiEndpoint; -import com.android.ims.internal.IImsRegistrationListener; -import com.android.ims.internal.IImsUt; - -/** - * MMTel interface for an ImsService. When updating this interface, ensure that base implementations - * of your changes are also present in MMTelFeature for compatibility with older versions of the - * MMTel feature. - * @hide - */ - -public interface IMMTelFeature { - - /** - * Notifies the MMTel feature that you would like to start a session. This should always be - * done before making/receiving IMS calls. The IMS service will register the device to the - * operator's network with the credentials (from ISIM) periodically in order to receive calls - * from the operator's network. When the IMS service receives a new call, it will send out an - * intent with the provided action string. The intent contains a call ID extra - * {@link IImsCallSession#getCallId} and it can be used to take a call. - * - * @param incomingCallIntent When an incoming call is received, the IMS service will call - * {@link PendingIntent#send} to send back the intent to the caller with - * {@link #INCOMING_CALL_RESULT_CODE} as the result code and the intent to fill in the call ID; - * It cannot be null. - * @param listener To listen to IMS registration events; It cannot be null - * @return an integer (greater than 0) representing the session id associated with the session - * that has been started. - */ - int startSession(PendingIntent incomingCallIntent, IImsRegistrationListener listener) - throws RemoteException; - - /** - * End a previously started session using the associated sessionId. - * @param sessionId an integer (greater than 0) representing the ongoing session. See - * {@link #startSession}. - */ - void endSession(int sessionId) throws RemoteException; - - /** - * Checks if the IMS service has successfully registered to the IMS network with the specified - * service & call type. - * - * @param callServiceType a service type that is specified in {@link ImsCallProfile} - * {@link ImsCallProfile#SERVICE_TYPE_NORMAL} - * {@link ImsCallProfile#SERVICE_TYPE_EMERGENCY} - * @param callType a call type that is specified in {@link ImsCallProfile} - * {@link ImsCallProfile#CALL_TYPE_VOICE_N_VIDEO} - * {@link ImsCallProfile#CALL_TYPE_VOICE} - * {@link ImsCallProfile#CALL_TYPE_VT} - * {@link ImsCallProfile#CALL_TYPE_VS} - * @return true if the specified service id is connected to the IMS network; false otherwise - * @throws RemoteException - */ - boolean isConnected(int callServiceType, int callType) throws RemoteException; - - /** - * Checks if the specified IMS service is opened. - * - * @return true if the specified service id is opened; false otherwise - */ - boolean isOpened() throws RemoteException; - - /** - * Add a new registration listener for the client associated with the session Id. - * @param listener An implementation of IImsRegistrationListener. - */ - void addRegistrationListener(IImsRegistrationListener listener) - throws RemoteException; - - /** - * Remove a previously registered listener using {@link #addRegistrationListener} for the client - * associated with the session Id. - * @param listener A previously registered IImsRegistrationListener - */ - void removeRegistrationListener(IImsRegistrationListener listener) - throws RemoteException; - - /** - * Creates a {@link ImsCallProfile} from the service capabilities & IMS registration state. - * - * @param sessionId a session id which is obtained from {@link #startSession} - * @param callServiceType a service type that is specified in {@link ImsCallProfile} - * {@link ImsCallProfile#SERVICE_TYPE_NONE} - * {@link ImsCallProfile#SERVICE_TYPE_NORMAL} - * {@link ImsCallProfile#SERVICE_TYPE_EMERGENCY} - * @param callType a call type that is specified in {@link ImsCallProfile} - * {@link ImsCallProfile#CALL_TYPE_VOICE} - * {@link ImsCallProfile#CALL_TYPE_VT} - * {@link ImsCallProfile#CALL_TYPE_VT_TX} - * {@link ImsCallProfile#CALL_TYPE_VT_RX} - * {@link ImsCallProfile#CALL_TYPE_VT_NODIR} - * {@link ImsCallProfile#CALL_TYPE_VS} - * {@link ImsCallProfile#CALL_TYPE_VS_TX} - * {@link ImsCallProfile#CALL_TYPE_VS_RX} - * @return a {@link ImsCallProfile} object - */ - ImsCallProfile createCallProfile(int sessionId, int callServiceType, int callType) - throws RemoteException; - - /** - * Creates a {@link ImsCallSession} with the specified call profile. - * Use other methods, if applicable, instead of interacting with - * {@link ImsCallSession} directly. - * - * @param sessionId a session id which is obtained from {@link #startSession} - * @param profile a call profile to make the call - * @param listener An implementation of IImsCallSessionListener. - */ - IImsCallSession createCallSession(int sessionId, ImsCallProfile profile, - IImsCallSessionListener listener) throws RemoteException; - - /** - * Retrieves the call session associated with a pending call. - * - * @param sessionId a session id which is obtained from {@link #startSession} - * @param callId a call id to make the call - */ - IImsCallSession getPendingCallSession(int sessionId, String callId) throws RemoteException; - - /** - * @return The Ut interface for the supplementary service configuration. - */ - IImsUt getUtInterface() throws RemoteException; - - /** - * @return The config interface for IMS Configuration - */ - IImsConfig getConfigInterface() throws RemoteException; - - /** - * Signal the MMTelFeature to turn on IMS when it has been turned off using {@link #turnOffIms} - * @param sessionId a session id which is obtained from {@link #startSession} - */ - void turnOnIms() throws RemoteException; - - /** - * Signal the MMTelFeature to turn off IMS when it has been turned on using {@link #turnOnIms} - * @param sessionId a session id which is obtained from {@link #startSession} - */ - void turnOffIms() throws RemoteException; - - /** - * @return The Emergency call-back mode interface for emergency VoLTE calls that support it. - */ - IImsEcbm getEcbmInterface() throws RemoteException; - - /** - * Sets the current UI TTY mode for the MMTelFeature. - * @param uiTtyMode An integer containing the new UI TTY Mode. - * @param onComplete A {@link Message} to be used when the mode has been set. - * @throws RemoteException - */ - void setUiTTYMode(int uiTtyMode, Message onComplete) throws RemoteException; - - /** - * @return MultiEndpoint interface for DEP notifications - */ - IImsMultiEndpoint getMultiEndpointInterface() throws RemoteException; -} diff --git a/android/telephony/ims/feature/MMTelFeature.java b/android/telephony/ims/feature/MMTelFeature.java index a71f0bf0..758c379f 100644 --- a/android/telephony/ims/feature/MMTelFeature.java +++ b/android/telephony/ims/feature/MMTelFeature.java @@ -32,90 +32,183 @@ import java.util.ArrayList; import java.util.List; /** - * Base implementation, which implements all methods in IMMTelFeature. Any class wishing to use - * MMTelFeature should extend this class and implement all methods that the service supports. + * Base implementation for MMTel. + * Any class wishing to use MMTelFeature should extend this class and implement all methods that the + * service supports. * * @hide */ -public class MMTelFeature extends ImsFeature implements IMMTelFeature { - - @Override +public class MMTelFeature extends ImsFeature { + + /** + * Notifies the MMTel feature that you would like to start a session. This should always be + * done before making/receiving IMS calls. The IMS service will register the device to the + * operator's network with the credentials (from ISIM) periodically in order to receive calls + * from the operator's network. When the IMS service receives a new call, it will send out an + * intent with the provided action string. The intent contains a call ID extra + * {@link IImsCallSession#getCallId} and it can be used to take a call. + * + * @param incomingCallIntent When an incoming call is received, the IMS service will call + * {@link PendingIntent#send} to send back the intent to the caller with + * ImsManager#INCOMING_CALL_RESULT_CODE as the result code and the intent to fill in the call + * ID; It cannot be null. + * @param listener To listen to IMS registration events; It cannot be null + * @return an integer (greater than 0) representing the session id associated with the session + * that has been started. + */ public int startSession(PendingIntent incomingCallIntent, IImsRegistrationListener listener) { return 0; } - @Override + /** + * End a previously started session using the associated sessionId. + * @param sessionId an integer (greater than 0) representing the ongoing session. See + * {@link #startSession}. + */ public void endSession(int sessionId) { } - @Override + /** + * Checks if the IMS service has successfully registered to the IMS network with the specified + * service & call type. + * + * @param callSessionType a service type that is specified in {@link ImsCallProfile} + * {@link ImsCallProfile#SERVICE_TYPE_NORMAL} + * {@link ImsCallProfile#SERVICE_TYPE_EMERGENCY} + * @param callType a call type that is specified in {@link ImsCallProfile} + * {@link ImsCallProfile#CALL_TYPE_VOICE_N_VIDEO} + * {@link ImsCallProfile#CALL_TYPE_VOICE} + * {@link ImsCallProfile#CALL_TYPE_VT} + * {@link ImsCallProfile#CALL_TYPE_VS} + * @return true if the specified service id is connected to the IMS network; false otherwise + */ public boolean isConnected(int callSessionType, int callType) { return false; } - @Override + /** + * Checks if the specified IMS service is opened. + * + * @return true if the specified service id is opened; false otherwise + */ public boolean isOpened() { return false; } - @Override + /** + * Add a new registration listener for the client associated with the session Id. + * @param listener An implementation of IImsRegistrationListener. + */ public void addRegistrationListener(IImsRegistrationListener listener) { } - @Override + /** + * Remove a previously registered listener using {@link #addRegistrationListener} for the client + * associated with the session Id. + * @param listener A previously registered IImsRegistrationListener + */ public void removeRegistrationListener(IImsRegistrationListener listener) { } - @Override + /** + * Creates a {@link ImsCallProfile} from the service capabilities & IMS registration state. + * + * @param sessionId a session id which is obtained from {@link #startSession} + * @param callSessionType a service type that is specified in {@link ImsCallProfile} + * {@link ImsCallProfile#SERVICE_TYPE_NONE} + * {@link ImsCallProfile#SERVICE_TYPE_NORMAL} + * {@link ImsCallProfile#SERVICE_TYPE_EMERGENCY} + * @param callType a call type that is specified in {@link ImsCallProfile} + * {@link ImsCallProfile#CALL_TYPE_VOICE} + * {@link ImsCallProfile#CALL_TYPE_VT} + * {@link ImsCallProfile#CALL_TYPE_VT_TX} + * {@link ImsCallProfile#CALL_TYPE_VT_RX} + * {@link ImsCallProfile#CALL_TYPE_VT_NODIR} + * {@link ImsCallProfile#CALL_TYPE_VS} + * {@link ImsCallProfile#CALL_TYPE_VS_TX} + * {@link ImsCallProfile#CALL_TYPE_VS_RX} + * @return a {@link ImsCallProfile} object + */ public ImsCallProfile createCallProfile(int sessionId, int callSessionType, int callType) { return null; } - @Override + /** + * Creates a {@link ImsCallSession} with the specified call profile. + * Use other methods, if applicable, instead of interacting with + * {@link ImsCallSession} directly. + * + * @param sessionId a session id which is obtained from {@link #startSession} + * @param profile a call profile to make the call + * @param listener An implementation of IImsCallSessionListener. + */ public IImsCallSession createCallSession(int sessionId, ImsCallProfile profile, IImsCallSessionListener listener) { return null; } - @Override + /** + * Retrieves the call session associated with a pending call. + * + * @param sessionId a session id which is obtained from {@link #startSession} + * @param callId a call id to make the call + */ public IImsCallSession getPendingCallSession(int sessionId, String callId) { return null; } - @Override + /** + * @return The Ut interface for the supplementary service configuration. + */ public IImsUt getUtInterface() { return null; } - @Override + /** + * @return The config interface for IMS Configuration + */ public IImsConfig getConfigInterface() { return null; } - @Override + /** + * Signal the MMTelFeature to turn on IMS when it has been turned off using {@link #turnOffIms} + */ public void turnOnIms() { } - @Override + /** + * Signal the MMTelFeature to turn off IMS when it has been turned on using {@link #turnOnIms} + */ public void turnOffIms() { } - @Override + /** + * @return The Emergency call-back mode interface for emergency VoLTE calls that support it. + */ public IImsEcbm getEcbmInterface() { return null; } - @Override + /** + * Sets the current UI TTY mode for the MMTelFeature. + * @param uiTtyMode An integer containing the new UI TTY Mode. + * @param onComplete A {@link Message} to be used when the mode has been set. + */ public void setUiTTYMode(int uiTtyMode, Message onComplete) { } - @Override + /** + * @return MultiEndpoint interface for DEP notifications + */ public IImsMultiEndpoint getMultiEndpointInterface() { return null; } - @Override + /** + * {@inheritDoc} + */ public void onFeatureRemoved() { } diff --git a/android/telephony/ims/feature/RcsFeature.java b/android/telephony/ims/feature/RcsFeature.java index 9cddc1b9..332cca3e 100644 --- a/android/telephony/ims/feature/RcsFeature.java +++ b/android/telephony/ims/feature/RcsFeature.java @@ -18,11 +18,11 @@ package android.telephony.ims.feature; /** * Base implementation of the RcsFeature APIs. Any ImsService wishing to support RCS should extend - * this class and provide implementations of the IRcsFeature methods that they support. + * this class and provide implementations of the RcsFeature methods that they support. * @hide */ -public class RcsFeature extends ImsFeature implements IRcsFeature { +public class RcsFeature extends ImsFeature { public RcsFeature() { super(); diff --git a/android/telephony/mbms/DownloadStateCallback.java b/android/telephony/mbms/DownloadStateCallback.java index 892fbf07..9f60cc36 100644 --- a/android/telephony/mbms/DownloadStateCallback.java +++ b/android/telephony/mbms/DownloadStateCallback.java @@ -38,7 +38,7 @@ public class DownloadStateCallback { * @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ALL_UPDATES, PROGRESS_UPDATES, STATE_UPDATES}) + @IntDef(flag = true, value = {ALL_UPDATES, PROGRESS_UPDATES, STATE_UPDATES}) public @interface FilterFlag {} /** diff --git a/android/telephony/mbms/FileInfo.java b/android/telephony/mbms/FileInfo.java index 0d737b58..e064adb5 100644 --- a/android/telephony/mbms/FileInfo.java +++ b/android/telephony/mbms/FileInfo.java @@ -17,10 +17,13 @@ package android.telephony.mbms; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import java.util.Objects; + /** * Describes a single file that is available over MBMS. */ @@ -47,6 +50,7 @@ public final class FileInfo implements Parcelable { * @hide */ @SystemApi + @TestApi public FileInfo(Uri uri, String mimeType) { this.uri = uri; this.mimeType = mimeType; @@ -82,4 +86,23 @@ public final class FileInfo implements Parcelable { public String getMimeType() { return mimeType; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FileInfo fileInfo = (FileInfo) o; + return Objects.equals(uri, fileInfo.uri) && + Objects.equals(mimeType, fileInfo.mimeType); + } + + @Override + public int hashCode() { + return Objects.hash(uri, mimeType); + } } diff --git a/android/telephony/mbms/FileServiceInfo.java b/android/telephony/mbms/FileServiceInfo.java index d8d7f48a..b30a3af7 100644 --- a/android/telephony/mbms/FileServiceInfo.java +++ b/android/telephony/mbms/FileServiceInfo.java @@ -17,6 +17,7 @@ package android.telephony.mbms; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; @@ -35,6 +36,7 @@ public final class FileServiceInfo extends ServiceInfo implements Parcelable { /** @hide */ @SystemApi + @TestApi public FileServiceInfo(Map<Locale, String> newNames, String newClassName, List<Locale> newLocales, String newServiceId, Date start, Date end, List<FileInfo> newFiles) { diff --git a/android/telephony/mbms/MbmsDownloadReceiver.java b/android/telephony/mbms/MbmsDownloadReceiver.java index 9af1eb9e..9ef188cf 100644 --- a/android/telephony/mbms/MbmsDownloadReceiver.java +++ b/android/telephony/mbms/MbmsDownloadReceiver.java @@ -165,16 +165,16 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { Log.w(LOG_TAG, "Download result did not include a result code. Ignoring."); return false; } + if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) { + Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring."); + return false; + } // We do not need to verify below extras if the result is not success. if (MbmsDownloadSession.RESULT_SUCCESSFUL != intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, MbmsDownloadSession.RESULT_CANCELLED)) { return true; } - if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) { - Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring."); - return false; - } if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring."); return false; @@ -242,10 +242,12 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { int result = intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, MbmsDownloadSession.RESULT_CANCELLED); intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result); + intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request); if (result != MbmsDownloadSession.RESULT_SUCCESSFUL) { Log.i(LOG_TAG, "Download request indicated a failed download. Aborting."); context.sendBroadcast(intentForApp); + setResultCode(RESULT_OK); return; } @@ -273,7 +275,6 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI, stagedFileLocation); intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, completedFileInfo); - intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request); context.sendBroadcast(intentForApp); setResultCode(RESULT_OK); diff --git a/android/telephony/mbms/UriPathPair.java b/android/telephony/mbms/UriPathPair.java index 187e9eed..dd20a692 100644 --- a/android/telephony/mbms/UriPathPair.java +++ b/android/telephony/mbms/UriPathPair.java @@ -17,6 +17,7 @@ package android.telephony.mbms; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.content.ContentResolver; import android.net.Uri; import android.os.Parcel; @@ -29,6 +30,7 @@ import android.telephony.mbms.vendor.VendorUtils; * @hide */ @SystemApi +@TestApi public final class UriPathPair implements Parcelable { private final Uri mFilePathUri; private final Uri mContentUri; diff --git a/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java b/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java index 9ccdd56f..4fee3df8 100644 --- a/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java +++ b/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java @@ -18,6 +18,7 @@ package android.telephony.mbms.vendor; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.content.Intent; import android.os.Binder; import android.os.IBinder; @@ -42,6 +43,7 @@ import java.util.Map; * @hide */ @SystemApi +@TestApi public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { private final Map<IBinder, DownloadStateCallback> mDownloadCallbackBinderMap = new HashMap<>(); private final Map<IBinder, DeathRecipient> mDownloadCallbackDeathRecipients = new HashMap<>(); diff --git a/android/telephony/mbms/vendor/VendorUtils.java b/android/telephony/mbms/vendor/VendorUtils.java index a43f1224..f1cac8cf 100644 --- a/android/telephony/mbms/vendor/VendorUtils.java +++ b/android/telephony/mbms/vendor/VendorUtils.java @@ -17,6 +17,7 @@ package android.telephony.mbms.vendor; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -34,6 +35,7 @@ import java.util.List; * @hide */ @SystemApi +@TestApi public class VendorUtils { /** diff --git a/android/util/FeatureFlagUtils.java b/android/util/FeatureFlagUtils.java index fc1d4873..2a272208 100644 --- a/android/util/FeatureFlagUtils.java +++ b/android/util/FeatureFlagUtils.java @@ -16,6 +16,7 @@ package android.util; +import android.content.Context; import android.os.SystemProperties; import android.text.TextUtils; @@ -37,7 +38,7 @@ public class FeatureFlagUtils { * @param feature the flag name * @return true if the flag is enabled (either by default in system, or override by user) */ - public static boolean isEnabled(String feature) { + public static boolean isEnabled(Context context, String feature) { // Tries to get feature flag from system property. // Step 1: check if feature flag has any override. Flag name: sys.fflag.override.<feature> String value = SystemProperties.get(FFLAG_OVERRIDE_PREFIX + feature); diff --git a/android/util/KeyValueListParser.java b/android/util/KeyValueListParser.java index be531ff3..d50395e2 100644 --- a/android/util/KeyValueListParser.java +++ b/android/util/KeyValueListParser.java @@ -147,4 +147,18 @@ public class KeyValueListParser { } return def; } + + /** + * @return the number of keys. + */ + public int size() { + return mValues.size(); + } + + /** + * @return the key at {@code index}. Use with {@link #size()} to enumerate all key-value pairs. + */ + public String keyAt(int index) { + return mValues.keyAt(index); + } } diff --git a/android/util/Log.java b/android/util/Log.java index 02998653..b94e48b3 100644 --- a/android/util/Log.java +++ b/android/util/Log.java @@ -16,45 +16,12 @@ package android.util; -import android.os.DeadSystemException; - -import com.android.internal.os.RuntimeInit; -import com.android.internal.util.FastPrintWriter; -import com.android.internal.util.LineBreakBufferedWriter; - import java.io.PrintWriter; import java.io.StringWriter; -import java.io.Writer; import java.net.UnknownHostException; /** - * API for sending log output. - * - * <p>Generally, you should use the {@link #v Log.v()}, {@link #d Log.d()}, - * {@link #i Log.i()}, {@link #w Log.w()}, and {@link #e Log.e()} methods to write logs. - * You can then <a href="{@docRoot}studio/debug/am-logcat.html">view the logs in logcat</a>. - * - * <p>The order in terms of verbosity, from least to most is - * ERROR, WARN, INFO, DEBUG, VERBOSE. Verbose should never be compiled - * into an application except during development. Debug logs are compiled - * in but stripped at runtime. Error, warning and info logs are always kept. - * - * <p><b>Tip:</b> A good convention is to declare a <code>TAG</code> constant - * in your class: - * - * <pre>private static final String TAG = "MyActivity";</pre> - * - * and use that in subsequent calls to the log methods. - * </p> - * - * <p><b>Tip:</b> Don't forget that when you make a call like - * <pre>Log.v(TAG, "index=" + i);</pre> - * that when you're building the string to pass into Log.d, the compiler uses a - * StringBuilder and at least three allocations occur: the StringBuilder - * itself, the buffer, and the String object. Realistically, there is also - * another buffer allocation and copy, and even more pressure on the gc. - * That means that if your log message is filtered out, you might be doing - * significant work and incurring significant overhead. + * Mock Log implementation for testing on non android host. */ public final class Log { @@ -88,29 +55,6 @@ public final class Log { */ public static final int ASSERT = 7; - /** - * Exception class used to capture a stack trace in {@link #wtf}. - * @hide - */ - public static class TerribleFailure extends Exception { - TerribleFailure(String msg, Throwable cause) { super(msg, cause); } - } - - /** - * Interface to handle terrible failures from {@link #wtf}. - * - * @hide - */ - public interface TerribleFailureHandler { - void onTerribleFailure(String tag, TerribleFailure what, boolean system); - } - - private static TerribleFailureHandler sWtfHandler = new TerribleFailureHandler() { - public void onTerribleFailure(String tag, TerribleFailure what, boolean system) { - RuntimeInit.wtf(tag, what, system); - } - }; - private Log() { } @@ -121,7 +65,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int v(String tag, String msg) { - return println_native(LOG_ID_MAIN, VERBOSE, tag, msg); + return println(LOG_ID_MAIN, VERBOSE, tag, msg); } /** @@ -132,7 +76,7 @@ public final class Log { * @param tr An exception to log */ public static int v(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr); + return println(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -142,7 +86,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int d(String tag, String msg) { - return println_native(LOG_ID_MAIN, DEBUG, tag, msg); + return println(LOG_ID_MAIN, DEBUG, tag, msg); } /** @@ -153,7 +97,7 @@ public final class Log { * @param tr An exception to log */ public static int d(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, DEBUG, tag, msg, tr); + return println(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -163,7 +107,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int i(String tag, String msg) { - return println_native(LOG_ID_MAIN, INFO, tag, msg); + return println(LOG_ID_MAIN, INFO, tag, msg); } /** @@ -174,7 +118,7 @@ public final class Log { * @param tr An exception to log */ public static int i(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, INFO, tag, msg, tr); + return println(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -184,7 +128,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int w(String tag, String msg) { - return println_native(LOG_ID_MAIN, WARN, tag, msg); + return println(LOG_ID_MAIN, WARN, tag, msg); } /** @@ -195,31 +139,9 @@ public final class Log { * @param tr An exception to log */ public static int w(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, WARN, tag, msg, tr); + return println(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr)); } - /** - * Checks to see whether or not a log for the specified tag is loggable at the specified level. - * - * The default level of any tag is set to INFO. This means that any level above and including - * INFO will be logged. Before you make any calls to a logging method you should check to see - * if your tag should be logged. You can change the default level by setting a system property: - * 'setprop log.tag.<YOUR_LOG_TAG> <LEVEL>' - * Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPPRESS will - * turn off all logging for your tag. You can also create a local.prop file that with the - * following in it: - * 'log.tag.<YOUR_LOG_TAG>=<LEVEL>' - * and place that in /data/local.prop. - * - * @param tag The tag to check. - * @param level The level to check. - * @return Whether or not that this is allowed to be logged. - * @throws IllegalArgumentException is thrown if the tag.length() > 23 - * for Nougat (7.0) releases (API <= 23) and prior, there is no - * tag limit of concern after this API level. - */ - public static native boolean isLoggable(String tag, int level); - /* * Send a {@link #WARN} log message and log the exception. * @param tag Used to identify the source of a log message. It usually identifies @@ -227,7 +149,7 @@ public final class Log { * @param tr An exception to log */ public static int w(String tag, Throwable tr) { - return printlns(LOG_ID_MAIN, WARN, tag, "", tr); + return println(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr)); } /** @@ -237,7 +159,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int e(String tag, String msg) { - return println_native(LOG_ID_MAIN, ERROR, tag, msg); + return println(LOG_ID_MAIN, ERROR, tag, msg); } /** @@ -248,82 +170,7 @@ public final class Log { * @param tr An exception to log */ public static int e(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, ERROR, tag, msg, tr); - } - - /** - * What a Terrible Failure: Report a condition that should never happen. - * The error will always be logged at level ASSERT with the call stack. - * Depending on system configuration, a report may be added to the - * {@link android.os.DropBoxManager} and/or the process may be terminated - * immediately with an error dialog. - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - */ - public static int wtf(String tag, String msg) { - return wtf(LOG_ID_MAIN, tag, msg, null, false, false); - } - - /** - * Like {@link #wtf(String, String)}, but also writes to the log the full - * call stack. - * @hide - */ - public static int wtfStack(String tag, String msg) { - return wtf(LOG_ID_MAIN, tag, msg, null, true, false); - } - - /** - * What a Terrible Failure: Report an exception that should never happen. - * Similar to {@link #wtf(String, String)}, with an exception to log. - * @param tag Used to identify the source of a log message. - * @param tr An exception to log. - */ - public static int wtf(String tag, Throwable tr) { - return wtf(LOG_ID_MAIN, tag, tr.getMessage(), tr, false, false); - } - - /** - * What a Terrible Failure: Report an exception that should never happen. - * Similar to {@link #wtf(String, Throwable)}, with a message as well. - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - * @param tr An exception to log. May be null. - */ - public static int wtf(String tag, String msg, Throwable tr) { - return wtf(LOG_ID_MAIN, tag, msg, tr, false, false); - } - - static int wtf(int logId, String tag, String msg, Throwable tr, boolean localStack, - boolean system) { - TerribleFailure what = new TerribleFailure(msg, tr); - // Only mark this as ERROR, do not use ASSERT since that should be - // reserved for cases where the system is guaranteed to abort. - // The onTerribleFailure call does not always cause a crash. - int bytes = printlns(logId, ERROR, tag, msg, localStack ? what : tr); - sWtfHandler.onTerribleFailure(tag, what, system); - return bytes; - } - - static void wtfQuiet(int logId, String tag, String msg, boolean system) { - TerribleFailure what = new TerribleFailure(msg, null); - sWtfHandler.onTerribleFailure(tag, what, system); - } - - /** - * Sets the terrible failure handler, for testing. - * - * @return the old handler - * - * @hide - */ - public static TerribleFailureHandler setWtfHandler(TerribleFailureHandler handler) { - if (handler == null) { - throw new NullPointerException("handler == null"); - } - TerribleFailureHandler oldHandler = sWtfHandler; - sWtfHandler = handler; - return oldHandler; + return println(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -346,7 +193,7 @@ public final class Log { } StringWriter sw = new StringWriter(); - PrintWriter pw = new FastPrintWriter(sw, false, 256); + PrintWriter pw = new PrintWriter(sw); tr.printStackTrace(pw); pw.flush(); return sw.toString(); @@ -361,7 +208,7 @@ public final class Log { * @return The number of bytes written. */ public static int println(int priority, String tag, String msg) { - return println_native(LOG_ID_MAIN, priority, tag, msg); + return println(LOG_ID_MAIN, priority, tag, msg); } /** @hide */ public static final int LOG_ID_MAIN = 0; @@ -370,115 +217,9 @@ public final class Log { /** @hide */ public static final int LOG_ID_SYSTEM = 3; /** @hide */ public static final int LOG_ID_CRASH = 4; - /** @hide */ public static native int println_native(int bufID, - int priority, String tag, String msg); - - /** - * Return the maximum payload the log daemon accepts without truncation. - * @return LOGGER_ENTRY_MAX_PAYLOAD. - */ - private static native int logger_entry_max_payload_native(); - - /** - * Helper function for long messages. Uses the LineBreakBufferedWriter to break - * up long messages and stacktraces along newlines, but tries to write in large - * chunks. This is to avoid truncation. - * @hide - */ - public static int printlns(int bufID, int priority, String tag, String msg, - Throwable tr) { - ImmediateLogWriter logWriter = new ImmediateLogWriter(bufID, priority, tag); - // Acceptable buffer size. Get the native buffer size, subtract two zero terminators, - // and the length of the tag. - // Note: we implicitly accept possible truncation for Modified-UTF8 differences. It - // is too expensive to compute that ahead of time. - int bufferSize = PreloadHolder.LOGGER_ENTRY_MAX_PAYLOAD // Base. - - 2 // Two terminators. - - (tag != null ? tag.length() : 0) // Tag length. - - 32; // Some slack. - // At least assume you can print *some* characters (tag is not too large). - bufferSize = Math.max(bufferSize, 100); - - LineBreakBufferedWriter lbbw = new LineBreakBufferedWriter(logWriter, bufferSize); - - lbbw.println(msg); - - if (tr != null) { - // This is to reduce the amount of log spew that apps do in the non-error - // condition of the network being unavailable. - Throwable t = tr; - while (t != null) { - if (t instanceof UnknownHostException) { - break; - } - if (t instanceof DeadSystemException) { - lbbw.println("DeadSystemException: The system died; " - + "earlier logs will point to the root cause"); - break; - } - t = t.getCause(); - } - if (t == null) { - tr.printStackTrace(lbbw); - } - } - - lbbw.flush(); - - return logWriter.getWritten(); - } - - /** - * PreloadHelper class. Caches the LOGGER_ENTRY_MAX_PAYLOAD value to avoid - * a JNI call during logging. - */ - static class PreloadHolder { - public final static int LOGGER_ENTRY_MAX_PAYLOAD = - logger_entry_max_payload_native(); - } - - /** - * Helper class to write to the logcat. Different from LogWriter, this writes - * the whole given buffer and does not break along newlines. - */ - private static class ImmediateLogWriter extends Writer { - - private int bufID; - private int priority; - private String tag; - - private int written = 0; - - /** - * Create a writer that immediately writes to the log, using the given - * parameters. - */ - public ImmediateLogWriter(int bufID, int priority, String tag) { - this.bufID = bufID; - this.priority = priority; - this.tag = tag; - } - - public int getWritten() { - return written; - } - - @Override - public void write(char[] cbuf, int off, int len) { - // Note: using String here has a bit of overhead as a Java object is created, - // but using the char[] directly is not easier, as it needs to be translated - // to a C char[] for logging. - written += println_native(bufID, priority, tag, new String(cbuf, off, len)); - } - - @Override - public void flush() { - // Ignored. - } - - @Override - public void close() { - // Ignored. - } + /** @hide */ @SuppressWarnings("unused") + public static int println(int bufID, + int priority, String tag, String msg) { + return 0; } } diff --git a/android/util/StatsManager.java b/android/util/StatsManager.java new file mode 100644 index 00000000..55b33a61 --- /dev/null +++ b/android/util/StatsManager.java @@ -0,0 +1,134 @@ +/* + * Copyright 2017 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 android.util; + +import android.Manifest; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.os.IBinder; +import android.os.IStatsManager; +import android.os.RemoteException; +import android.os.ServiceManager; + +/** + * API for StatsD clients to send configurations and retrieve data. + * + * @hide + */ +@SystemApi +public final class StatsManager { + IStatsManager mService; + private static final String TAG = "StatsManager"; + + /** + * Constructor for StatsManagerClient. + * + * @hide + */ + public StatsManager() { + } + + /** + * Clients can send a configuration and simultaneously registers the name of a broadcast + * receiver that listens for when it should request data. + * + * @param configKey An arbitrary string that allows clients to track the configuration. + * @param config Wire-encoded StatsDConfig proto that specifies metrics (and all + * dependencies eg, conditions and matchers). + * @param pkg The package name to receive the broadcast. + * @param cls The name of the class that receives the broadcast. + * @return true if successful + */ + @RequiresPermission(Manifest.permission.DUMP) + public boolean addConfiguration(String configKey, byte[] config, String pkg, String cls) { + synchronized (this) { + try { + IStatsManager service = getIStatsManagerLocked(); + if (service == null) { + throw new RuntimeException("StatsD service connection lost"); + } + return service.addConfiguration(configKey, config, pkg, cls); + } catch (RemoteException e) { + Slog.d(TAG, "Failed to connect to statsd when getting data"); + return false; + } + } + } + + /** + * Remove a configuration from logging. + * + * @param configKey Configuration key to remove. + * @return true if successful + */ + @RequiresPermission(Manifest.permission.DUMP) + public boolean removeConfiguration(String configKey) { + synchronized (this) { + try { + IStatsManager service = getIStatsManagerLocked(); + if (service == null) { + throw new RuntimeException("StatsD service connection lost"); + } + return service.removeConfiguration(configKey); + } catch (RemoteException e) { + Slog.d(TAG, "Failed to connect to statsd when getting data"); + return false; + } + } + } + + /** + * Clients can request data with a binder call. + * + * @param configKey Configuration key to retrieve data from. + * @return Serialized ConfigMetricsReport proto. Returns null on failure. + */ + @RequiresPermission(Manifest.permission.DUMP) + public byte[] getData(String configKey) { + synchronized (this) { + try { + IStatsManager service = getIStatsManagerLocked(); + if (service == null) { + throw new RuntimeException("StatsD service connection lost"); + } + return service.getData(configKey); + } catch (RemoteException e) { + Slog.d(TAG, "Failed to connecto statsd when getting data"); + return null; + } + } + } + + private class StatsdDeathRecipient implements IBinder.DeathRecipient { + @Override + public void binderDied() { + synchronized (this) { + mService = null; + } + } + } + + private IStatsManager getIStatsManagerLocked() throws RemoteException { + if (mService != null) { + return mService; + } + mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats")); + if (mService != null) { + mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0); + } + return mService; + } +} diff --git a/android/util/TimeUtils.java b/android/util/TimeUtils.java index 2b03ed6c..cc4a0b60 100644 --- a/android/util/TimeUtils.java +++ b/android/util/TimeUtils.java @@ -340,6 +340,14 @@ public class TimeUtils { } /** @hide Just for debugging; not internationalized. */ + public static String formatDuration(long duration) { + synchronized (sFormatSync) { + int len = formatDurationLocked(duration, 0); + return new String(sFormatStr, 0, len); + } + } + + /** @hide Just for debugging; not internationalized. */ public static void formatDuration(long duration, PrintWriter pw) { formatDuration(duration, pw, 0); } diff --git a/android/util/apk/ApkSignatureSchemeV2Verifier.java b/android/util/apk/ApkSignatureSchemeV2Verifier.java index a9ccae11..18081234 100644 --- a/android/util/apk/ApkSignatureSchemeV2Verifier.java +++ b/android/util/apk/ApkSignatureSchemeV2Verifier.java @@ -16,9 +16,6 @@ package android.util.apk; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; import android.util.ArrayMap; import android.util.Pair; @@ -30,7 +27,6 @@ import java.math.BigInteger; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.DirectByteBuffer; import java.security.DigestException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -122,40 +118,6 @@ public class ApkSignatureSchemeV2Verifier { } /** - * APK Signature Scheme v2 block and additional information relevant to verifying the signatures - * contained in the block against the file. - */ - private static class SignatureInfo { - /** Contents of APK Signature Scheme v2 block. */ - private final ByteBuffer signatureBlock; - - /** Position of the APK Signing Block in the file. */ - private final long apkSigningBlockOffset; - - /** Position of the ZIP Central Directory in the file. */ - private final long centralDirOffset; - - /** Position of the ZIP End of Central Directory (EoCD) in the file. */ - private final long eocdOffset; - - /** Contents of ZIP End of Central Directory (EoCD) of the file. */ - private final ByteBuffer eocd; - - private SignatureInfo( - ByteBuffer signatureBlock, - long apkSigningBlockOffset, - long centralDirOffset, - long eocdOffset, - ByteBuffer eocd) { - this.signatureBlock = signatureBlock; - this.apkSigningBlockOffset = apkSigningBlockOffset; - this.centralDirOffset = centralDirOffset; - this.eocdOffset = eocdOffset; - this.eocd = eocd; - } - } - - /** * Returns the APK Signature Scheme v2 block contained in the provided APK file and the * additional information relevant for verifying the block against the file. * @@ -497,6 +459,7 @@ public class ApkSignatureSchemeV2Verifier { // TODO: Compute digests of chunks in parallel when beneficial. This requires some research // into how to parallelize (if at all) based on the capabilities of the hardware on which // this code is running and based on the size of input. + DataDigester digester = new MultipleDigestDataDigester(mds); int dataSourceIndex = 0; for (DataSource input : contents) { long inputOffset = 0; @@ -508,7 +471,7 @@ public class ApkSignatureSchemeV2Verifier { mds[i].update(chunkContentPrefix); } try { - input.feedIntoMessageDigests(mds, inputOffset, chunkSize); + input.feedIntoDataDigester(digester, inputOffset, chunkSize); } catch (IOException e) { throw new DigestException( "Failed to digest chunk #" + chunkIndex + " of section #" @@ -967,155 +930,26 @@ public class ApkSignatureSchemeV2Verifier { } /** - * Source of data to be digested. + * {@link DataDigester} that updates multiple {@link MessageDigest}s whenever data is feeded. */ - private static interface DataSource { - - /** - * Returns the size (in bytes) of the data offered by this source. - */ - long size(); - - /** - * Feeds the specified region of this source's data into the provided digests. Each digest - * instance gets the same data. - * - * @param offset offset of the region inside this data source. - * @param size size (in bytes) of the region. - */ - void feedIntoMessageDigests(MessageDigest[] mds, long offset, int size) throws IOException; - } + private static class MultipleDigestDataDigester implements DataDigester { + private final MessageDigest[] mMds; - /** - * {@link DataSource} which provides data from a file descriptor by memory-mapping the sections - * of the file requested by - * {@link DataSource#feedIntoMessageDigests(MessageDigest[], long, int) feedIntoMessageDigests}. - */ - private static final class MemoryMappedFileDataSource implements DataSource { - private static final long MEMORY_PAGE_SIZE_BYTES = Os.sysconf(OsConstants._SC_PAGESIZE); - - private final FileDescriptor mFd; - private final long mFilePosition; - private final long mSize; - - /** - * Constructs a new {@code MemoryMappedFileDataSource} for the specified region of the file. - * - * @param position start position of the region in the file. - * @param size size (in bytes) of the region. - */ - public MemoryMappedFileDataSource(FileDescriptor fd, long position, long size) { - mFd = fd; - mFilePosition = position; - mSize = size; + MultipleDigestDataDigester(MessageDigest[] mds) { + mMds = mds; } @Override - public long size() { - return mSize; - } - - @Override - public void feedIntoMessageDigests( - MessageDigest[] mds, long offset, int size) throws IOException { - // IMPLEMENTATION NOTE: After a lot of experimentation, the implementation of this - // method was settled on a straightforward mmap with prefaulting. - // - // This method is not using FileChannel.map API because that API does not offset a way - // to "prefault" the resulting memory pages. Without prefaulting, performance is about - // 10% slower on small to medium APKs, but is significantly worse for APKs in 500+ MB - // range. FileChannel.load (which currently uses madvise) doesn't help. Finally, - // invoking madvise (MADV_SEQUENTIAL) after mmap with prefaulting wastes quite a bit of - // time, which is not compensated for by faster reads. - - // We mmap the smallest region of the file containing the requested data. mmap requires - // that the start offset in the file must be a multiple of memory page size. We thus may - // need to mmap from an offset less than the requested offset. - long filePosition = mFilePosition + offset; - long mmapFilePosition = - (filePosition / MEMORY_PAGE_SIZE_BYTES) * MEMORY_PAGE_SIZE_BYTES; - int dataStartOffsetInMmapRegion = (int) (filePosition - mmapFilePosition); - long mmapRegionSize = size + dataStartOffsetInMmapRegion; - long mmapPtr = 0; - try { - mmapPtr = Os.mmap( - 0, // let the OS choose the start address of the region in memory - mmapRegionSize, - OsConstants.PROT_READ, - OsConstants.MAP_SHARED | OsConstants.MAP_POPULATE, // "prefault" all pages - mFd, - mmapFilePosition); - // Feeding a memory region into MessageDigest requires the region to be represented - // as a direct ByteBuffer. - ByteBuffer buf = new DirectByteBuffer( - size, - mmapPtr + dataStartOffsetInMmapRegion, - mFd, // not really needed, but just in case - null, // no need to clean up -- it's taken care of by the finally block - true // read only buffer - ); - for (MessageDigest md : mds) { - buf.position(0); - md.update(buf); - } - } catch (ErrnoException e) { - throw new IOException("Failed to mmap " + mmapRegionSize + " bytes", e); - } finally { - if (mmapPtr != 0) { - try { - Os.munmap(mmapPtr, mmapRegionSize); - } catch (ErrnoException ignored) {} - } + public void consume(ByteBuffer buffer) { + buffer = buffer.slice(); + for (MessageDigest md : mMds) { + buffer.position(0); + md.update(buffer); } } - } - - /** - * {@link DataSource} which provides data from a {@link ByteBuffer}. - */ - private static final class ByteBufferDataSource implements DataSource { - /** - * Underlying buffer. The data is stored between position 0 and the buffer's capacity. - * The buffer's position is 0 and limit is equal to capacity. - */ - private final ByteBuffer mBuf; - - public ByteBufferDataSource(ByteBuffer buf) { - // Defensive copy, to avoid changes to mBuf being visible in buf. - mBuf = buf.slice(); - } @Override - public long size() { - return mBuf.capacity(); - } - - @Override - public void feedIntoMessageDigests( - MessageDigest[] mds, long offset, int size) throws IOException { - // There's no way to tell MessageDigest to read data from ByteBuffer from a position - // other than the buffer's current position. We thus need to change the buffer's - // position to match the requested offset. - // - // In the future, it may be necessary to compute digests of multiple regions in - // parallel. Given that digest computation is a slow operation, we enable multiple - // such requests to be fulfilled by this instance. This is achieved by serially - // creating a new ByteBuffer corresponding to the requested data range and then, - // potentially concurrently, feeding these buffers into MessageDigest instances. - ByteBuffer region; - synchronized (mBuf) { - mBuf.position((int) offset); - mBuf.limit((int) offset + size); - region = mBuf.slice(); - } - - for (MessageDigest md : mds) { - // Need to reset position to 0 at the start of each iteration because - // MessageDigest.update below sets it to the buffer's limit. - region.position(0); - md.update(region); - } - } + public void finish() {} } /** diff --git a/android/util/apk/ApkVerityBuilder.java b/android/util/apk/ApkVerityBuilder.java new file mode 100644 index 00000000..7412ef41 --- /dev/null +++ b/android/util/apk/ApkVerityBuilder.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2017 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 android.util.apk; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; + +/** + * ApkVerityBuilder builds the APK verity tree and the verity header, which will be used by the + * kernel to verity the APK content on access. + * + * <p>Unlike a regular Merkle tree, APK verity tree does not cover the content fully. Due to + * the existing APK format, it has to skip APK Signing Block and also has some special treatment for + * the "Central Directory offset" field of ZIP End of Central Directory. + * + * @hide + */ +abstract class ApkVerityBuilder { + private ApkVerityBuilder() {} + + private static final int CHUNK_SIZE_BYTES = 4096; // Typical Linux block size + private static final int DIGEST_SIZE_BYTES = 32; // SHA-256 size + private static final int FSVERITY_HEADER_SIZE_BYTES = 64; + private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE = 4; + private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; + private static final String JCA_DIGEST_ALGORITHM = "SHA-256"; + private static final byte[] DEFAULT_SALT = new byte[8]; + + static class ApkVerityResult { + public final ByteBuffer fsverityData; + public final byte[] rootHash; + + ApkVerityResult(ByteBuffer fsverityData, byte[] rootHash) { + this.fsverityData = fsverityData; + this.rootHash = rootHash; + } + } + + /** + * Generates fsverity metadata and the Merkle tree into the {@link ByteBuffer} created by the + * {@link ByteBufferFactory}. The bytes layout in the buffer will be used by the kernel and is + * ready to be appended to the target file to set up fsverity. For fsverity to work, this data + * must be placed at the next page boundary, and the caller must add additional padding in that + * case. + * + * @return ApkVerityResult containing the fsverity data and the root hash of the Merkle tree. + */ + static ApkVerityResult generateApkVerity(RandomAccessFile apk, + SignatureInfo signatureInfo, ByteBufferFactory bufferFactory) + throws IOException, SecurityException, NoSuchAlgorithmException, DigestException { + assertSigningBlockAlignedAndHasFullPages(signatureInfo); + + long signingBlockSize = + signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset; + long dataSize = apk.length() - signingBlockSize - ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE; + int[] levelOffset = calculateVerityLevelOffset(dataSize); + ByteBuffer output = bufferFactory.create( + CHUNK_SIZE_BYTES + // fsverity header + extensions + padding + levelOffset[levelOffset.length - 1] + // Merkle tree size + FSVERITY_HEADER_SIZE_BYTES); // second fsverity header (verbatim copy) + + // Start generating the tree from the block boundary as the kernel will expect. + ByteBuffer treeOutput = slice(output, CHUNK_SIZE_BYTES, + output.limit() - FSVERITY_HEADER_SIZE_BYTES); + byte[] rootHash = generateApkVerityTree(apk, signatureInfo, DEFAULT_SALT, levelOffset, + treeOutput); + + ByteBuffer integrityHeader = generateFsverityHeader(apk.length(), DEFAULT_SALT); + output.put(integrityHeader); + output.put(generateFsverityExtensions()); + + integrityHeader.rewind(); + output.put(integrityHeader); + output.rewind(); + return new ApkVerityResult(output, rootHash); + } + + /** + * A helper class to consume and digest data by block continuously, and write into a buffer. + */ + private static class BufferedDigester implements DataDigester { + /** Amount of the data to digest in each cycle before writting out the digest. */ + private static final int BUFFER_SIZE = CHUNK_SIZE_BYTES; + + /** + * Amount of data the {@link MessageDigest} has consumed since the last reset. This must be + * always less than BUFFER_SIZE since {@link MessageDigest} is reset whenever it has + * consumed BUFFER_SIZE of data. + */ + private int mBytesDigestedSinceReset; + + /** The final output {@link ByteBuffer} to write the digest to sequentially. */ + private final ByteBuffer mOutput; + + private final MessageDigest mMd; + private final byte[] mDigestBuffer = new byte[DIGEST_SIZE_BYTES]; + private final byte[] mSalt; + + private BufferedDigester(byte[] salt, ByteBuffer output) throws NoSuchAlgorithmException { + mSalt = salt; + mOutput = output.slice(); + mMd = MessageDigest.getInstance(JCA_DIGEST_ALGORITHM); + mMd.update(mSalt); + mBytesDigestedSinceReset = 0; + } + + /** + * Consumes and digests data up to BUFFER_SIZE (may continue from the previous remaining), + * then writes the final digest to the output buffer. Repeat until all data are consumed. + * If the last consumption is not enough for BUFFER_SIZE, the state will stay and future + * consumption will continuous from there. + */ + @Override + public void consume(ByteBuffer buffer) throws DigestException { + int offset = buffer.position(); + int remaining = buffer.remaining(); + while (remaining > 0) { + int allowance = (int) Math.min(remaining, BUFFER_SIZE - mBytesDigestedSinceReset); + // Optimization: set the buffer limit to avoid allocating a new ByteBuffer object. + buffer.limit(buffer.position() + allowance); + mMd.update(buffer); + offset += allowance; + remaining -= allowance; + mBytesDigestedSinceReset += allowance; + + if (mBytesDigestedSinceReset == BUFFER_SIZE) { + mMd.digest(mDigestBuffer, 0, mDigestBuffer.length); + mOutput.put(mDigestBuffer); + // After digest, MessageDigest resets automatically, so no need to reset again. + mMd.update(mSalt); + mBytesDigestedSinceReset = 0; + } + } + } + + /** Finish the current digestion if any. */ + @Override + public void finish() throws DigestException { + if (mBytesDigestedSinceReset == 0) { + return; + } + mMd.digest(mDigestBuffer, 0, mDigestBuffer.length); + mOutput.put(mDigestBuffer); + } + + private void fillUpLastOutputChunk() { + int extra = (int) (BUFFER_SIZE - mOutput.position() % BUFFER_SIZE); + if (extra == 0) { + return; + } + mOutput.put(ByteBuffer.allocate(extra)); + } + } + + /** + * Digest the source by chunk in the given range. If the last chunk is not a full chunk, + * digest the remaining. + */ + private static void consumeByChunk(DataDigester digester, DataSource source, int chunkSize) + throws IOException, DigestException { + long inputRemaining = source.size(); + long inputOffset = 0; + while (inputRemaining > 0) { + int size = (int) Math.min(inputRemaining, chunkSize); + source.feedIntoDataDigester(digester, inputOffset, size); + inputOffset += size; + inputRemaining -= size; + } + } + + // Rationale: 1) 1 MB should fit in memory space on all devices. 2) It is not too granular + // thus the syscall overhead is not too big. + private static final int MMAP_REGION_SIZE_BYTES = 1024 * 1024; + + private static void generateApkVerityDigestAtLeafLevel(RandomAccessFile apk, + SignatureInfo signatureInfo, byte[] salt, ByteBuffer output) + throws IOException, NoSuchAlgorithmException, DigestException { + BufferedDigester digester = new BufferedDigester(salt, output); + + // 1. Digest from the beginning of the file, until APK Signing Block is reached. + consumeByChunk(digester, + new MemoryMappedFileDataSource(apk.getFD(), 0, signatureInfo.apkSigningBlockOffset), + MMAP_REGION_SIZE_BYTES); + + // 2. Skip APK Signing Block and continue digesting, until the Central Directory offset + // field in EoCD is reached. + long eocdCdOffsetFieldPosition = + signatureInfo.eocdOffset + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET; + consumeByChunk(digester, + new MemoryMappedFileDataSource(apk.getFD(), signatureInfo.centralDirOffset, + eocdCdOffsetFieldPosition - signatureInfo.centralDirOffset), + MMAP_REGION_SIZE_BYTES); + + // 3. Fill up the rest of buffer with 0s. + ByteBuffer alternativeCentralDirOffset = ByteBuffer.allocate( + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE).order(ByteOrder.LITTLE_ENDIAN); + alternativeCentralDirOffset.putInt(Math.toIntExact(signatureInfo.apkSigningBlockOffset)); + alternativeCentralDirOffset.flip(); + digester.consume(alternativeCentralDirOffset); + + // 4. Read from end of the Central Directory offset field in EoCD to the end of the file. + long offsetAfterEocdCdOffsetField = + eocdCdOffsetFieldPosition + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE; + consumeByChunk(digester, + new MemoryMappedFileDataSource(apk.getFD(), offsetAfterEocdCdOffsetField, + apk.length() - offsetAfterEocdCdOffsetField), + MMAP_REGION_SIZE_BYTES); + digester.finish(); + + // 5. Fill up the rest of buffer with 0s. + digester.fillUpLastOutputChunk(); + } + + private static byte[] generateApkVerityTree(RandomAccessFile apk, SignatureInfo signatureInfo, + byte[] salt, int[] levelOffset, ByteBuffer output) + throws IOException, NoSuchAlgorithmException, DigestException { + // 1. Digest the apk to generate the leaf level hashes. + generateApkVerityDigestAtLeafLevel(apk, signatureInfo, salt, slice(output, + levelOffset[levelOffset.length - 2], levelOffset[levelOffset.length - 1])); + + // 2. Digest the lower level hashes bottom up. + for (int level = levelOffset.length - 3; level >= 0; level--) { + ByteBuffer inputBuffer = slice(output, levelOffset[level + 1], levelOffset[level + 2]); + ByteBuffer outputBuffer = slice(output, levelOffset[level], levelOffset[level + 1]); + + DataSource source = new ByteBufferDataSource(inputBuffer); + BufferedDigester digester = new BufferedDigester(salt, outputBuffer); + consumeByChunk(digester, source, CHUNK_SIZE_BYTES); + digester.finish(); + + digester.fillUpLastOutputChunk(); + } + + // 3. Digest the first block (i.e. first level) to generate the root hash. + byte[] rootHash = new byte[DIGEST_SIZE_BYTES]; + BufferedDigester digester = new BufferedDigester(salt, ByteBuffer.wrap(rootHash)); + digester.consume(slice(output, 0, CHUNK_SIZE_BYTES)); + digester.finish(); + return rootHash; + } + + private static ByteBuffer generateFsverityHeader(long fileSize, byte[] salt) { + if (salt.length != 8) { + throw new IllegalArgumentException("salt is not 8 bytes long"); + } + + ByteBuffer buffer = ByteBuffer.allocate(FSVERITY_HEADER_SIZE_BYTES); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + // TODO(b/30972906): insert a reference when there is a public one. + buffer.put("TrueBrew".getBytes()); // magic + buffer.put((byte) 1); // major version + buffer.put((byte) 0); // minor version + buffer.put((byte) 12); // log2(block-size) == log2(4096) + buffer.put((byte) 7); // log2(leaves-per-node) == log2(block-size / digest-size) + // == log2(4096 / 32) + buffer.putShort((short) 1); // meta algorithm, 1: SHA-256 FIXME finalize constant + buffer.putShort((short) 1); // data algorithm, 1: SHA-256 FIXME finalize constant + buffer.putInt(0x1); // flags, 0x1: has extension, FIXME also hide it + buffer.putInt(0); // reserved + buffer.putLong(fileSize); // original i_size + buffer.put(salt); // salt (8 bytes) + + // TODO(b/30972906): Add extension. + + buffer.rewind(); + return buffer; + } + + private static ByteBuffer generateFsverityExtensions() { + return ByteBuffer.allocate(64); // TODO(b/30972906): implement this. + } + + /** + * Returns an array of summed area table of level size in the verity tree. In other words, the + * returned array is offset of each level in the verity tree file format, plus an additional + * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size + * is level + 1. Thus, the returned array is guarantee to have at least 2 elements. + */ + private static int[] calculateVerityLevelOffset(long fileSize) { + ArrayList<Long> levelSize = new ArrayList<>(); + while (true) { + long levelDigestSize = divideRoundup(fileSize, CHUNK_SIZE_BYTES) * DIGEST_SIZE_BYTES; + long chunksSize = CHUNK_SIZE_BYTES * divideRoundup(levelDigestSize, CHUNK_SIZE_BYTES); + levelSize.add(chunksSize); + if (levelDigestSize <= CHUNK_SIZE_BYTES) { + break; + } + fileSize = levelDigestSize; + } + + // Reverse and convert to summed area table. + int[] levelOffset = new int[levelSize.size() + 1]; + levelOffset[0] = 0; + for (int i = 0; i < levelSize.size(); i++) { + // We don't support verity tree if it is larger then Integer.MAX_VALUE. + levelOffset[i + 1] = levelOffset[i] + + Math.toIntExact(levelSize.get(levelSize.size() - i - 1)); + } + return levelOffset; + } + + private static void assertSigningBlockAlignedAndHasFullPages(SignatureInfo signatureInfo) { + if (signatureInfo.apkSigningBlockOffset % CHUNK_SIZE_BYTES != 0) { + throw new IllegalArgumentException( + "APK Signing Block does not start at the page boundary: " + + signatureInfo.apkSigningBlockOffset); + } + + if ((signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset) + % CHUNK_SIZE_BYTES != 0) { + throw new IllegalArgumentException( + "Size of APK Signing Block is not a multiple of 4096: " + + (signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset)); + } + } + + /** Returns a slice of the buffer which shares content with the provided buffer. */ + private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) { + ByteBuffer b = buffer.duplicate(); + b.position(0); // to ensure position <= limit invariant. + b.limit(end); + b.position(begin); + return b.slice(); + } + + /** Divides a number and round up to the closest integer. */ + private static long divideRoundup(long dividend, long divisor) { + return (dividend + divisor - 1) / divisor; + } +} diff --git a/android/util/apk/ByteBufferDataSource.java b/android/util/apk/ByteBufferDataSource.java new file mode 100644 index 00000000..3976568a --- /dev/null +++ b/android/util/apk/ByteBufferDataSource.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 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 android.util.apk; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.DigestException; + +/** + * {@link DataSource} which provides data from a {@link ByteBuffer}. + */ +class ByteBufferDataSource implements DataSource { + /** + * Underlying buffer. The data is stored between position 0 and the buffer's capacity. + * The buffer's position is 0 and limit is equal to capacity. + */ + private final ByteBuffer mBuf; + + ByteBufferDataSource(ByteBuffer buf) { + // Defensive copy, to avoid changes to mBuf being visible in buf, and to ensure position is + // 0 and limit == capacity. + mBuf = buf.slice(); + } + + @Override + public long size() { + return mBuf.capacity(); + } + + @Override + public void feedIntoDataDigester(DataDigester md, long offset, int size) + throws IOException, DigestException { + // There's no way to tell MessageDigest to read data from ByteBuffer from a position + // other than the buffer's current position. We thus need to change the buffer's + // position to match the requested offset. + // + // In the future, it may be necessary to compute digests of multiple regions in + // parallel. Given that digest computation is a slow operation, we enable multiple + // such requests to be fulfilled by this instance. This is achieved by serially + // creating a new ByteBuffer corresponding to the requested data range and then, + // potentially concurrently, feeding these buffers into MessageDigest instances. + ByteBuffer region; + synchronized (mBuf) { + mBuf.position(0); + mBuf.limit((int) offset + size); + mBuf.position((int) offset); + region = mBuf.slice(); + } + + md.consume(region); + } +} diff --git a/android/util/apk/ByteBufferFactory.java b/android/util/apk/ByteBufferFactory.java new file mode 100644 index 00000000..7a998822 --- /dev/null +++ b/android/util/apk/ByteBufferFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 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 android.util.apk; + +import java.nio.ByteBuffer; + +/** + * Provider of {@link ByteBuffer} instances. + * @hide + */ +public interface ByteBufferFactory { + /** Initiates a {@link ByteBuffer} with the given size. */ + ByteBuffer create(int capacity); +} diff --git a/android/util/apk/DataDigester.java b/android/util/apk/DataDigester.java new file mode 100644 index 00000000..278be803 --- /dev/null +++ b/android/util/apk/DataDigester.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 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 android.util.apk; + +import java.nio.ByteBuffer; +import java.security.DigestException; + +interface DataDigester { + /** Consumes the {@link ByteBuffer}. */ + void consume(ByteBuffer buffer) throws DigestException; + + /** Finishes the digestion. Must be called after the last {@link #consume(ByteBuffer)}. */ + void finish() throws DigestException; +} diff --git a/android/util/apk/DataSource.java b/android/util/apk/DataSource.java new file mode 100644 index 00000000..82f3800a --- /dev/null +++ b/android/util/apk/DataSource.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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 android.util.apk; + +import java.io.IOException; +import java.security.DigestException; + +/** Source of data to be digested. */ +interface DataSource { + + /** + * Returns the size (in bytes) of the data offered by this source. + */ + long size(); + + /** + * Feeds the specified region of this source's data into the provided digester. + * + * @param offset offset of the region inside this data source. + * @param size size (in bytes) of the region. + */ + void feedIntoDataDigester(DataDigester md, long offset, int size) + throws IOException, DigestException; +} diff --git a/android/util/apk/MemoryMappedFileDataSource.java b/android/util/apk/MemoryMappedFileDataSource.java new file mode 100644 index 00000000..8d2b1e32 --- /dev/null +++ b/android/util/apk/MemoryMappedFileDataSource.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2017 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 android.util.apk; + +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.DirectByteBuffer; +import java.security.DigestException; + +/** + * {@link DataSource} which provides data from a file descriptor by memory-mapping the sections + * of the file. + */ +class MemoryMappedFileDataSource implements DataSource { + private static final long MEMORY_PAGE_SIZE_BYTES = Os.sysconf(OsConstants._SC_PAGESIZE); + + private final FileDescriptor mFd; + private final long mFilePosition; + private final long mSize; + + /** + * Constructs a new {@code MemoryMappedFileDataSource} for the specified region of the file. + * + * @param position start position of the region in the file. + * @param size size (in bytes) of the region. + */ + MemoryMappedFileDataSource(FileDescriptor fd, long position, long size) { + mFd = fd; + mFilePosition = position; + mSize = size; + } + + @Override + public long size() { + return mSize; + } + + @Override + public void feedIntoDataDigester(DataDigester md, long offset, int size) + throws IOException, DigestException { + // IMPLEMENTATION NOTE: After a lot of experimentation, the implementation of this + // method was settled on a straightforward mmap with prefaulting. + // + // This method is not using FileChannel.map API because that API does not offset a way + // to "prefault" the resulting memory pages. Without prefaulting, performance is about + // 10% slower on small to medium APKs, but is significantly worse for APKs in 500+ MB + // range. FileChannel.load (which currently uses madvise) doesn't help. Finally, + // invoking madvise (MADV_SEQUENTIAL) after mmap with prefaulting wastes quite a bit of + // time, which is not compensated for by faster reads. + + // We mmap the smallest region of the file containing the requested data. mmap requires + // that the start offset in the file must be a multiple of memory page size. We thus may + // need to mmap from an offset less than the requested offset. + long filePosition = mFilePosition + offset; + long mmapFilePosition = + (filePosition / MEMORY_PAGE_SIZE_BYTES) * MEMORY_PAGE_SIZE_BYTES; + int dataStartOffsetInMmapRegion = (int) (filePosition - mmapFilePosition); + long mmapRegionSize = size + dataStartOffsetInMmapRegion; + long mmapPtr = 0; + try { + mmapPtr = Os.mmap( + 0, // let the OS choose the start address of the region in memory + mmapRegionSize, + OsConstants.PROT_READ, + OsConstants.MAP_SHARED | OsConstants.MAP_POPULATE, // "prefault" all pages + mFd, + mmapFilePosition); + ByteBuffer buf = new DirectByteBuffer( + size, + mmapPtr + dataStartOffsetInMmapRegion, + mFd, // not really needed, but just in case + null, // no need to clean up -- it's taken care of by the finally block + true // read only buffer + ); + md.consume(buf); + } catch (ErrnoException e) { + throw new IOException("Failed to mmap " + mmapRegionSize + " bytes", e); + } finally { + if (mmapPtr != 0) { + try { + Os.munmap(mmapPtr, mmapRegionSize); + } catch (ErrnoException ignored) { } + } + } + } +} diff --git a/android/util/apk/SignatureInfo.java b/android/util/apk/SignatureInfo.java new file mode 100644 index 00000000..8e1233af --- /dev/null +++ b/android/util/apk/SignatureInfo.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 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 android.util.apk; + +import java.nio.ByteBuffer; + +/** + * APK Signature Scheme v2 block and additional information relevant to verifying the signatures + * contained in the block against the file. + */ +class SignatureInfo { + /** Contents of APK Signature Scheme v2 block. */ + public final ByteBuffer signatureBlock; + + /** Position of the APK Signing Block in the file. */ + public final long apkSigningBlockOffset; + + /** Position of the ZIP Central Directory in the file. */ + public final long centralDirOffset; + + /** Position of the ZIP End of Central Directory (EoCD) in the file. */ + public final long eocdOffset; + + /** Contents of ZIP End of Central Directory (EoCD) of the file. */ + public final ByteBuffer eocd; + + SignatureInfo(ByteBuffer signatureBlock, long apkSigningBlockOffset, long centralDirOffset, + long eocdOffset, ByteBuffer eocd) { + this.signatureBlock = signatureBlock; + this.apkSigningBlockOffset = apkSigningBlockOffset; + this.centralDirOffset = centralDirOffset; + this.eocdOffset = eocdOffset; + this.eocd = eocd; + } +} diff --git a/android/util/proto/ProtoOutputStream.java b/android/util/proto/ProtoOutputStream.java index 43a97897..a94806a0 100644 --- a/android/util/proto/ProtoOutputStream.java +++ b/android/util/proto/ProtoOutputStream.java @@ -127,42 +127,48 @@ public final class ProtoOutputStream { public static final long FIELD_TYPE_UNKNOWN = 0; + /** + * The types are copied from external/protobuf/src/google/protobuf/descriptor.h directly, + * so no extra mapping needs to be maintained in this case. + */ public static final long FIELD_TYPE_DOUBLE = 1L << FIELD_TYPE_SHIFT; public static final long FIELD_TYPE_FLOAT = 2L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_INT32 = 3L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_INT64 = 4L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_UINT32 = 5L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_UINT64 = 6L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_SINT32 = 7L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_SINT64 = 8L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_FIXED32 = 9L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_FIXED64 = 10L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_SFIXED32 = 11L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_SFIXED64 = 12L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_BOOL = 13L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_STRING = 14L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_BYTES = 15L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_ENUM = 16L << FIELD_TYPE_SHIFT; - public static final long FIELD_TYPE_OBJECT = 17L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_INT64 = 3L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_UINT64 = 4L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_INT32 = 5L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_FIXED64 = 6L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_FIXED32 = 7L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_BOOL = 8L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_STRING = 9L << FIELD_TYPE_SHIFT; +// public static final long FIELD_TYPE_GROUP = 10L << FIELD_TYPE_SHIFT; // Deprecated. + public static final long FIELD_TYPE_MESSAGE = 11L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_BYTES = 12L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_UINT32 = 13L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_ENUM = 14L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_SFIXED32 = 15L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_SFIXED64 = 16L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_SINT32 = 17L << FIELD_TYPE_SHIFT; + public static final long FIELD_TYPE_SINT64 = 18L << FIELD_TYPE_SHIFT; private static final String[] FIELD_TYPE_NAMES = new String[] { "Double", "Float", - "Int32", "Int64", - "UInt32", "UInt64", - "SInt32", - "SInt64", - "Fixed32", + "Int32", "Fixed64", - "SFixed32", - "SFixed64", + "Fixed32", "Bool", "String", + "Group", // This field is deprecated but reserved here for indexing. + "Message", "Bytes", + "UInt32", "Enum", - "Object", + "SFixed32", + "SFixed64", + "SInt32", + "SInt64", }; // @@ -867,21 +873,21 @@ public final class ProtoOutputStream { assertNotCompacted(); final int id = (int)fieldId; - switch ((int)((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) { + switch ((int) ((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) { // bytes - case (int)((FIELD_TYPE_BYTES | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT): + case (int) ((FIELD_TYPE_BYTES | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT): writeBytesImpl(id, val); break; - case (int)((FIELD_TYPE_BYTES | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT): - case (int)((FIELD_TYPE_BYTES | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT): + case (int) ((FIELD_TYPE_BYTES | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT): + case (int) ((FIELD_TYPE_BYTES | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT): writeRepeatedBytesImpl(id, val); break; // Object - case (int)((FIELD_TYPE_OBJECT | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT): + case (int) ((FIELD_TYPE_MESSAGE | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT): writeObjectImpl(id, val); break; - case (int)((FIELD_TYPE_OBJECT | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT): - case (int)((FIELD_TYPE_OBJECT | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT): + case (int) ((FIELD_TYPE_MESSAGE | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT): + case (int) ((FIELD_TYPE_MESSAGE | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT): writeRepeatedObjectImpl(id, val); break; // nothing else allowed @@ -899,7 +905,7 @@ public final class ProtoOutputStream { assertNotCompacted(); final int id = (int)fieldId; - if ((fieldId & FIELD_TYPE_MASK) == FIELD_TYPE_OBJECT) { + if ((fieldId & FIELD_TYPE_MASK) == FIELD_TYPE_MESSAGE) { final long count = fieldId & FIELD_COUNT_MASK; if (count == FIELD_COUNT_SINGLE) { return startObjectImpl(id, false); @@ -2091,7 +2097,7 @@ public final class ProtoOutputStream { @Deprecated public long startObject(long fieldId) { assertNotCompacted(); - final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_OBJECT); + final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_MESSAGE); return startObjectImpl(id, false); } @@ -2119,7 +2125,7 @@ public final class ProtoOutputStream { @Deprecated public long startRepeatedObject(long fieldId) { assertNotCompacted(); - final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_OBJECT); + final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_MESSAGE); return startObjectImpl(id, true); } @@ -2217,7 +2223,7 @@ public final class ProtoOutputStream { @Deprecated public void writeObject(long fieldId, byte[] value) { assertNotCompacted(); - final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_OBJECT); + final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_MESSAGE); writeObjectImpl(id, value); } @@ -2237,7 +2243,7 @@ public final class ProtoOutputStream { @Deprecated public void writeRepeatedObject(long fieldId, byte[] value) { assertNotCompacted(); - final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_OBJECT); + final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_MESSAGE); writeRepeatedObjectImpl(id, value); } @@ -2296,7 +2302,7 @@ public final class ProtoOutputStream { final String typeString = getFieldTypeString(fieldType); if (typeString != null && countString != null) { final StringBuilder sb = new StringBuilder(); - if (expectedType == FIELD_TYPE_OBJECT) { + if (expectedType == FIELD_TYPE_MESSAGE) { sb.append("start"); } else { sb.append("write"); @@ -2306,7 +2312,7 @@ public final class ProtoOutputStream { sb.append(" called for field "); sb.append((int)fieldId); sb.append(" which should be used with "); - if (fieldType == FIELD_TYPE_OBJECT) { + if (fieldType == FIELD_TYPE_MESSAGE) { sb.append("start"); } else { sb.append("write"); @@ -2321,7 +2327,7 @@ public final class ProtoOutputStream { throw new IllegalArgumentException(sb.toString()); } else { final StringBuilder sb = new StringBuilder(); - if (expectedType == FIELD_TYPE_OBJECT) { + if (expectedType == FIELD_TYPE_MESSAGE) { sb.append("start"); } else { sb.append("write"); diff --git a/android/view/Display.java b/android/view/Display.java index e7c3f92d..6a44cdb9 100644 --- a/android/view/Display.java +++ b/android/view/Display.java @@ -294,11 +294,10 @@ public final class Display { /** * Display state: The display is dozing in a suspended low power state; it is still - * on but is optimized for showing static system-provided content while the device - * is non-interactive. This mode may be used to conserve even more power by allowing - * the hardware to stop applying frame buffer updates from the graphics subsystem or - * to take over the display and manage it autonomously to implement low power always-on - * display functionality. + * on but the CPU is not updating it. This may be used in one of two ways: to show + * static system-provided content while the device is non-interactive, or to allow + * a "Sidekick" compute resource to update the display. For this reason, the + * CPU must not control the display in this mode. * * @see #getState * @see android.os.PowerManager#isInteractive @@ -313,6 +312,18 @@ public final class Display { */ public static final int STATE_VR = 5; + /** + * Display state: The display is in a suspended full power state; it is still + * on but the CPU is not updating it. This may be used in one of two ways: to show + * static system-provided content while the device is non-interactive, or to allow + * a "Sidekick" compute resource to update the display. For this reason, the + * CPU must not control the display in this mode. + * + * @see #getState + * @see android.os.PowerManager#isInteractive + */ + public static final int STATE_ON_SUSPEND = 6; + /* The color mode constants defined below must be kept in sync with the ones in * system/core/include/system/graphics-base.h */ @@ -994,7 +1005,7 @@ public final class Display { * Gets the state of the display, such as whether it is on or off. * * @return The state of the display: one of {@link #STATE_OFF}, {@link #STATE_ON}, - * {@link #STATE_DOZE}, {@link #STATE_DOZE_SUSPEND}, or + * {@link #STATE_DOZE}, {@link #STATE_DOZE_SUSPEND}, {@link #STATE_ON_SUSPEND}, or * {@link #STATE_UNKNOWN}. */ public int getState() { @@ -1113,6 +1124,8 @@ public final class Display { return "DOZE_SUSPEND"; case STATE_VR: return "VR"; + case STATE_ON_SUSPEND: + return "ON_SUSPEND"; default: return Integer.toString(state); } @@ -1120,11 +1133,11 @@ public final class Display { /** * Returns true if display updates may be suspended while in the specified - * display power state. + * display power state. In SUSPEND states, updates are absolutely forbidden. * @hide */ public static boolean isSuspendedState(int state) { - return state == STATE_OFF || state == STATE_DOZE_SUSPEND; + return state == STATE_OFF || state == STATE_DOZE_SUSPEND || state == STATE_ON_SUSPEND; } /** diff --git a/android/view/DisplayFrames.java b/android/view/DisplayFrames.java new file mode 100644 index 00000000..e6861d83 --- /dev/null +++ b/android/view/DisplayFrames.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2017 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 android.view; + +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; +import static com.android.server.wm.proto.DisplayFramesProto.STABLE_BOUNDS; + +import android.graphics.Rect; +import android.util.proto.ProtoOutputStream; + +import java.io.PrintWriter; + +/** + * Container class for all the display frames that affect how we do window layout on a display. + * @hide + */ +public class DisplayFrames { + public final int mDisplayId; + + /** + * The current size of the screen; really; extends into the overscan area of the screen and + * doesn't account for any system elements like the status bar. + */ + public final Rect mOverscan = new Rect(); + + /** + * The current visible size of the screen; really; (ir)regardless of whether the status bar can + * be hidden but not extending into the overscan area. + */ + public final Rect mUnrestricted = new Rect(); + + /** Like mOverscan*, but allowed to move into the overscan region where appropriate. */ + public final Rect mRestrictedOverscan = new Rect(); + + /** + * The current size of the screen; these may be different than (0,0)-(dw,dh) if the status bar + * can't be hidden; in that case it effectively carves out that area of the display from all + * other windows. + */ + public final Rect mRestricted = new Rect(); + + /** + * During layout, the current screen borders accounting for any currently visible system UI + * elements. + */ + public final Rect mSystem = new Rect(); + + /** For applications requesting stable content insets, these are them. */ + public final Rect mStable = new Rect(); + + /** + * For applications requesting stable content insets but have also set the fullscreen window + * flag, these are the stable dimensions without the status bar. + */ + public final Rect mStableFullscreen = new Rect(); + + /** + * During layout, the current screen borders with all outer decoration (status bar, input method + * dock) accounted for. + */ + public final Rect mCurrent = new Rect(); + + /** + * During layout, the frame in which content should be displayed to the user, accounting for all + * screen decoration except for any space they deem as available for other content. This is + * usually the same as mCurrent*, but may be larger if the screen decor has supplied content + * insets. + */ + public final Rect mContent = new Rect(); + + /** + * During layout, the frame in which voice content should be displayed to the user, accounting + * for all screen decoration except for any space they deem as available for other content. + */ + public final Rect mVoiceContent = new Rect(); + + /** During layout, the current screen borders along which input method windows are placed. */ + public final Rect mDock = new Rect(); + + private final Rect mDisplayInfoOverscan = new Rect(); + private final Rect mRotatedDisplayInfoOverscan = new Rect(); + public int mDisplayWidth; + public int mDisplayHeight; + + public int mRotation; + + public DisplayFrames(int displayId, DisplayInfo info) { + mDisplayId = displayId; + onDisplayInfoUpdated(info); + } + + public void onDisplayInfoUpdated(DisplayInfo info) { + mDisplayWidth = info.logicalWidth; + mDisplayHeight = info.logicalHeight; + mRotation = info.rotation; + mDisplayInfoOverscan.set( + info.overscanLeft, info.overscanTop, info.overscanRight, info.overscanBottom); + } + + public void onBeginLayout() { + switch (mRotation) { + case ROTATION_90: + mRotatedDisplayInfoOverscan.left = mDisplayInfoOverscan.top; + mRotatedDisplayInfoOverscan.top = mDisplayInfoOverscan.right; + mRotatedDisplayInfoOverscan.right = mDisplayInfoOverscan.bottom; + mRotatedDisplayInfoOverscan.bottom = mDisplayInfoOverscan.left; + break; + case ROTATION_180: + mRotatedDisplayInfoOverscan.left = mDisplayInfoOverscan.right; + mRotatedDisplayInfoOverscan.top = mDisplayInfoOverscan.bottom; + mRotatedDisplayInfoOverscan.right = mDisplayInfoOverscan.left; + mRotatedDisplayInfoOverscan.bottom = mDisplayInfoOverscan.top; + break; + case ROTATION_270: + mRotatedDisplayInfoOverscan.left = mDisplayInfoOverscan.bottom; + mRotatedDisplayInfoOverscan.top = mDisplayInfoOverscan.left; + mRotatedDisplayInfoOverscan.right = mDisplayInfoOverscan.top; + mRotatedDisplayInfoOverscan.bottom = mDisplayInfoOverscan.right; + break; + default: + mRotatedDisplayInfoOverscan.set(mDisplayInfoOverscan); + break; + } + + mRestrictedOverscan.set(0, 0, mDisplayWidth, mDisplayHeight); + mOverscan.set(mRestrictedOverscan); + mSystem.set(mRestrictedOverscan); + mUnrestricted.set(mRotatedDisplayInfoOverscan); + mUnrestricted.right = mDisplayWidth - mUnrestricted.right; + mUnrestricted.bottom = mDisplayHeight - mUnrestricted.bottom; + mRestricted.set(mUnrestricted); + mDock.set(mUnrestricted); + mContent.set(mUnrestricted); + mVoiceContent.set(mUnrestricted); + mStable.set(mUnrestricted); + mStableFullscreen.set(mUnrestricted); + mCurrent.set(mUnrestricted); + + } + + public int getInputMethodWindowVisibleHeight() { + return mDock.bottom - mCurrent.bottom; + } + + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + mStable.writeToProto(proto, STABLE_BOUNDS); + proto.end(token); + } + + public void dump(String prefix, PrintWriter pw) { + pw.println(prefix + "DisplayFrames w=" + mDisplayWidth + " h=" + mDisplayHeight + + " r=" + mRotation); + final String myPrefix = prefix + " "; + dumpFrame(mStable, "mStable", myPrefix, pw); + dumpFrame(mStableFullscreen, "mStableFullscreen", myPrefix, pw); + dumpFrame(mDock, "mDock", myPrefix, pw); + dumpFrame(mCurrent, "mCurrent", myPrefix, pw); + dumpFrame(mSystem, "mSystem", myPrefix, pw); + dumpFrame(mContent, "mContent", myPrefix, pw); + dumpFrame(mVoiceContent, "mVoiceContent", myPrefix, pw); + dumpFrame(mOverscan, "mOverscan", myPrefix, pw); + dumpFrame(mRestrictedOverscan, "mRestrictedOverscan", myPrefix, pw); + dumpFrame(mRestricted, "mRestricted", myPrefix, pw); + dumpFrame(mUnrestricted, "mUnrestricted", myPrefix, pw); + dumpFrame(mDisplayInfoOverscan, "mDisplayInfoOverscan", myPrefix, pw); + dumpFrame(mRotatedDisplayInfoOverscan, "mRotatedDisplayInfoOverscan", myPrefix, pw); + } + + private void dumpFrame(Rect frame, String name, String prefix, PrintWriter pw) { + pw.print(prefix + name + "="); frame.printShortString(pw); pw.println(); + } +} diff --git a/android/view/FocusFinder.java b/android/view/FocusFinder.java index 74555de5..713cfb48 100644 --- a/android/view/FocusFinder.java +++ b/android/view/FocusFinder.java @@ -530,7 +530,7 @@ public class FocusFinder { * axis distances. Warning: this fudge factor is finely tuned, be sure to * run all focus tests if you dare tweak it. */ - int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) { + long getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance) { return 13 * majorAxisDistance * majorAxisDistance + minorAxisDistance * minorAxisDistance; } diff --git a/android/view/IWindowManagerImpl.java b/android/view/IWindowManagerImpl.java index b34dfbfe..6c006cae 100644 --- a/android/view/IWindowManagerImpl.java +++ b/android/view/IWindowManagerImpl.java @@ -16,6 +16,7 @@ package android.view; +import android.app.IAssistDataReceiver; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.GraphicBuffer; @@ -29,7 +30,6 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.DisplayMetrics; -import com.android.internal.app.IAssistScreenshotReceiver; import com.android.internal.os.IResultReceiver; import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.policy.IShortcutService; @@ -261,7 +261,7 @@ public class IWindowManagerImpl implements IWindowManager { } @Override - public boolean requestAssistScreenshot(IAssistScreenshotReceiver receiver) + public boolean requestAssistScreenshot(IAssistDataReceiver receiver) throws RemoteException { // TODO Auto-generated method stub return false; @@ -507,7 +507,7 @@ public class IWindowManagerImpl implements IWindowManager { throws RemoteException {} @Override - public void createInputConsumer(String name, InputChannel inputChannel) + public void createInputConsumer(IBinder token, String name, InputChannel inputChannel) throws RemoteException {} @Override diff --git a/android/view/NotificationHeaderView.java b/android/view/NotificationHeaderView.java index 58045602..ab0b3eec 100644 --- a/android/view/NotificationHeaderView.java +++ b/android/view/NotificationHeaderView.java @@ -20,6 +20,7 @@ import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Rect; @@ -43,6 +44,7 @@ public class NotificationHeaderView extends ViewGroup { public static final int NO_COLOR = Notification.COLOR_INVALID; private final int mChildMinWidth; private final int mContentEndMargin; + private final int mGravity; private View mAppName; private View mHeaderText; private OnClickListener mExpandClickListener; @@ -50,7 +52,6 @@ public class NotificationHeaderView extends ViewGroup { private ImageView mExpandButton; private CachingIconView mIcon; private View mProfileBadge; - private View mInfo; private int mIconColor; private int mOriginalNotificationColor; private boolean mExpanded; @@ -61,6 +62,7 @@ public class NotificationHeaderView extends ViewGroup { private boolean mEntireHeaderClickable; private boolean mExpandOnlyOnButton; private boolean mAcceptAllTouches; + private int mTotalWidth; ViewOutlineProvider mProvider = new ViewOutlineProvider() { @Override @@ -92,6 +94,11 @@ public class NotificationHeaderView extends ViewGroup { mHeaderBackgroundHeight = res.getDimensionPixelSize( R.dimen.notification_header_background_height); mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand); + + int[] attrIds = { android.R.attr.gravity }; + TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes); + mGravity = ta.getInt(0, 0); + ta.recycle(); } @Override @@ -146,6 +153,7 @@ public class NotificationHeaderView extends ViewGroup { mHeaderText.measure(childWidthSpec, wrapContentHeightSpec); } } + mTotalWidth = Math.min(totalWidth, givenWidth); setMeasuredDimension(givenWidth, givenHeight); } @@ -153,6 +161,10 @@ public class NotificationHeaderView extends ViewGroup { protected void onLayout(boolean changed, int l, int t, int r, int b) { int left = getPaddingStart(); int end = getMeasuredWidth(); + final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0; + if (centerAligned) { + left += getMeasuredWidth() / 2 - mTotalWidth / 2; + } int childCount = getChildCount(); int ownHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); for (int i = 0; i < childCount; i++) { diff --git a/android/view/RenderNode.java b/android/view/RenderNode.java index ea6e63c3..50701518 100644 --- a/android/view/RenderNode.java +++ b/android/view/RenderNode.java @@ -353,6 +353,11 @@ public class RenderNode { return nHasShadow(mNativeRenderNode); } + /** setShadowColor */ + public boolean setShadowColor(int color) { + return nSetShadowColor(mNativeRenderNode, color); + } + /** * Enables or disables clipping to the outline. * @@ -910,6 +915,8 @@ public class RenderNode { @CriticalNative private static native boolean nHasShadow(long renderNode); @CriticalNative + private static native boolean nSetShadowColor(long renderNode, int color); + @CriticalNative private static native boolean nSetClipToOutline(long renderNode, boolean clipToOutline); @CriticalNative private static native boolean nSetRevealClip(long renderNode, diff --git a/android/view/RenderNodeAnimator.java b/android/view/RenderNodeAnimator.java index 95150409..c4a71601 100644 --- a/android/view/RenderNodeAnimator.java +++ b/android/view/RenderNodeAnimator.java @@ -19,7 +19,6 @@ package android.view; import android.animation.Animator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; -import android.graphics.Canvas; import android.graphics.CanvasProperty; import android.graphics.Paint; import android.util.SparseIntArray; @@ -281,12 +280,9 @@ public class RenderNodeAnimator extends Animator { setTarget(mViewTarget.mRenderNode); } - public void setTarget(Canvas canvas) { - if (!(canvas instanceof DisplayListCanvas)) { - throw new IllegalArgumentException("Not a GLES20RecordingCanvas"); - } - final DisplayListCanvas recordingCanvas = (DisplayListCanvas) canvas; - setTarget(recordingCanvas.mNode); + /** Sets the animation target to the owning view of the DisplayListCanvas */ + public void setTarget(DisplayListCanvas canvas) { + setTarget(canvas.mNode); } private void setTarget(RenderNode node) { diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java index 6f8315ae..5641009c 100644 --- a/android/view/SurfaceControl.java +++ b/android/view/SurfaceControl.java @@ -295,6 +295,12 @@ public class SurfaceControl { public static final int POWER_MODE_DOZE_SUSPEND = 3; /** + * Display power mode on: used while putting the screen into a suspended + * full power mode. Use only with {@link SurfaceControl#setDisplayPowerMode}. + */ + public static final int POWER_MODE_ON_SUSPEND = 4; + + /** * A value for windowType used to indicate that the window should be omitted from screenshots * and display mirroring. A temporary workaround until we express such things with * the hierarchy. @@ -1206,56 +1212,65 @@ public class SurfaceControl { } public Transaction show(SurfaceControl sc) { + sc.checkNotReleased(); nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SURFACE_HIDDEN); return this; } public Transaction hide(SurfaceControl sc) { + sc.checkNotReleased(); nativeSetFlags(mNativeObject, sc.mNativeObject, SURFACE_HIDDEN, SURFACE_HIDDEN); return this; } public Transaction setPosition(SurfaceControl sc, float x, float y) { + sc.checkNotReleased(); nativeSetPosition(mNativeObject, sc.mNativeObject, x, y); return this; } public Transaction setSize(SurfaceControl sc, int w, int h) { - nativeSetSize(mNativeObject, sc.mNativeObject, - w, h); + sc.checkNotReleased(); + nativeSetSize(mNativeObject, sc.mNativeObject, w, h); return this; } public Transaction setLayer(SurfaceControl sc, int z) { + sc.checkNotReleased(); nativeSetLayer(mNativeObject, sc.mNativeObject, z); return this; } public Transaction setRelativeLayer(SurfaceControl sc, SurfaceControl relativeTo, int z) { + sc.checkNotReleased(); nativeSetRelativeLayer(mNativeObject, sc.mNativeObject, relativeTo.getHandle(), z); return this; } public Transaction setTransparentRegionHint(SurfaceControl sc, Region transparentRegion) { + sc.checkNotReleased(); nativeSetTransparentRegionHint(mNativeObject, sc.mNativeObject, transparentRegion); return this; } public Transaction setAlpha(SurfaceControl sc, float alpha) { + sc.checkNotReleased(); nativeSetAlpha(mNativeObject, sc.mNativeObject, alpha); return this; } public Transaction setMatrix(SurfaceControl sc, float dsdx, float dtdx, float dtdy, float dsdy) { + sc.checkNotReleased(); nativeSetMatrix(mNativeObject, sc.mNativeObject, dsdx, dtdx, dtdy, dsdy); return this; } public Transaction setWindowCrop(SurfaceControl sc, Rect crop) { + sc.checkNotReleased(); if (crop != null) { nativeSetWindowCrop(mNativeObject, sc.mNativeObject, crop.left, crop.top, crop.right, crop.bottom); @@ -1267,6 +1282,7 @@ public class SurfaceControl { } public Transaction setFinalCrop(SurfaceControl sc, Rect crop) { + sc.checkNotReleased(); if (crop != null) { nativeSetFinalCrop(mNativeObject, sc.mNativeObject, crop.left, crop.top, crop.right, crop.bottom); @@ -1278,40 +1294,48 @@ public class SurfaceControl { } public Transaction setLayerStack(SurfaceControl sc, int layerStack) { + sc.checkNotReleased(); nativeSetLayerStack(mNativeObject, sc.mNativeObject, layerStack); return this; } - public Transaction deferTransactionUntil(SurfaceControl sc, IBinder handle, long frameNumber) { + public Transaction deferTransactionUntil(SurfaceControl sc, IBinder handle, + long frameNumber) { + sc.checkNotReleased(); nativeDeferTransactionUntil(mNativeObject, sc.mNativeObject, handle, frameNumber); return this; } public Transaction deferTransactionUntilSurface(SurfaceControl sc, Surface barrierSurface, long frameNumber) { + sc.checkNotReleased(); nativeDeferTransactionUntilSurface(mNativeObject, sc.mNativeObject, barrierSurface.mNativeObject, frameNumber); return this; } public Transaction reparentChildren(SurfaceControl sc, IBinder newParentHandle) { + sc.checkNotReleased(); nativeReparentChildren(mNativeObject, sc.mNativeObject, newParentHandle); return this; } /** Re-parents a specific child layer to a new parent */ public Transaction reparent(SurfaceControl sc, IBinder newParentHandle) { + sc.checkNotReleased(); nativeReparent(mNativeObject, sc.mNativeObject, newParentHandle); return this; } public Transaction detachChildren(SurfaceControl sc) { + sc.checkNotReleased(); nativeSeverChildren(mNativeObject, sc.mNativeObject); return this; } public Transaction setOverrideScalingMode(SurfaceControl sc, int overrideScalingMode) { + sc.checkNotReleased(); nativeSetOverrideScalingMode(mNativeObject, sc.mNativeObject, overrideScalingMode); return this; @@ -1322,6 +1346,7 @@ public class SurfaceControl { * @param color A float array with three values to represent r, g, b in range [0..1] */ public Transaction setColor(SurfaceControl sc, @Size(3) float[] color) { + sc.checkNotReleased(); nativeSetColor(mNativeObject, sc.mNativeObject, color); return this; } @@ -1334,6 +1359,7 @@ public class SurfaceControl { * (at which point the geometry influencing aspects of this transaction will then occur) */ public Transaction setGeometryAppliesWithResize(SurfaceControl sc) { + sc.checkNotReleased(); nativeSetGeometryAppliesWithResize(mNativeObject, sc.mNativeObject); return this; } @@ -1343,6 +1369,7 @@ public class SurfaceControl { * Surface with the {@link #SECURE} flag. */ Transaction setSecure(SurfaceControl sc, boolean isSecure) { + sc.checkNotReleased(); if (isSecure) { nativeSetFlags(mNativeObject, sc.mNativeObject, SECURE, SECURE); } else { @@ -1356,6 +1383,7 @@ public class SurfaceControl { * Surface with the {@link #OPAQUE} flag. */ public Transaction setOpaque(SurfaceControl sc, boolean isOpaque) { + sc.checkNotReleased(); if (isOpaque) { nativeSetFlags(mNativeObject, sc.mNativeObject, SURFACE_OPAQUE, SURFACE_OPAQUE); } else { diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java index 2166f6e4..7c76bab2 100644 --- a/android/view/ThreadedRenderer.java +++ b/android/view/ThreadedRenderer.java @@ -70,6 +70,7 @@ public final class ThreadedRenderer { * Name of the file that holds the shaders cache. */ private static final String CACHE_PATH_SHADERS = "com.android.opengl.shaders_cache"; + private static final String CACHE_PATH_SKIASHADERS = "com.android.skia.shaders_cache"; /** * System property used to enable or disable threaded rendering profiling. @@ -272,7 +273,9 @@ public final class ThreadedRenderer { * @hide */ public static void setupDiskCache(File cacheDir) { - ThreadedRenderer.setupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath()); + ThreadedRenderer.setupShadersDiskCache( + new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath(), + new File(cacheDir, CACHE_PATH_SKIASHADERS).getAbsolutePath()); } /** @@ -1007,7 +1010,7 @@ public final class ThreadedRenderer { /** Not actually public - internal use only. This doc to make lint happy */ public static native void disableVsync(); - static native void setupShadersDiskCache(String cacheFile); + static native void setupShadersDiskCache(String cacheFile, String skiaCacheFile); private static native void nRotateProcessStatsBuffer(); private static native void nSetProcessStatsBuffer(int fd); diff --git a/android/view/View.java b/android/view/View.java index c043dcac..be09fe86 100644 --- a/android/view/View.java +++ b/android/view/View.java @@ -14218,6 +14218,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setScaleX(float scaleX) { if (scaleX != getScaleX()) { + requireIsFinite(scaleX, "scaleX"); invalidateViewProperty(true, false); mRenderNode.setScaleX(scaleX); invalidateViewProperty(false, true); @@ -14254,6 +14255,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setScaleY(float scaleY) { if (scaleY != getScaleY()) { + requireIsFinite(scaleY, "scaleY"); invalidateViewProperty(true, false); mRenderNode.setScaleY(scaleY); invalidateViewProperty(false, true); @@ -14803,6 +14805,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + private static void requireIsFinite(float transform, String propertyName) { + if (Float.isNaN(transform)) { + throw new IllegalArgumentException("Cannot set '" + propertyName + "' to Float.NaN"); + } + if (Float.isInfinite(transform)) { + throw new IllegalArgumentException("Cannot set '" + propertyName + "' to infinity"); + } + } + /** * The visual x position of this view, in pixels. This is equivalent to the * {@link #setTranslationX(float) translationX} property plus the current @@ -14889,6 +14900,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setElevation(float elevation) { if (elevation != getElevation()) { + requireIsFinite(elevation, "elevation"); invalidateViewProperty(true, false); mRenderNode.setElevation(elevation); invalidateViewProperty(false, true); @@ -14981,6 +14993,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setTranslationZ(float translationZ) { if (translationZ != getTranslationZ()) { + requireIsFinite(translationZ, "translationZ"); invalidateViewProperty(true, false); mRenderNode.setTranslationZ(translationZ); invalidateViewProperty(false, true); @@ -15169,6 +15182,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return mRenderNode.hasShadow(); } + /** + * @hide + */ + public void setShadowColor(@ColorInt int color) { + if (mRenderNode.setShadowColor(color)) { + invalidateViewProperty(true, true); + } + } + /** @hide */ public void setRevealClip(boolean shouldClip, float x, float y, float radius) { diff --git a/android/view/ViewConfiguration.java b/android/view/ViewConfiguration.java index 45008627..c44c8dda 100644 --- a/android/view/ViewConfiguration.java +++ b/android/view/ViewConfiguration.java @@ -92,7 +92,7 @@ public class ViewConfiguration { * Defines the duration in milliseconds a user needs to hold down the * appropriate button to enable the accessibility shortcut once it's configured. */ - private static final int A11Y_SHORTCUT_KEY_TIMEOUT_AFTER_CONFIRMATION = 1500; + private static final int A11Y_SHORTCUT_KEY_TIMEOUT_AFTER_CONFIRMATION = 1000; /** * Defines the duration in milliseconds we will wait to see if a touch event diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java index 37829f0b..e30496fb 100644 --- a/android/view/ViewRootImpl.java +++ b/android/view/ViewRootImpl.java @@ -513,7 +513,7 @@ public final class ViewRootImpl implements ViewParent, mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); if (!sCompatibilityDone) { - sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P; + sAlwaysAssignFocus = true; sCompatibilityDone = true; } diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java index c29a1daf..eb5fc92e 100644 --- a/android/view/WindowManager.java +++ b/android/view/WindowManager.java @@ -1422,7 +1422,7 @@ public interface WindowManager extends ViewManager { * this window is visible. * @hide */ - @RequiresPermission(android.Manifest.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS) + @RequiresPermission(permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS) public static final int PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS = 0x00080000; /** @@ -1443,6 +1443,15 @@ public interface WindowManager extends ViewManager { public static final int PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN = 0x00200000; /** + * Flag to indicate that this window should be considered a screen decoration similar to the + * nav bar and status bar. This will cause this window to affect the window insets reported + * to other windows when it is visible. + * @hide + */ + @RequiresPermission(permission.STATUS_BAR_SERVICE) + public static final int PRIVATE_FLAG_IS_SCREEN_DECOR = 0x00400000; + + /** * Control flags that are private to the platform. * @hide */ @@ -1526,7 +1535,11 @@ public interface WindowManager extends ViewManager { @ViewDebug.FlagToString( mask = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, equals = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, - name = "ACQUIRES_SLEEP_TOKEN") + name = "ACQUIRES_SLEEP_TOKEN"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_IS_SCREEN_DECOR, + equals = PRIVATE_FLAG_IS_SCREEN_DECOR, + name = "IS_SCREEN_DECOR") }) @TestApi public int privateFlags; diff --git a/android/view/WindowManagerGlobal.java b/android/view/WindowManagerGlobal.java index c7e8dee3..cca66d6b 100644 --- a/android/view/WindowManagerGlobal.java +++ b/android/view/WindowManagerGlobal.java @@ -605,9 +605,10 @@ public final class WindowManagerGlobal { public void setStoppedState(IBinder token, boolean stopped) { synchronized (mLock) { int count = mViews.size(); - for (int i = 0; i < count; i++) { + for (int i = count - 1; i >= 0; i--) { if (token == null || mParams.get(i).token == token) { ViewRootImpl root = mRoots.get(i); + // Client might remove the view by "stopped" event. root.setWindowStopped(stopped); } } diff --git a/android/view/WindowManagerInternal.java b/android/view/WindowManagerInternal.java index 69cc1002..cd1b1908 100644 --- a/android/view/WindowManagerInternal.java +++ b/android/view/WindowManagerInternal.java @@ -18,6 +18,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.ClipData; import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManagerInternal; @@ -140,6 +141,30 @@ public abstract class WindowManagerInternal { } /** + * An interface to customize drag and drop behaviors. + */ + public interface IDragDropCallback { + /** + * Called when drag operation is started. + */ + default boolean performDrag(IWindow window, IBinder dragToken, + int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY, + ClipData data) { + return true; + } + + /** + * Called when drop result is reported. + */ + default void reportDropResult(IWindow window, boolean consumed) {} + + /** + * Called when drag operation is cancelled. + */ + default void cancelDragAndDrop(IBinder dragToken) {} + } + + /** * Request that the window manager call * {@link DisplayManagerInternal#performTraversalInTransactionFromWindowManager} * within a surface transaction at a later time. @@ -225,9 +250,6 @@ public abstract class WindowManagerInternal { */ public abstract boolean isKeyguardLocked(); - /** @return {@code true} if the keyguard is going away. */ - public abstract boolean isKeyguardGoingAway(); - /** * @return Whether the keyguard is showing and not occluded. */ @@ -354,4 +376,9 @@ public abstract class WindowManagerInternal { * {@param vr2dDisplayId}. */ public abstract void setVr2dDisplayId(int vr2dDisplayId); + + /** + * Sets callback to DragDropController. + */ + public abstract void registerDragDropControllerCallback(IDragDropCallback callback); } diff --git a/android/view/WindowManagerPolicy.java b/android/view/WindowManagerPolicy.java index 137e551d..534335bf 100644 --- a/android/view/WindowManagerPolicy.java +++ b/android/view/WindowManagerPolicy.java @@ -66,7 +66,6 @@ import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.SystemApi; -import android.app.ActivityManager.StackId; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.CompatibilityInfo; @@ -722,12 +721,6 @@ public interface WindowManagerPolicy { public void setInitialDisplaySize(Display display, int width, int height, int density); /** - * Called by window manager to set the overscan region that should be used for the - * given display. - */ - public void setDisplayOverscan(Display display, int left, int top, int right, int bottom); - - /** * Check permissions when adding a window. * * @param attrs The window's LayoutParams. @@ -758,7 +751,8 @@ public interface WindowManagerPolicy { * @param attrs The window layout parameters to be modified. These values * are modified in-place. */ - public void adjustWindowParamsLw(WindowManager.LayoutParams attrs); + public void adjustWindowParamsLw(WindowState win, WindowManager.LayoutParams attrs, + boolean hasStatusBarServicePermission); /** * After the window manager has computed the current configuration based @@ -1172,14 +1166,10 @@ public interface WindowManagerPolicy { /** * Called when layout of the windows is about to start. * - * @param isDefaultDisplay true if window is on {@link Display#DEFAULT_DISPLAY}. - * @param displayWidth The current full width of the screen. - * @param displayHeight The current full height of the screen. - * @param displayRotation The current rotation being applied to the base window. + * @param displayFrames frames of the display we are doing layout on. * @param uiMode The current uiMode in configuration. */ - public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight, - int displayRotation, int uiMode); + default void beginLayoutLw(DisplayFrames displayFrames, int uiMode) {} /** * Returns the bottom-most layer of the system decor, above which no policy decor should @@ -1188,37 +1178,28 @@ public interface WindowManagerPolicy { public int getSystemDecorLayerLw(); /** - * Return the rectangle of the screen that is available for applications to run in. - * This will be called immediately after {@link #beginLayoutLw}. - * - * @param r The rectangle to be filled with the boundaries available to applications. - */ - public void getContentRectLw(Rect r); - - /** - * Called for each window attached to the window manager as layout is - * proceeding. The implementation of this function must take care of - * setting the window's frame, either here or in finishLayout(). + * Called for each window attached to the window manager as layout is proceeding. The + * implementation of this function must take care of setting the window's frame, either here or + * in finishLayout(). * * @param win The window being positioned. * @param attached For sub-windows, the window it is attached to; this * window will already have had layoutWindow() called on it * so you can use its Rect. Otherwise null. + * @param displayFrames The display frames. */ - public void layoutWindowLw(WindowState win, WindowState attached); + default void layoutWindowLw( + WindowState win, WindowState attached, DisplayFrames displayFrames) {} /** - * Return the insets for the areas covered by system windows. These values - * are computed on the most recent layout, so they are not guaranteed to - * be correct. + * Return the insets for the areas covered by system windows. These values are computed on the + * most recent layout, so they are not guaranteed to be correct. * * @param attrs The LayoutParams of the window. * @param taskBounds The bounds of the task this window is on or {@code null} if no task is * associated with the window. - * @param displayRotation Rotation of the display. - * @param displayWidth The width of the display. - * @param displayHeight The height of the display. + * @param displayFrames display frames. * @param outContentInsets The areas covered by system windows, expressed as positive insets. * @param outStableInsets The areas covered by stable system windows irrespective of their * current visibility. Expressed as positive insets. @@ -1226,16 +1207,11 @@ public interface WindowManagerPolicy { * @return Whether to always consume the navigation bar. * See {@link #isNavBarForcedShownLw(WindowState)}. */ - public boolean getInsetHintLw(WindowManager.LayoutParams attrs, Rect taskBounds, - int displayRotation, int displayWidth, int displayHeight, Rect outContentInsets, - Rect outStableInsets, Rect outOutsets); - - /** - * Called when layout of the windows is finished. After this function has - * returned, all windows given to layoutWindow() <em>must</em> have had a - * frame assigned. - */ - public void finishLayoutLw(); + default boolean getInsetHintLw(WindowManager.LayoutParams attrs, Rect taskBounds, + DisplayFrames displayFrames, Rect outContentInsets, Rect outStableInsets, + Rect outOutsets) { + return false; + } /** Layout state may have changed (so another layout will be performed) */ static final int FINISH_LAYOUT_REDO_LAYOUT = 0x0001; @@ -1652,11 +1628,6 @@ public interface WindowManagerPolicy { public void showGlobalActions(); /** - * @return The current height of the input method window. - */ - public int getInputMethodWindowVisibleHeightLw(); - - /** * Called when the current user changes. Guaranteed to be called before the broadcast * of the new user id is made to all listeners. * diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java index 19213ca0..c3d6c695 100644 --- a/android/view/accessibility/AccessibilityInteractionClient.java +++ b/android/view/accessibility/AccessibilityInteractionClient.java @@ -187,8 +187,11 @@ public final class AccessibilityInteractionClient Log.i(LOG_TAG, "Window cache miss"); } final long identityToken = Binder.clearCallingIdentity(); - window = connection.getWindow(accessibilityWindowId); - Binder.restoreCallingIdentity(identityToken); + try { + window = connection.getWindow(accessibilityWindowId); + } finally { + Binder.restoreCallingIdentity(identityToken); + } if (window != null) { sAccessibilityCache.addWindow(window); return window; @@ -225,8 +228,11 @@ public final class AccessibilityInteractionClient Log.i(LOG_TAG, "Windows cache miss"); } final long identityToken = Binder.clearCallingIdentity(); - windows = connection.getWindows(); - Binder.restoreCallingIdentity(identityToken); + try { + windows = connection.getWindows(); + } finally { + Binder.restoreCallingIdentity(identityToken); + } if (windows != null) { sAccessibilityCache.setWindows(windows); return windows; @@ -283,10 +289,14 @@ public final class AccessibilityInteractionClient } final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId( - accessibilityWindowId, accessibilityNodeId, interactionId, this, - prefetchFlags, Thread.currentThread().getId(), arguments); - Binder.restoreCallingIdentity(identityToken); + final boolean success; + try { + success = connection.findAccessibilityNodeInfoByAccessibilityId( + accessibilityWindowId, accessibilityNodeId, interactionId, this, + prefetchFlags, Thread.currentThread().getId(), arguments); + } finally { + Binder.restoreCallingIdentity(identityToken); + } if (success) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); @@ -333,10 +343,15 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success = connection.findAccessibilityNodeInfosByViewId( - accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, - Thread.currentThread().getId()); - Binder.restoreCallingIdentity(identityToken); + final boolean success; + try { + success = connection.findAccessibilityNodeInfosByViewId( + accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, + Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (success) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); @@ -381,10 +396,15 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success = connection.findAccessibilityNodeInfosByText( - accessibilityWindowId, accessibilityNodeId, text, interactionId, this, - Thread.currentThread().getId()); - Binder.restoreCallingIdentity(identityToken); + final boolean success; + try { + success = connection.findAccessibilityNodeInfosByText( + accessibilityWindowId, accessibilityNodeId, text, interactionId, this, + Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (success) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); @@ -428,10 +448,15 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success = connection.findFocus(accessibilityWindowId, - accessibilityNodeId, focusType, interactionId, this, - Thread.currentThread().getId()); - Binder.restoreCallingIdentity(identityToken); + final boolean success; + try { + success = connection.findFocus(accessibilityWindowId, + accessibilityNodeId, focusType, interactionId, this, + Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (success) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( interactionId); @@ -472,10 +497,15 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success = connection.focusSearch(accessibilityWindowId, - accessibilityNodeId, direction, interactionId, this, - Thread.currentThread().getId()); - Binder.restoreCallingIdentity(identityToken); + final boolean success; + try { + success = connection.focusSearch(accessibilityWindowId, + accessibilityNodeId, direction, interactionId, this, + Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (success) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( interactionId); @@ -515,10 +545,15 @@ public final class AccessibilityInteractionClient if (connection != null) { final int interactionId = mInteractionIdCounter.getAndIncrement(); final long identityToken = Binder.clearCallingIdentity(); - final boolean success = connection.performAccessibilityAction( - accessibilityWindowId, accessibilityNodeId, action, arguments, - interactionId, this, Thread.currentThread().getId()); - Binder.restoreCallingIdentity(identityToken); + final boolean success; + try { + success = connection.performAccessibilityAction( + accessibilityWindowId, accessibilityNodeId, action, arguments, + interactionId, this, Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (success) { return getPerformAccessibilityActionResultAndClear(interactionId); } diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 0b9bc576..35f6acba 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -436,8 +436,11 @@ public final class AccessibilityManager { // client using it is called through Binder from another process. Example: MMS // app adds a SMS notification and the NotificationManagerService calls this method long identityToken = Binder.clearCallingIdentity(); - service.sendAccessibilityEvent(event, userId); - Binder.restoreCallingIdentity(identityToken); + try { + service.sendAccessibilityEvent(event, userId); + } finally { + Binder.restoreCallingIdentity(identityToken); + } if (DEBUG) { Log.i(LOG_TAG, event + " sent"); } diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java index e79d201b..9241ec00 100644 --- a/android/view/autofill/AutofillManager.java +++ b/android/view/autofill/AutofillManager.java @@ -54,6 +54,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +// TODO: use java.lang.ref.Cleaner once Android supports Java 9 +import sun.misc.Cleaner; + /** * The {@link AutofillManager} provides ways for apps and custom views to integrate with the * Autofill Framework lifecycle. @@ -303,6 +306,9 @@ public final class AutofillManager { private IAutoFillManagerClient mServiceClient; @GuardedBy("mLock") + private Cleaner mServiceClientCleaner; + + @GuardedBy("mLock") private AutofillCallback mCallback; private final Context mContext; @@ -1172,10 +1178,19 @@ public final class AutofillManager { if (mServiceClient == null) { mServiceClient = new AutofillManagerClient(this); try { - final int flags = mService.addClient(mServiceClient, mContext.getUserId()); + final int userId = mContext.getUserId(); + final int flags = mService.addClient(mServiceClient, userId); mEnabled = (flags & FLAG_ADD_CLIENT_ENABLED) != 0; sDebug = (flags & FLAG_ADD_CLIENT_DEBUG) != 0; sVerbose = (flags & FLAG_ADD_CLIENT_VERBOSE) != 0; + final IAutoFillManager service = mService; + final IAutoFillManagerClient serviceClient = mServiceClient; + mServiceClientCleaner = Cleaner.create(this, () -> { + try { + service.removeClient(serviceClient, userId); + } catch (RemoteException e) { + } + }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1272,18 +1287,36 @@ public final class AutofillManager { } } - private void setState(boolean enabled, boolean resetSession, boolean resetClient) { + /** @hide */ + public static final int SET_STATE_FLAG_ENABLED = 0x01; + /** @hide */ + public static final int SET_STATE_FLAG_RESET_SESSION = 0x02; + /** @hide */ + public static final int SET_STATE_FLAG_RESET_CLIENT = 0x04; + /** @hide */ + public static final int SET_STATE_FLAG_DEBUG = 0x08; + /** @hide */ + public static final int SET_STATE_FLAG_VERBOSE = 0x10; + + private void setState(int flags) { + if (sVerbose) Log.v(TAG, "setState(" + flags + ")"); synchronized (mLock) { - mEnabled = enabled; - if (!mEnabled || resetSession) { + mEnabled = (flags & SET_STATE_FLAG_ENABLED) != 0; + if (!mEnabled || (flags & SET_STATE_FLAG_RESET_SESSION) != 0) { // Reset the session state resetSessionLocked(); } - if (resetClient) { + if ((flags & SET_STATE_FLAG_RESET_CLIENT) != 0) { // Reset connection to system mServiceClient = null; + if (mServiceClientCleaner != null) { + mServiceClientCleaner.clean(); + mServiceClientCleaner = null; + } } } + sDebug = (flags & SET_STATE_FLAG_DEBUG) != 0; + sVerbose = (flags & SET_STATE_FLAG_VERBOSE) != 0; } /** @@ -1609,6 +1642,7 @@ public final class AutofillManager { pw.print(pfx); pw.print("sessionId: "); pw.println(mSessionId); pw.print(pfx); pw.print("state: "); pw.println(getStateAsStringLocked()); pw.print(pfx); pw.print("context: "); pw.println(mContext); + pw.print(pfx); pw.print("client: "); pw.println(getClientLocked()); pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled); pw.print(pfx); pw.print("hasService: "); pw.println(mService != null); pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null); @@ -1625,6 +1659,8 @@ public final class AutofillManager { pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds); pw.print(pfx); pw.print("save trigger id: "); pw.println(mSaveTriggerId); pw.print(pfx); pw.print("save on finish(): "); pw.println(mSaveOnFinish); + pw.print(pfx); pw.print("debug: "); pw.print(sDebug); + pw.print(" verbose: "); pw.println(sVerbose); } private String getStateAsStringLocked() { @@ -1880,7 +1916,7 @@ public final class AutofillManager { public abstract static class AutofillCallback { /** @hide */ - @IntDef({EVENT_INPUT_SHOWN, EVENT_INPUT_HIDDEN}) + @IntDef({EVENT_INPUT_SHOWN, EVENT_INPUT_HIDDEN, EVENT_INPUT_UNAVAILABLE}) @Retention(RetentionPolicy.SOURCE) public @interface AutofillEventType {} @@ -1940,10 +1976,10 @@ public final class AutofillManager { } @Override - public void setState(boolean enabled, boolean resetSession, boolean resetClient) { + public void setState(int flags) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() -> afm.setState(enabled, resetSession, resetClient)); + afm.post(() -> afm.setState(flags)); } } diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java index 26d2141e..2779aa2d 100644 --- a/android/view/textclassifier/TextClassification.java +++ b/android/view/textclassifier/TextClassification.java @@ -23,6 +23,7 @@ import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; +import android.os.LocaleList; import android.view.View.OnClickListener; import android.view.textclassifier.TextClassifier.EntityType; @@ -438,4 +439,31 @@ public final class TextClassification { mLogType, mVersionInfo); } } + + /** + * TextClassification optional input parameters. + */ + public static final class Options { + + private LocaleList mDefaultLocales; + + /** + * @param defaultLocales ordered list of locale preferences that may be used to disambiguate + * the provided text. If no locale preferences exist, set this to null or an empty + * locale list. + */ + public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** + * @return ordered list of locale preferences that can be used to disambiguate + * the provided text. + */ + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + } } diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java index 46dbd0e3..aeb84897 100644 --- a/android/view/textclassifier/TextClassifier.java +++ b/android/view/textclassifier/TextClassifier.java @@ -56,23 +56,7 @@ public interface TextClassifier { * No-op TextClassifier. * This may be used to turn off TextClassifier features. */ - TextClassifier NO_OP = new TextClassifier() { - - @Override - public TextSelection suggestSelection( - CharSequence text, - int selectionStartIndex, - int selectionEndIndex, - LocaleList defaultLocales) { - return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build(); - } - - @Override - public TextClassification classifyText( - CharSequence text, int startIndex, int endIndex, LocaleList defaultLocales) { - return TextClassification.EMPTY; - } - }; + TextClassifier NO_OP = new TextClassifier() {}; /** * Returns suggested text selection start and end indices, recognized entity types, and their @@ -82,21 +66,34 @@ public interface TextClassifier { * by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex) * @param selectionStartIndex start index of the selected part of text * @param selectionEndIndex end index of the selected part of text - * @param defaultLocales ordered list of locale preferences that can be used to disambiguate - * the provided text. If no locale preferences exist, set this to null or an empty locale - * list in which case the classifier will decide whether to use no locale information, use - * a default locale, or use the system default. + * @param options optional input parameters * * @throws IllegalArgumentException if text is null; selectionStartIndex is negative; * selectionEndIndex is greater than text.length() or not greater than selectionStartIndex */ @WorkerThread @NonNull - TextSelection suggestSelection( + default TextSelection suggestSelection( + @NonNull CharSequence text, + @IntRange(from = 0) int selectionStartIndex, + @IntRange(from = 0) int selectionEndIndex, + @Nullable TextSelection.Options options) { + return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build(); + } + + /** + * @see #suggestSelection(CharSequence, int, int, TextSelection.Options) + */ + // TODO: Consider deprecating (b/68846316) + @WorkerThread + @NonNull + default TextSelection suggestSelection( @NonNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, - @Nullable LocaleList defaultLocales); + @Nullable LocaleList defaultLocales) { + return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build(); + } /** * Classifies the specified text and returns a {@link TextClassification} object that can be @@ -106,41 +103,48 @@ public interface TextClassifier { * by the sub sequence starting at startIndex and ending at endIndex) * @param startIndex start index of the text to classify * @param endIndex end index of the text to classify - * @param defaultLocales ordered list of locale preferences that can be used to disambiguate - * the provided text. If no locale preferences exist, set this to null or an empty locale - * list in which case the classifier will decide whether to use no locale information, use - * a default locale, or use the system default. + * @param options optional input parameters * * @throws IllegalArgumentException if text is null; startIndex is negative; * endIndex is greater than text.length() or not greater than startIndex */ @WorkerThread @NonNull - TextClassification classifyText( + default TextClassification classifyText( @NonNull CharSequence text, @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, - @Nullable LocaleList defaultLocales); + @Nullable TextClassification.Options options) { + return TextClassification.EMPTY; + } /** - * Returns a {@link LinksInfo} that may be applied to the text to annotate it with links + * @see #classifyText(CharSequence, int, int, TextClassification.Options) + */ + // TODO: Consider deprecating (b/68846316) + @WorkerThread + @NonNull + default TextClassification classifyText( + @NonNull CharSequence text, + @IntRange(from = 0) int startIndex, + @IntRange(from = 0) int endIndex, + @Nullable LocaleList defaultLocales) { + return TextClassification.EMPTY; + } + + /** + * Returns a {@link TextLinks} that may be applied to the text to annotate it with links * information. * * @param text the text to generate annotations for - * @param linkMask See {@link android.text.util.Linkify} for a list of linkMasks that may be - * specified. Subclasses of this interface may specify additional linkMasks - * @param defaultLocales ordered list of locale preferences that can be used to disambiguate - * the provided text. If no locale preferences exist, set this to null or an empty locale - * list in which case the classifier will decide whether to use no locale information, use - * a default locale, or use the system default. + * @param options configuration for link generation. If null, defaults will be used. * * @throws IllegalArgumentException if text is null - * @hide */ @WorkerThread - default LinksInfo getLinks( - @NonNull CharSequence text, int linkMask, @Nullable LocaleList defaultLocales) { - return LinksInfo.NO_OP; + default TextLinks generateLinks( + @NonNull CharSequence text, @Nullable TextLinks.Options options) { + return new TextLinks.Builder(text.toString()).build(); } /** diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java index 1c07be4b..2ad6e02c 100644 --- a/android/view/textclassifier/TextClassifierImpl.java +++ b/android/view/textclassifier/TextClassifierImpl.java @@ -30,13 +30,8 @@ import android.os.ParcelFileDescriptor; import android.provider.Browser; import android.provider.ContactsContract; import android.provider.Settings; -import android.text.Spannable; -import android.text.TextUtils; -import android.text.method.WordIterator; -import android.text.style.ClickableSpan; import android.text.util.Linkify; import android.util.Patterns; -import android.view.View; import android.widget.TextViewMetrics; import com.android.internal.annotations.GuardedBy; @@ -46,13 +41,8 @@ import com.android.internal.util.Preconditions; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.text.BreakIterator; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -100,16 +90,24 @@ final class TextClassifierImpl implements TextClassifier { @Override public TextSelection suggestSelection( @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex, - @Nullable LocaleList defaultLocales) { + @Nullable TextSelection.Options options) { validateInput(text, selectionStartIndex, selectionEndIndex); try { if (text.length() > 0) { - final SmartSelection smartSelection = getSmartSelection(defaultLocales); + final LocaleList locales = (options == null) ? null : options.getDefaultLocales(); + final SmartSelection smartSelection = getSmartSelection(locales); final String string = text.toString(); - final int[] startEnd = smartSelection.suggest( - string, selectionStartIndex, selectionEndIndex); - final int start = startEnd[0]; - final int end = startEnd[1]; + final int start; + final int end; + if (getSettings().isDarkLaunch() && !options.isDarkLaunchAllowed()) { + start = selectionStartIndex; + end = selectionEndIndex; + } else { + final int[] startEnd = smartSelection.suggest( + string, selectionStartIndex, selectionEndIndex); + start = startEnd[0]; + end = startEnd[1]; + } if (start <= end && start >= 0 && end <= string.length() && start <= selectionStartIndex && end >= selectionEndIndex) { @@ -139,18 +137,27 @@ final class TextClassifierImpl implements TextClassifier { } // Getting here means something went wrong, return a NO_OP result. return TextClassifier.NO_OP.suggestSelection( - text, selectionStartIndex, selectionEndIndex, defaultLocales); + text, selectionStartIndex, selectionEndIndex, options); + } + + @Override + public TextSelection suggestSelection( + @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex, + @Nullable LocaleList defaultLocales) { + return suggestSelection(text, selectionStartIndex, selectionEndIndex, + new TextSelection.Options().setDefaultLocales(defaultLocales)); } @Override public TextClassification classifyText( @NonNull CharSequence text, int startIndex, int endIndex, - @Nullable LocaleList defaultLocales) { + @Nullable TextClassification.Options options) { validateInput(text, startIndex, endIndex); try { if (text.length() > 0) { final String string = text.toString(); - SmartSelection.ClassificationResult[] results = getSmartSelection(defaultLocales) + final LocaleList locales = (options == null) ? null : options.getDefaultLocales(); + final SmartSelection.ClassificationResult[] results = getSmartSelection(locales) .classifyText(string, startIndex, endIndex, getHintFlags(string, startIndex, endIndex)); if (results.length > 0) { @@ -165,23 +172,41 @@ final class TextClassifierImpl implements TextClassifier { Log.e(LOG_TAG, "Error getting text classification info.", t); } // Getting here means something went wrong, return a NO_OP result. - return TextClassifier.NO_OP.classifyText( - text, startIndex, endIndex, defaultLocales); + return TextClassifier.NO_OP.classifyText(text, startIndex, endIndex, options); } @Override - public LinksInfo getLinks( - @NonNull CharSequence text, int linkMask, @Nullable LocaleList defaultLocales) { - Preconditions.checkArgument(text != null); + public TextClassification classifyText( + @NonNull CharSequence text, int startIndex, int endIndex, + @Nullable LocaleList defaultLocales) { + return classifyText(text, startIndex, endIndex, + new TextClassification.Options().setDefaultLocales(defaultLocales)); + } + + @Override + public TextLinks generateLinks( + @NonNull CharSequence text, @Nullable TextLinks.Options options) { + Preconditions.checkNotNull(text); + final String textString = text.toString(); + final TextLinks.Builder builder = new TextLinks.Builder(textString); try { - return LinksInfoFactory.create( - mContext, getSmartSelection(defaultLocales), text.toString(), linkMask); + LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null; + final SmartSelection smartSelection = getSmartSelection(defaultLocales); + final SmartSelection.AnnotatedSpan[] annotations = smartSelection.annotate(textString); + for (SmartSelection.AnnotatedSpan span : annotations) { + final Map<String, Float> entityScores = new HashMap<>(); + final SmartSelection.ClassificationResult[] results = span.getClassification(); + for (int i = 0; i < results.length; i++) { + entityScores.put(results[i].mCollection, results[i].mScore); + } + builder.addLink(new TextLinks.TextLink( + textString, span.getStartIndex(), span.getEndIndex(), entityScores)); + } } catch (Throwable t) { // Avoid throwing from this method. Log the error. Log.e(LOG_TAG, "Error getting links info.", t); } - // Getting here means something went wrong, return a NO_OP result. - return TextClassifier.NO_OP.getLinks(text, linkMask, defaultLocales); + return builder.build(); } @Override @@ -210,7 +235,9 @@ final class TextClassifierImpl implements TextClassifier { if (mSmartSelection == null || !Objects.equals(mLocale, locale)) { destroySmartSelectionIfExistsLocked(); final ParcelFileDescriptor fd = getFdLocked(locale); - mSmartSelection = new SmartSelection(fd.getFd()); + final int modelFd = fd.getFd(); + mVersion = SmartSelection.getVersion(modelFd); + mSmartSelection = new SmartSelection(modelFd); closeAndLogError(fd); mLocale = locale; } @@ -231,18 +258,26 @@ final class TextClassifierImpl implements TextClassifier { @GuardedBy("mSmartSelectionLock") // Do not call outside this lock. private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException { ParcelFileDescriptor updateFd; + int updateVersion = -1; try { updateFd = ParcelFileDescriptor.open( new File(UPDATED_MODEL_FILE_PATH), ParcelFileDescriptor.MODE_READ_ONLY); + if (updateFd != null) { + updateVersion = SmartSelection.getVersion(updateFd.getFd()); + } } catch (FileNotFoundException e) { updateFd = null; } ParcelFileDescriptor factoryFd; + int factoryVersion = -1; try { final String factoryModelFilePath = getFactoryModelFilePathsLocked().get(locale); if (factoryModelFilePath != null) { factoryFd = ParcelFileDescriptor.open( new File(factoryModelFilePath), ParcelFileDescriptor.MODE_READ_ONLY); + if (factoryFd != null) { + factoryVersion = SmartSelection.getVersion(factoryFd.getFd()); + } } else { factoryFd = null; } @@ -278,15 +313,11 @@ final class TextClassifierImpl implements TextClassifier { return factoryFd; } - final int updateVersion = SmartSelection.getVersion(updateFdInt); - final int factoryVersion = SmartSelection.getVersion(factoryFd.getFd()); if (updateVersion > factoryVersion) { closeAndLogError(factoryFd); - mVersion = updateVersion; return updateFd; } else { closeAndLogError(updateFd); - mVersion = factoryVersion; return factoryFd; } } @@ -466,180 +497,6 @@ final class TextClassifierImpl implements TextClassifier { } /** - * Detects and creates links for specified text. - */ - private static final class LinksInfoFactory { - - private LinksInfoFactory() {} - - public static LinksInfo create( - Context context, SmartSelection smartSelection, String text, int linkMask) { - final WordIterator wordIterator = new WordIterator(); - wordIterator.setCharSequence(text, 0, text.length()); - final List<SpanSpec> spans = new ArrayList<>(); - int start = 0; - int end; - while ((end = wordIterator.nextBoundary(start)) != BreakIterator.DONE) { - final String token = text.substring(start, end); - if (TextUtils.isEmpty(token)) { - continue; - } - - final int[] selection = smartSelection.suggest(text, start, end); - final int selectionStart = selection[0]; - final int selectionEnd = selection[1]; - if (selectionStart >= 0 && selectionEnd <= text.length() - && selectionStart <= selectionEnd) { - final SmartSelection.ClassificationResult[] results = - smartSelection.classifyText( - text, selectionStart, selectionEnd, - getHintFlags(text, selectionStart, selectionEnd)); - if (results.length > 0) { - final String type = getHighestScoringType(results); - if (matches(type, linkMask)) { - // For links without disambiguation, we simply use the default intent. - final List<Intent> intents = IntentFactory.create( - context, type, text.substring(selectionStart, selectionEnd)); - if (!intents.isEmpty() && hasActivityHandler(context, intents.get(0))) { - final ClickableSpan span = createSpan(context, intents.get(0)); - spans.add(new SpanSpec(selectionStart, selectionEnd, span)); - } - } - } - } - start = end; - } - return new LinksInfoImpl(text, avoidOverlaps(spans, text)); - } - - /** - * Returns true if the classification type matches the specified linkMask. - */ - private static boolean matches(String type, int linkMask) { - type = type.trim().toLowerCase(Locale.ENGLISH); - if ((linkMask & Linkify.PHONE_NUMBERS) != 0 - && TextClassifier.TYPE_PHONE.equals(type)) { - return true; - } - if ((linkMask & Linkify.EMAIL_ADDRESSES) != 0 - && TextClassifier.TYPE_EMAIL.equals(type)) { - return true; - } - if ((linkMask & Linkify.MAP_ADDRESSES) != 0 - && TextClassifier.TYPE_ADDRESS.equals(type)) { - return true; - } - if ((linkMask & Linkify.WEB_URLS) != 0 - && TextClassifier.TYPE_URL.equals(type)) { - return true; - } - return false; - } - - /** - * Trim the number of spans so that no two spans overlap. - * - * This algorithm first ensures that there is only one span per start index, then it - * makes sure that no two spans overlap. - */ - private static List<SpanSpec> avoidOverlaps(List<SpanSpec> spans, String text) { - Collections.sort(spans, Comparator.comparingInt(span -> span.mStart)); - // Group spans by start index. Take the longest span. - final Map<Integer, SpanSpec> reps = new LinkedHashMap<>(); // order matters. - final int size = spans.size(); - for (int i = 0; i < size; i++) { - final SpanSpec span = spans.get(i); - final LinksInfoFactory.SpanSpec rep = reps.get(span.mStart); - if (rep == null || rep.mEnd < span.mEnd) { - reps.put(span.mStart, span); - } - } - // Avoid span intersections. Take the longer span. - final LinkedList<SpanSpec> result = new LinkedList<>(); - for (SpanSpec rep : reps.values()) { - if (result.isEmpty()) { - result.add(rep); - continue; - } - - final SpanSpec last = result.getLast(); - if (rep.mStart < last.mEnd) { - // Spans intersect. Use the one with characters. - if ((rep.mEnd - rep.mStart) > (last.mEnd - last.mStart)) { - result.set(result.size() - 1, rep); - } - } else { - result.add(rep); - } - } - return result; - } - - private static ClickableSpan createSpan(final Context context, final Intent intent) { - return new ClickableSpan() { - // TODO: Style this span. - @Override - public void onClick(View widget) { - context.startActivity(intent); - } - }; - } - - private static boolean hasActivityHandler(Context context, Intent intent) { - if (intent == null) { - return false; - } - final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0); - return resolveInfo != null && resolveInfo.activityInfo != null; - } - - /** - * Implementation of LinksInfo that adds ClickableSpans to the specified text. - */ - private static final class LinksInfoImpl implements LinksInfo { - - private final CharSequence mOriginalText; - private final List<SpanSpec> mSpans; - - LinksInfoImpl(CharSequence originalText, List<SpanSpec> spans) { - mOriginalText = originalText; - mSpans = spans; - } - - @Override - public boolean apply(@NonNull CharSequence text) { - Preconditions.checkArgument(text != null); - if (text instanceof Spannable && mOriginalText.toString().equals(text.toString())) { - Spannable spannable = (Spannable) text; - final int size = mSpans.size(); - for (int i = 0; i < size; i++) { - final SpanSpec span = mSpans.get(i); - spannable.setSpan(span.mSpan, span.mStart, span.mEnd, 0); - } - return true; - } - return false; - } - } - - /** - * Span plus its start and end index. - */ - private static final class SpanSpec { - - private final int mStart; - private final int mEnd; - private final ClickableSpan mSpan; - - SpanSpec(int start, int end, ClickableSpan span) { - mStart = start; - mEnd = end; - mSpan = span; - } - } - } - - /** * Creates intents based on the classification type. */ private static final class IntentFactory { @@ -656,8 +513,8 @@ final class TextClassifierImpl implements TextClassifier { intents.add(new Intent(Intent.ACTION_SENDTO) .setData(Uri.parse(String.format("mailto:%s", text)))); intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) - .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) - .putExtra(ContactsContract.Intents.Insert.EMAIL, text)); + .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) + .putExtra(ContactsContract.Intents.Insert.EMAIL, text)); break; case TextClassifier.TYPE_PHONE: intents.add(new Intent(Intent.ACTION_DIAL) diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java new file mode 100644 index 00000000..f3cc827f --- /dev/null +++ b/android/view/textclassifier/TextLinks.java @@ -0,0 +1,252 @@ +/* + * Copyright 2017 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 android.view.textclassifier; + +import android.annotation.FloatRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.LocaleList; +import android.text.SpannableString; +import android.text.style.ClickableSpan; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * A collection of links, representing subsequences of text and the entity types (phone number, + * address, url, etc) they may be. + */ +public final class TextLinks { + private final String mFullText; + private final List<TextLink> mLinks; + + private TextLinks(String fullText, Collection<TextLink> links) { + mFullText = fullText; + mLinks = Collections.unmodifiableList(new ArrayList<>(links)); + } + + /** + * Returns an unmodifiable Collection of the links. + */ + public Collection<TextLink> getLinks() { + return mLinks; + } + + /** + * Annotates the given text with the generated links. It will fail if the provided text doesn't + * match the original text used to crete the TextLinks. + * + * @param text the text to apply the links to. Must match the original text. + * @param spanFactory a factory to generate spans from TextLinks. Will use a default if null. + * + * @return Success or failure. + */ + public boolean apply( + @NonNull SpannableString text, + @Nullable Function<TextLink, ClickableSpan> spanFactory) { + Preconditions.checkNotNull(text); + if (!mFullText.equals(text.toString())) { + return false; + } + + if (spanFactory == null) { + spanFactory = DEFAULT_SPAN_FACTORY; + } + for (TextLink link : mLinks) { + final ClickableSpan span = spanFactory.apply(link); + if (span != null) { + text.setSpan(span, link.getStart(), link.getEnd(), 0); + } + } + return true; + } + + /** + * A link, identifying a substring of text and possible entity types for it. + */ + public static final class TextLink { + private final EntityConfidence<String> mEntityScores; + private final String mOriginalText; + private final int mStart; + private final int mEnd; + + /** + * Create a new TextLink. + * + * @throws IllegalArgumentException if entityScores is null or empty. + */ + public TextLink(String originalText, int start, int end, Map<String, Float> entityScores) { + Preconditions.checkNotNull(originalText); + Preconditions.checkNotNull(entityScores); + Preconditions.checkArgument(!entityScores.isEmpty()); + Preconditions.checkArgument(start <= end); + mOriginalText = originalText; + mStart = start; + mEnd = end; + mEntityScores = new EntityConfidence<>(); + + for (Map.Entry<String, Float> entry : entityScores.entrySet()) { + mEntityScores.setEntityType(entry.getKey(), entry.getValue()); + } + } + + /** + * Returns the start index of this link in the original text. + * + * @return the start index. + */ + public int getStart() { + return mStart; + } + + /** + * Returns the end index of this link in the original text. + * + * @return the end index. + */ + public int getEnd() { + return mEnd; + } + + /** + * Returns the number of entity types that have confidence scores. + * + * @return the entity count. + */ + public int getEntityCount() { + return mEntityScores.getEntities().size(); + } + + /** + * Returns the entity type at a given index. Entity types are sorted by confidence. + * + * @return the entity type at the provided index. + */ + @NonNull public @TextClassifier.EntityType String getEntity(int index) { + return mEntityScores.getEntities().get(index); + } + + /** + * Returns the confidence score for a particular entity type. + * + * @param entityType the entity type. + */ + public @FloatRange(from = 0.0, to = 1.0) float getConfidenceScore( + @TextClassifier.EntityType String entityType) { + return mEntityScores.getConfidenceScore(entityType); + } + } + + /** + * Optional input parameters for generating TextLinks. + */ + public static final class Options { + private final LocaleList mLocaleList; + + private Options(LocaleList localeList) { + this.mLocaleList = localeList; + } + + /** + * Builder to construct Options. + */ + public static final class Builder { + private LocaleList mLocaleList; + + /** + * Sets the LocaleList to use. + * + * @return this Builder. + */ + public Builder setLocaleList(@Nullable LocaleList localeList) { + this.mLocaleList = localeList; + return this; + } + + /** + * Builds the Options object. + */ + public Options build() { + return new Options(mLocaleList); + } + } + public @Nullable LocaleList getDefaultLocales() { + return mLocaleList; + } + }; + + /** + * A function to create spans from TextLinks. + * + * Applies only to TextViews. + * We can hide this until we are convinced we want it to be part of the public API. + * + * @hide + */ + public static final Function<TextLink, ClickableSpan> DEFAULT_SPAN_FACTORY = + new Function<TextLink, ClickableSpan>() { + @Override + public ClickableSpan apply(TextLink textLink) { + // TODO: Implement. + throw new UnsupportedOperationException("Not yet implemented"); + } + }; + + /** + * A builder to construct a TextLinks instance. + */ + public static final class Builder { + private final String mFullText; + private final Collection<TextLink> mLinks; + + /** + * Create a new TextLinks.Builder. + * + * @param fullText The full text that links will be added to. + */ + public Builder(@NonNull String fullText) { + mFullText = Preconditions.checkNotNull(fullText); + mLinks = new ArrayList<>(); + } + + /** + * Adds a TextLink. + * + * @return this instance. + */ + public Builder addLink(TextLink link) { + Preconditions.checkNotNull(link); + mLinks.add(link); + return this; + } + + /** + * Constructs a TextLinks instance. + * + * @return the constructed TextLinks. + */ + public TextLinks build() { + return new TextLinks(mFullText, mLinks); + } + } +} diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java index 11ebe835..0a67954a 100644 --- a/android/view/textclassifier/TextSelection.java +++ b/android/view/textclassifier/TextSelection.java @@ -19,6 +19,8 @@ package android.view.textclassifier; import android.annotation.FloatRange; import android.annotation.IntRange; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.LocaleList; import android.view.textclassifier.TextClassifier.EntityType; import com.android.internal.util.Preconditions; @@ -181,4 +183,55 @@ public final class TextSelection { mStartIndex, mEndIndex, mEntityConfidence, mLogSource, mVersionInfo); } } + + /** + * TextSelection optional input parameters. + */ + public static final class Options { + + private LocaleList mDefaultLocales; + private boolean mDarkLaunchAllowed; + + /** + * @param defaultLocales ordered list of locale preferences that may be used to disambiguate + * the provided text. If no locale preferences exist, set this to null or an empty + * locale list. + */ + public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** + * @return ordered list of locale preferences that can be used to disambiguate + * the provided text. + */ + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + + /** + * @param allowed whether or not the TextClassifier should return selection suggestions + * when "dark launched". When a TextClassifier is dark launched, it can suggest + * selection changes that should not be used to actually change the user's selection. + * Instead, the suggested selection is logged, compared with the user's selection + * interaction, and used to generate quality metrics for the TextClassifier. + * + * @hide + */ + public void setDarkLaunchAllowed(boolean allowed) { + mDarkLaunchAllowed = allowed; + } + + /** + * Returns true if the TextClassifier should return selection suggestions when + * "dark launched". Otherwise, returns false. + * + * @hide + */ + public boolean isDarkLaunchAllowed() { + return mDarkLaunchAllowed; + } + } } diff --git a/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/android/view/textclassifier/logging/SmartSelectionEventTracker.java index 83af19bb..2833564f 100644 --- a/android/view/textclassifier/logging/SmartSelectionEventTracker.java +++ b/android/view/textclassifier/logging/SmartSelectionEventTracker.java @@ -48,31 +48,45 @@ public final class SmartSelectionEventTracker { private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START; private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS; private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX; - private static final int VERSION_TAG = MetricsEvent.FIELD_SELECTION_VERSION_TAG; - private static final int SMART_INDICES = MetricsEvent.FIELD_SELECTION_SMART_RANGE; - private static final int EVENT_INDICES = MetricsEvent.FIELD_SELECTION_RANGE; + private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE; + private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION; + private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL; + private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE; + private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START; + private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END; + private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START; + private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END; private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID; private static final String ZERO = "0"; private static final String TEXTVIEW = "textview"; private static final String EDITTEXT = "edittext"; + private static final String UNSELECTABLE_TEXTVIEW = "nosel-textview"; private static final String WEBVIEW = "webview"; private static final String EDIT_WEBVIEW = "edit-webview"; + private static final String CUSTOM_TEXTVIEW = "customview"; + private static final String CUSTOM_EDITTEXT = "customedit"; + private static final String CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview"; private static final String UNKNOWN = "unknown"; @Retention(RetentionPolicy.SOURCE) @IntDef({WidgetType.UNSPECIFIED, WidgetType.TEXTVIEW, WidgetType.WEBVIEW, WidgetType.EDITTEXT, WidgetType.EDIT_WEBVIEW}) public @interface WidgetType { - int UNSPECIFIED = 0; - int TEXTVIEW = 1; - int WEBVIEW = 2; - int EDITTEXT = 3; - int EDIT_WEBVIEW = 4; + int UNSPECIFIED = 0; + int TEXTVIEW = 1; + int WEBVIEW = 2; + int EDITTEXT = 3; + int EDIT_WEBVIEW = 4; + int UNSELECTABLE_TEXTVIEW = 5; + int CUSTOM_TEXTVIEW = 6; + int CUSTOM_EDITTEXT = 7; + int CUSTOM_UNSELECTABLE_TEXTVIEW = 8; } private final MetricsLogger mMetricsLogger = new MetricsLogger(); private final int mWidgetType; + @Nullable private final String mWidgetVersion; private final Context mContext; @Nullable private String mSessionId; @@ -83,10 +97,18 @@ public final class SmartSelectionEventTracker { private long mSessionStartTime; private long mLastEventTime; private boolean mSmartSelectionTriggered; - private String mVersionTag; + private String mModelName; public SmartSelectionEventTracker(@NonNull Context context, @WidgetType int widgetType) { mWidgetType = widgetType; + mWidgetVersion = null; + mContext = Preconditions.checkNotNull(context); + } + + public SmartSelectionEventTracker( + @NonNull Context context, @WidgetType int widgetType, @Nullable String widgetVersion) { + mWidgetType = widgetType; + mWidgetVersion = widgetVersion; mContext = Preconditions.checkNotNull(context); } @@ -115,7 +137,7 @@ public final class SmartSelectionEventTracker { case SelectionEvent.EventType.SMART_SELECTION_SINGLE: // fall through case SelectionEvent.EventType.SMART_SELECTION_MULTI: mSmartSelectionTriggered = true; - mVersionTag = getVersionTag(event); + mModelName = getModelName(event); mSmartIndices[0] = event.mStart; mSmartIndices[1] = event.mEnd; break; @@ -137,14 +159,19 @@ public final class SmartSelectionEventTracker { final long prevEventDelta = mLastEventTime == 0 ? 0 : now - mLastEventTime; final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION) .setType(getLogType(event)) - .setSubtype(getLogSubType(event)) + .setSubtype(MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL) .setPackageName(mContext.getPackageName()) .addTaggedData(START_EVENT_DELTA, now - mSessionStartTime) .addTaggedData(PREV_EVENT_DELTA, prevEventDelta) .addTaggedData(INDEX, mIndex) - .addTaggedData(VERSION_TAG, mVersionTag) - .addTaggedData(SMART_INDICES, getSmartDelta()) - .addTaggedData(EVENT_INDICES, getEventDelta(event)) + .addTaggedData(WIDGET_TYPE, getWidgetTypeName()) + .addTaggedData(WIDGET_VERSION, mWidgetVersion) + .addTaggedData(MODEL_NAME, mModelName) + .addTaggedData(ENTITY_TYPE, event.mEntityType) + .addTaggedData(SMART_START, getSmartRangeDelta(mSmartIndices[0])) + .addTaggedData(SMART_END, getSmartRangeDelta(mSmartIndices[1])) + .addTaggedData(EVENT_START, getRangeDelta(event.mStart)) + .addTaggedData(EVENT_END, getRangeDelta(event.mEnd)) .addTaggedData(SESSION_ID, mSessionId); mMetricsLogger.write(log); debugLog(log); @@ -169,7 +196,7 @@ public final class SmartSelectionEventTracker { mSessionStartTime = 0; mLastEventTime = 0; mSmartSelectionTriggered = false; - mVersionTag = getVersionTag(null); + mModelName = getModelName(null); mSessionId = null; } @@ -251,113 +278,75 @@ public final class SmartSelectionEventTracker { } } - private static int getLogSubType(SelectionEvent event) { - switch (event.mEntityType) { - case TextClassifier.TYPE_OTHER: - return MetricsEvent.TEXT_CLASSIFIER_TYPE_OTHER; - case TextClassifier.TYPE_EMAIL: - return MetricsEvent.TEXT_CLASSIFIER_TYPE_EMAIL; - case TextClassifier.TYPE_PHONE: - return MetricsEvent.TEXT_CLASSIFIER_TYPE_PHONE; - case TextClassifier.TYPE_ADDRESS: - return MetricsEvent.TEXT_CLASSIFIER_TYPE_ADDRESS; - case TextClassifier.TYPE_URL: - return MetricsEvent.TEXT_CLASSIFIER_TYPE_URL; - default: - return MetricsEvent.TEXT_CLASSIFIER_TYPE_UNKNOWN; - } - } - - private static String getLogSubTypeString(int logSubType) { - switch (logSubType) { - case MetricsEvent.TEXT_CLASSIFIER_TYPE_OTHER: - return TextClassifier.TYPE_OTHER; - case MetricsEvent.TEXT_CLASSIFIER_TYPE_EMAIL: - return TextClassifier.TYPE_EMAIL; - case MetricsEvent.TEXT_CLASSIFIER_TYPE_PHONE: - return TextClassifier.TYPE_PHONE; - case MetricsEvent.TEXT_CLASSIFIER_TYPE_ADDRESS: - return TextClassifier.TYPE_ADDRESS; - case MetricsEvent.TEXT_CLASSIFIER_TYPE_URL: - return TextClassifier.TYPE_URL; - default: - return TextClassifier.TYPE_UNKNOWN; - } - } - - private int getSmartDelta() { - if (mSmartSelectionTriggered) { - return (clamp(mSmartIndices[0] - mOrigStart) << 16) - | (clamp(mSmartIndices[1] - mOrigStart) & 0xffff); - } - // If the smart selection model was not run, return invalid selection indices [0,0]. This - // allows us to tell from the terminal event alone whether the model was run. - return 0; + private int getRangeDelta(int offset) { + return offset - mOrigStart; } - private int getEventDelta(SelectionEvent event) { - return (clamp(event.mStart - mOrigStart) << 16) - | (clamp(event.mEnd - mOrigStart) & 0xffff); + private int getSmartRangeDelta(int offset) { + return mSmartSelectionTriggered ? getRangeDelta(offset) : 0; } - private String getVersionTag(@Nullable SelectionEvent event) { - final String widgetType; + private String getWidgetTypeName() { switch (mWidgetType) { case WidgetType.TEXTVIEW: - widgetType = TEXTVIEW; - break; + return TEXTVIEW; case WidgetType.WEBVIEW: - widgetType = WEBVIEW; - break; + return WEBVIEW; case WidgetType.EDITTEXT: - widgetType = EDITTEXT; - break; + return EDITTEXT; case WidgetType.EDIT_WEBVIEW: - widgetType = EDIT_WEBVIEW; - break; + return EDIT_WEBVIEW; + case WidgetType.UNSELECTABLE_TEXTVIEW: + return UNSELECTABLE_TEXTVIEW; + case WidgetType.CUSTOM_TEXTVIEW: + return CUSTOM_TEXTVIEW; + case WidgetType.CUSTOM_EDITTEXT: + return CUSTOM_EDITTEXT; + case WidgetType.CUSTOM_UNSELECTABLE_TEXTVIEW: + return CUSTOM_UNSELECTABLE_TEXTVIEW; default: - widgetType = UNKNOWN; + return UNKNOWN; } - final String version = event == null + } + + private String getModelName(@Nullable SelectionEvent event) { + return event == null ? SelectionEvent.NO_VERSION_TAG : Objects.toString(event.mVersionTag, SelectionEvent.NO_VERSION_TAG); - return String.format("%s/%s", widgetType, version); } private static String createSessionId() { return UUID.randomUUID().toString(); } - private static int clamp(int val) { - return Math.max(Math.min(val, Short.MAX_VALUE), Short.MIN_VALUE); - } - private static void debugLog(LogMaker log) { if (!DEBUG_LOG_ENABLED) return; - final String tag = Objects.toString(log.getTaggedData(VERSION_TAG), "tag"); + final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN); + final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), ""); + final String widget = widgetVersion.isEmpty() + ? widgetType : widgetType + "-" + widgetVersion; final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO)); if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) { String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), ""); sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1); - Log.d(LOG_TAG, String.format("New selection session: %s(%s)", tag, sessionId)); + Log.d(LOG_TAG, String.format("New selection session: %s (%s)", widget, sessionId)); } + final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN); + final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN); final String type = getLogTypeString(log.getType()); - final String subType = getLogSubTypeString(log.getSubtype()); - - final int smartIndices = Integer.parseInt( - Objects.toString(log.getTaggedData(SMART_INDICES), ZERO)); - final int smartStart = (short) ((smartIndices & 0xffff0000) >> 16); - final int smartEnd = (short) (smartIndices & 0xffff); - - final int eventIndices = Integer.parseInt( - Objects.toString(log.getTaggedData(EVENT_INDICES), ZERO)); - final int eventStart = (short) ((eventIndices & 0xffff0000) >> 16); - final int eventEnd = (short) (eventIndices & 0xffff); - - Log.d(LOG_TAG, String.format("%2d: %s/%s, context=%d,%d - old=%d,%d (%s)", - index, type, subType, eventStart, eventEnd, smartStart, smartEnd, tag)); + final int smartStart = Integer.parseInt( + Objects.toString(log.getTaggedData(SMART_START), ZERO)); + final int smartEnd = Integer.parseInt( + Objects.toString(log.getTaggedData(SMART_END), ZERO)); + final int eventStart = Integer.parseInt( + Objects.toString(log.getTaggedData(EVENT_START), ZERO)); + final int eventEnd = Integer.parseInt( + Objects.toString(log.getTaggedData(EVENT_END), ZERO)); + + Log.d(LOG_TAG, String.format("%2d: %s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)", + index, type, entity, eventStart, eventEnd, smartStart, smartEnd, widget, model)); } /** @@ -369,12 +358,12 @@ public final class SmartSelectionEventTracker { /** * Use this to specify an indeterminate positive index. */ - public static final int OUT_OF_BOUNDS = Short.MAX_VALUE; + public static final int OUT_OF_BOUNDS = Integer.MAX_VALUE; /** * Use this to specify an indeterminate negative index. */ - public static final int OUT_OF_BOUNDS_NEGATIVE = Short.MIN_VALUE; + public static final int OUT_OF_BOUNDS_NEGATIVE = Integer.MIN_VALUE; private static final String NO_VERSION_TAG = ""; diff --git a/android/webkit/ServiceWorkerClient.java b/android/webkit/ServiceWorkerClient.java index d6e8f36c..9124c855 100644 --- a/android/webkit/ServiceWorkerClient.java +++ b/android/webkit/ServiceWorkerClient.java @@ -29,9 +29,9 @@ public class ServiceWorkerClient { * application to return the data. If the return value is {@code null}, the * Service Worker will continue to load the resource as usual. * Otherwise, the return response and data will be used. - * NOTE: This method is called on a thread other than the UI thread - * so clients should exercise caution when accessing private data - * or the view system. + * + * <p class="note"><b>Note:</b> This method is called on a thread other than the UI thread so + * clients should exercise caution when accessing private data or the view system. * * @param request Object containing the details of the request. * @return A {@link android.webkit.WebResourceResponse} containing the diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java index 259bf60a..665d694e 100644 --- a/android/webkit/WebView.java +++ b/android/webkit/WebView.java @@ -191,7 +191,7 @@ import java.util.Map; * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}). * - * <p>NOTE: Using zoom if either the height or width is set to + * <p class="note"><b>Note:</b> Using zoom if either the height or width is set to * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} may lead to undefined behavior * and should be avoided. * @@ -308,10 +308,15 @@ import java.util.Map; * WebView may upload anonymous diagnostic data to Google when the user has consented. This data * helps Google improve WebView. Data is collected on a per-app basis for each app which has * instantiated a WebView. An individual app can opt out of this feature by putting the following - * tag in its manifest: + * tag in its manifest's {@code <application>} element: * <pre> - * <meta-data android:name="android.webkit.WebView.MetricsOptOut" - * android:value="true" /> + * <manifest> + * <application> + * ... + * <meta-data android:name="android.webkit.WebView.MetricsOptOut" + * android:value="true" /> + * </application> + * </manifest> * </pre> * <p> * Data will only be uploaded for a given app if the user has consented AND the app has not opted @@ -323,11 +328,17 @@ import java.util.Map; * If Safe Browsing is enabled, WebView will block malicious URLs and present a warning UI to the * user to allow them to navigate back safely or proceed to the malicious page. * <p> - * The recommended way for apps to enable the feature is putting the following tag in the manifest: + * The recommended way for apps to enable the feature is putting the following tag in the manifest's + * {@code <application>} element: * <p> * <pre> - * <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" - * android:value="true" /> + * <manifest> + * <application> + * ... + * <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" + * android:value="true" /> + * </application> + * </manifest> * </pre> * */ @@ -536,9 +547,13 @@ public class WebView extends AbsoluteLayout } /** - * Constructs a new WebView with a Context object. + * Constructs a new WebView with an Activity Context object. * - * @param context a Context object used to access application assets + * <p class="note"><b>Note:</b> WebView should always be instantiated with an Activity Context. + * If instantiated with an Application Context, WebView will be unable to provide several + * features, such as JavaScript dialogs and autofill. + * + * @param context an Activity Context to access application assets */ public WebView(Context context) { this(context, null); @@ -547,7 +562,7 @@ public class WebView extends AbsoluteLayout /** * Constructs a new WebView with layout parameters. * - * @param context a Context object used to access application assets + * @param context an Activity Context to access application assets * @param attrs an AttributeSet passed to our parent */ public WebView(Context context, AttributeSet attrs) { @@ -557,7 +572,7 @@ public class WebView extends AbsoluteLayout /** * Constructs a new WebView with layout parameters and a default style. * - * @param context a Context object used to access application assets + * @param context an Activity Context to access application assets * @param attrs an AttributeSet passed to our parent * @param defStyleAttr an attribute in the current theme that contains a * reference to a style resource that supplies default values for @@ -570,7 +585,7 @@ public class WebView extends AbsoluteLayout /** * Constructs a new WebView with layout parameters and a default style. * - * @param context a Context object used to access application assets + * @param context an Activity Context to access application assets * @param attrs an AttributeSet passed to our parent * @param defStyleAttr an attribute in the current theme that contains a * reference to a style resource that supplies default values for @@ -587,7 +602,7 @@ public class WebView extends AbsoluteLayout /** * Constructs a new WebView with layout parameters and a default style. * - * @param context a Context object used to access application assets + * @param context an Activity Context to access application assets * @param attrs an AttributeSet passed to our parent * @param defStyleAttr an attribute in the current theme that contains a * reference to a style resource that supplies default values for @@ -612,7 +627,7 @@ public class WebView extends AbsoluteLayout * time. This guarantees that these interfaces will be available when the JS * context is initialized. * - * @param context a Context object used to access application assets + * @param context an Activity Context to access application assets * @param attrs an AttributeSet passed to our parent * @param defStyleAttr an attribute in the current theme that contains a * reference to a style resource that supplies default values for diff --git a/android/webkit/WebViewClient.java b/android/webkit/WebViewClient.java index c5b64eb8..517ad07c 100644 --- a/android/webkit/WebViewClient.java +++ b/android/webkit/WebViewClient.java @@ -130,7 +130,7 @@ public class WebViewClient { * <p>This method is called when the body of the HTTP response has started loading, is reflected * in the DOM, and will be visible in subsequent draws. This callback occurs early in the * document loading process, and as such you should expect that linked resources (for example, - * css and images) may not be available. + * CSS and images) may not be available. * * <p>For more fine-grained notification of visual state updates, see {@link * WebView#postVisualStateCallback}. @@ -150,13 +150,15 @@ public class WebViewClient { * Notify the host application of a resource request and allow the * application to return the data. If the return value is {@code null}, the WebView * will continue to load the resource as usual. Otherwise, the return - * response and data will be used. NOTE: This method is called on a thread + * response and data will be used. + * + * <p class="note"><b>Note:</b> This method is called on a thread * other than the UI thread so clients should exercise caution * when accessing private data or the view system. * - * <p>Note: when Safe Browsing is enabled, these URLs still undergo Safe Browsing checks. If - * this is undesired, whitelist the URL with {@link WebView#setSafeBrowsingWhitelist} or ignore - * the warning with {@link #onSafeBrowsingHit}. + * <p class="note"><b>Note:</b> When Safe Browsing is enabled, these URLs still undergo Safe + * Browsing checks. If this is undesired, whitelist the URL with {@link + * WebView#setSafeBrowsingWhitelist} or ignore the warning with {@link #onSafeBrowsingHit}. * * @param view The {@link android.webkit.WebView} that is requesting the * resource. @@ -178,13 +180,15 @@ public class WebViewClient { * Notify the host application of a resource request and allow the * application to return the data. If the return value is {@code null}, the WebView * will continue to load the resource as usual. Otherwise, the return - * response and data will be used. NOTE: This method is called on a thread + * response and data will be used. + * + * <p class="note"><b>Note:</b> This method is called on a thread * other than the UI thread so clients should exercise caution * when accessing private data or the view system. * - * <p>Note: when Safe Browsing is enabled, these URLs still undergo Safe Browsing checks. If - * this is undesired, whitelist the URL with {@link WebView#setSafeBrowsingWhitelist} or ignore - * the warning with {@link #onSafeBrowsingHit}. + * <p class="note"><b>Note:</b> When Safe Browsing is enabled, these URLs still undergo Safe + * Browsing checks. If this is undesired, whitelist the URL with {@link + * WebView#setSafeBrowsingWhitelist} or ignore the warning with {@link #onSafeBrowsingHit}. * * @param view The {@link android.webkit.WebView} that is requesting the * resource. @@ -248,7 +252,7 @@ public class WebViewClient { public static final int ERROR_FILE_NOT_FOUND = -14; /** Too many requests during this load */ public static final int ERROR_TOO_MANY_REQUESTS = -15; - /** Resource load was cancelled by Safe Browsing */ + /** Resource load was canceled by Safe Browsing */ public static final int ERROR_UNSAFE_RESOURCE = -16; /** @hide */ @@ -272,8 +276,8 @@ public class WebViewClient { /** * Report an error to the host application. These errors are unrecoverable - * (i.e. the main resource is unavailable). The errorCode parameter - * corresponds to one of the ERROR_* constants. + * (i.e. the main resource is unavailable). The {@code errorCode} parameter + * corresponds to one of the {@code ERROR_*} constants. * @param view The WebView that is initiating the callback. * @param errorCode The error code corresponding to an ERROR_* value. * @param description A String describing the error. @@ -289,11 +293,11 @@ public class WebViewClient { /** * Report web resource loading error to the host application. These errors usually indicate * inability to connect to the server. Note that unlike the deprecated version of the callback, - * the new version will be called for any resource (iframe, image, etc), not just for the main + * the new version will be called for any resource (iframe, image, etc.), not just for the main * page. Thus, it is recommended to perform minimum required work in this callback. * @param view The WebView that is initiating the callback. * @param request The originating request. - * @param error Information about the error occured. + * @param error Information about the error occurred. */ public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { if (request.isForMainFrame()) { @@ -306,12 +310,12 @@ public class WebViewClient { /** * Notify the host application that an HTTP error has been received from the server while * loading a resource. HTTP errors have status codes >= 400. This callback will be called - * for any resource (iframe, image, etc), not just for the main page. Thus, it is recommended to - * perform minimum required work in this callback. Note that the content of the server - * response may not be provided within the <b>errorResponse</b> parameter. + * for any resource (iframe, image, etc.), not just for the main page. Thus, it is recommended + * to perform minimum required work in this callback. Note that the content of the server + * response may not be provided within the {@code errorResponse} parameter. * @param view The WebView that is initiating the callback. * @param request The originating request. - * @param errorResponse Information about the error occured. + * @param errorResponse Information about the error occurred. */ public void onReceivedHttpError( WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { @@ -365,7 +369,7 @@ public class WebViewClient { * if desired and providing the keys. There are three ways to * respond: proceed(), cancel() or ignore(). Webview stores the response * in memory (for the life of the application) if proceed() or cancel() is - * called and does not call onReceivedClientCertRequest() again for the + * called and does not call {@code onReceivedClientCertRequest()} again for the * same host and port pair. Webview does not store the response if ignore() * is called. Note that, multiple layers in chromium network stack might be * caching the responses, so the behavior for ignore is only a best case @@ -432,7 +436,7 @@ public class WebViewClient { /** * Notify the host application that a key was not handled by the WebView. * Except system keys, WebView always consumes the keys in the normal flow - * or if shouldOverrideKeyEvent returns {@code true}. This is called asynchronously + * or if {@link #shouldOverrideKeyEvent} returns {@code true}. This is called asynchronously * from where the key is dispatched. It gives the host application a chance * to handle the unhandled key events. * @@ -446,7 +450,7 @@ public class WebViewClient { /** * Notify the host application that a input event was not handled by the WebView. * Except system keys, WebView always consumes input events in the normal flow - * or if shouldOverrideKeyEvent returns {@code true}. This is called asynchronously + * or if {@link #shouldOverrideKeyEvent} returns {@code true}. This is called asynchronously * from where the event is dispatched. It gives the host application a chance * to handle the unhandled input events. * @@ -503,7 +507,7 @@ public class WebViewClient { } /** - * Notify host application that the given webview's render process has exited. + * Notify host application that the given WebView's render process has exited. * * Multiple WebView instances may be associated with a single render process; * onRenderProcessGone will be called for each WebView that was affected. @@ -513,10 +517,10 @@ public class WebViewClient { * * The given WebView can't be used, and should be removed from the view hierarchy, * all references to it should be cleaned up, e.g any references in the Activity - * or other classes saved using findViewById and similar calls, etc + * or other classes saved using {@link android.view.View#findViewById} and similar calls, etc. * * To cause an render process crash for test purpose, the application can - * call loadUrl("chrome://crash") on the WebView. Note that multiple WebView + * call {@code loadUrl("chrome://crash")} on the WebView. Note that multiple WebView * instances may be affected if they share a render process, not just the * specific WebView which loaded chrome://crash. * @@ -537,12 +541,13 @@ public class WebViewClient { * behavior is to show an interstitial to the user, with the reporting checkbox visible. * * If the application needs to show its own custom interstitial UI, the callback can be invoked - * asynchronously with backToSafety() or proceed(), depending on user response. + * asynchronously with {@link SafeBrowsingResponse#backToSafety} or {@link + * SafeBrowsingResponse#proceed}, depending on user response. * * @param view The WebView that hit the malicious resource. * @param request Object containing the details of the request. * @param threatType The reason the resource was caught by Safe Browsing, corresponding to a - * SAFE_BROWSING_THREAT_* value. + * {@code SAFE_BROWSING_THREAT_*} value. * @param callback Applications must invoke one of the callback methods. */ public void onSafeBrowsingHit(WebView view, WebResourceRequest request, diff --git a/android/webkit/WebViewFragment.java b/android/webkit/WebViewFragment.java index d803f62d..e5b7c8d2 100644 --- a/android/webkit/WebViewFragment.java +++ b/android/webkit/WebViewFragment.java @@ -27,7 +27,10 @@ import android.webkit.WebView; * A fragment that displays a WebView. * <p> * The WebView is automically paused or resumed when the Fragment is paused or resumed. + * + * @deprecated Manually call {@link WebView#onPause()} and {@link WebView#onResume()} */ +@Deprecated public class WebViewFragment extends Fragment { private WebView mWebView; private boolean mIsWebViewAvailable; diff --git a/android/webkit/WebViewLibraryLoader.java b/android/webkit/WebViewLibraryLoader.java index 341c69fd..de0b97d1 100644 --- a/android/webkit/WebViewLibraryLoader.java +++ b/android/webkit/WebViewLibraryLoader.java @@ -229,7 +229,9 @@ public class WebViewLibraryLoader { /** * Load WebView's native library into the current process. - * Note: assumes that we have waited for relro creation. + * + * <p class="note"><b>Note:</b> Assumes that we have waited for relro creation. + * * @param clazzLoader class loader used to find the linker namespace to load the library into. * @param packageInfo the package from which WebView is loaded. */ diff --git a/android/webkit/WebViewProvider.java b/android/webkit/WebViewProvider.java index c46c681c..a8969252 100644 --- a/android/webkit/WebViewProvider.java +++ b/android/webkit/WebViewProvider.java @@ -316,7 +316,7 @@ public interface WebViewProvider { /** * Provides mechanism for the name-sake methods declared in View and ViewGroup to be delegated * into the WebViewProvider instance. - * NOTE For many of these methods, the WebView will provide a super.Foo() call before or after + * NOTE: For many of these methods, the WebView will provide a super.Foo() call before or after * making the call into the provider instance. This is done for convenience in the common case * of maintaining backward compatibility. For remaining super class calls (e.g. where the * provider may need to only conditionally make the call based on some internal state) see the diff --git a/android/widget/AbsListView.java b/android/widget/AbsListView.java index 170582b3..e0c897d3 100644 --- a/android/widget/AbsListView.java +++ b/android/widget/AbsListView.java @@ -3866,6 +3866,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private void onTouchDown(MotionEvent ev) { mHasPerformedLongPress = false; mActivePointerId = ev.getPointerId(0); + hideSelector(); if (mTouchMode == TOUCH_MODE_OVERFLING) { // Stopped the fling. It is a scroll. @@ -5226,17 +5227,21 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } mRecycler.fullyDetachScrapViews(); + boolean selectorOnScreen = false; if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { final int childIndex = mSelectedPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(mSelectedPosition, getChildAt(childIndex)); + selectorOnScreen = true; } } else if (mSelectorPosition != INVALID_POSITION) { final int childIndex = mSelectorPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { - positionSelector(INVALID_POSITION, getChildAt(childIndex)); + positionSelector(mSelectorPosition, getChildAt(childIndex)); + selectorOnScreen = true; } - } else { + } + if (!selectorOnScreen) { mSelectorRect.setEmpty(); } diff --git a/android/widget/Editor.java b/android/widget/Editor.java index e6da69dc..d477ffdf 100644 --- a/android/widget/Editor.java +++ b/android/widget/Editor.java @@ -3842,14 +3842,10 @@ public class Editor { mProcessTextIntentActionsHandler.onInitializeMenu(menu); } - if (menu.hasVisibleItems() || mode.getCustomView() != null) { - if (mHasSelection && !mTextView.hasTransientState()) { - mTextView.setHasTransientState(true); - } - return true; - } else { - return false; + if (mHasSelection && !mTextView.hasTransientState()) { + mTextView.setHasTransientState(true); } + return true; } private Callback getCustomCallback() { @@ -6557,12 +6553,12 @@ public class Editor { * Adds "PROCESS_TEXT" menu items to the specified menu. */ public void onInitializeMenu(Menu menu) { - final int size = mSupportedActivities.size(); loadSupportedActivities(); + final int size = mSupportedActivities.size(); for (int i = 0; i < size; i++) { final ResolveInfo resolveInfo = mSupportedActivities.get(i); menu.add(Menu.NONE, Menu.NONE, - Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++, + Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i, getLabel(resolveInfo)) .setIntent(createProcessTextIntentForResolveInfo(resolveInfo)) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); diff --git a/android/widget/PopupWindow.java b/android/widget/PopupWindow.java index 8dc8cab1..e91db139 100644 --- a/android/widget/PopupWindow.java +++ b/android/widget/PopupWindow.java @@ -1354,6 +1354,7 @@ public class PopupWindow { } mDecorView = createDecorView(mBackgroundView); + mDecorView.setIsRootNamespace(true); // The background owner should be elevated so that it casts a shadow. mBackgroundView.setElevation(mElevation); diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java index 5e22650a..d0ad27af 100644 --- a/android/widget/SelectionActionModeHelper.java +++ b/android/widget/SelectionActionModeHelper.java @@ -20,10 +20,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.annotation.WorkerThread; +import android.content.Context; import android.graphics.Canvas; import android.graphics.PointF; import android.graphics.RectF; import android.os.AsyncTask; +import android.os.Build; import android.os.LocaleList; import android.text.Layout; import android.text.Selection; @@ -81,6 +83,7 @@ public final class SelectionActionModeHelper { mEditor = Preconditions.checkNotNull(editor); mTextView = mEditor.getTextView(); mTextClassificationHelper = new TextClassificationHelper( + mTextView.getContext(), mTextView.getTextClassifier(), getText(mTextView), 0, 1, mTextView.getTextLocales()); @@ -385,6 +388,7 @@ public final class SelectionActionModeHelper { private void resetTextClassificationHelper() { mTextClassificationHelper.init( + mTextView.getContext(), mTextView.getTextClassifier(), getText(mTextView), mTextView.getSelectionStart(), mTextView.getSelectionEnd(), @@ -587,7 +591,9 @@ public final class SelectionActionModeHelper { Preconditions.checkNotNull(textView); final @SmartSelectionEventTracker.WidgetType int widgetType = textView.isTextEditable() ? SmartSelectionEventTracker.WidgetType.EDITTEXT - : SmartSelectionEventTracker.WidgetType.TEXTVIEW; + : (textView.isTextSelectable() + ? SmartSelectionEventTracker.WidgetType.TEXTVIEW + : SmartSelectionEventTracker.WidgetType.UNSELECTABLE_TEXTVIEW); mDelegate = new SmartSelectionEventTracker(textView.getContext(), widgetType); mEditTextLogger = textView.isTextEditable(); mWordIterator = BreakIterator.getWordInstance(textView.getTextLocale()); @@ -787,6 +793,7 @@ public final class SelectionActionModeHelper { private static final int TRIM_DELTA = 120; // characters + private Context mContext; private TextClassifier mTextClassifier; /** The original TextView text. **/ @@ -795,7 +802,10 @@ public final class SelectionActionModeHelper { private int mSelectionStart; /** End index relative to mText. */ private int mSelectionEnd; - private LocaleList mLocales; + + private final TextSelection.Options mSelectionOptions = new TextSelection.Options(); + private final TextClassification.Options mClassificationOptions = + new TextClassification.Options(); /** Trimmed text starting from mTrimStart in mText. */ private CharSequence mTrimmedText; @@ -816,21 +826,24 @@ public final class SelectionActionModeHelper { /** Whether the TextClassifier has been initialized. */ private boolean mHot; - TextClassificationHelper(TextClassifier textClassifier, + TextClassificationHelper(Context context, TextClassifier textClassifier, CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) { - init(textClassifier, text, selectionStart, selectionEnd, locales); + init(context, textClassifier, text, selectionStart, selectionEnd, locales); } @UiThread - public void init(TextClassifier textClassifier, + public void init(Context context, TextClassifier textClassifier, CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) { + mContext = Preconditions.checkNotNull(context); mTextClassifier = Preconditions.checkNotNull(textClassifier); mText = Preconditions.checkNotNull(text).toString(); mLastClassificationText = null; // invalidate. Preconditions.checkArgument(selectionEnd > selectionStart); mSelectionStart = selectionStart; mSelectionEnd = selectionEnd; - mLocales = locales; + mClassificationOptions.setDefaultLocales(locales); + mSelectionOptions.setDefaultLocales(locales) + .setDarkLaunchAllowed(true); } @WorkerThread @@ -843,8 +856,16 @@ public final class SelectionActionModeHelper { public SelectionResult suggestSelection() { mHot = true; trimText(); - final TextSelection selection = mTextClassifier.suggestSelection( - mTrimmedText, mRelativeStart, mRelativeEnd, mLocales); + final TextSelection selection; + if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) { + selection = mTextClassifier.suggestSelection( + mTrimmedText, mRelativeStart, mRelativeEnd, mSelectionOptions); + } else { + // Use old APIs. + selection = mTextClassifier.suggestSelection( + mTrimmedText, mRelativeStart, mRelativeEnd, + mSelectionOptions.getDefaultLocales()); + } // Do not classify new selection boundaries if TextClassifier should be dark launched. if (!mTextClassifier.getSettings().isDarkLaunch()) { mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart); @@ -874,20 +895,28 @@ public final class SelectionActionModeHelper { if (!Objects.equals(mText, mLastClassificationText) || mSelectionStart != mLastClassificationSelectionStart || mSelectionEnd != mLastClassificationSelectionEnd - || !Objects.equals(mLocales, mLastClassificationLocales)) { + || !Objects.equals( + mClassificationOptions.getDefaultLocales(), + mLastClassificationLocales)) { mLastClassificationText = mText; mLastClassificationSelectionStart = mSelectionStart; mLastClassificationSelectionEnd = mSelectionEnd; - mLastClassificationLocales = mLocales; + mLastClassificationLocales = mClassificationOptions.getDefaultLocales(); trimText(); + final TextClassification classification; + if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) { + classification = mTextClassifier.classifyText( + mTrimmedText, mRelativeStart, mRelativeEnd, mClassificationOptions); + } else { + // Use old APIs. + classification = mTextClassifier.classifyText( + mTrimmedText, mRelativeStart, mRelativeEnd, + mClassificationOptions.getDefaultLocales()); + } mLastClassificationResult = new SelectionResult( - mSelectionStart, - mSelectionEnd, - mTextClassifier.classifyText( - mTrimmedText, mRelativeStart, mRelativeEnd, mLocales), - selection); + mSelectionStart, mSelectionEnd, classification, selection); } return mLastClassificationResult; |