diff options
Diffstat (limited to 'core/java')
623 files changed, 43316 insertions, 14731 deletions
diff --git a/core/java/Android.bp b/core/java/Android.bp index 42b0f6bad0ae..b43cf277aa57 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -2,3 +2,32 @@ filegroup { name: "IKeyAttestationApplicationIdProvider.aidl", srcs: ["android/security/keymaster/IKeyAttestationApplicationIdProvider.aidl"], } + +// only used by key_store_service +cc_library_shared { + name: "libkeystore_aidl", + srcs: ["android/security/IKeystoreService.aidl"], + aidl: { + export_aidl_headers: true, + include_dirs: [ + "frameworks/base/core/java/", + "system/security/keystore/", + ], + }, + shared_libs: [ + "libbinder", + "libcutils", + "libhardware", + "libhidlbase", + "libhidltransport", + "libhwbinder", + "liblog", + "libkeystore_parcelables", + "libselinux", + "libutils", + ], + export_shared_lib_headers: [ + "libbinder", + "libkeystore_parcelables", + ], +} diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index a558d6850af1..8824643db447 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -358,6 +358,11 @@ public abstract class AccessibilityService extends Service { */ public static final int GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN = 7; + /** + * Action to lock the screen + */ + public static final int GLOBAL_ACTION_LOCK_SCREEN = 8; + private static final String LOG_TAG = "AccessibilityService"; /** diff --git a/core/java/android/accessibilityservice/GestureDescription.java b/core/java/android/accessibilityservice/GestureDescription.java index 92567d758856..56f4ae2b5832 100644 --- a/core/java/android/accessibilityservice/GestureDescription.java +++ b/core/java/android/accessibilityservice/GestureDescription.java @@ -428,6 +428,18 @@ public final class GestureDescription { } @Override + public String toString() { + return "TouchPoint{" + + "mStrokeId=" + mStrokeId + + ", mContinuedStrokeId=" + mContinuedStrokeId + + ", mIsStartOfPath=" + mIsStartOfPath + + ", mIsEndOfPath=" + mIsEndOfPath + + ", mX=" + mX + + ", mY=" + mY + + '}'; + } + + @Override public int describeContents() { return 0; } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 7a1931718888..037aeb058f15 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -35,23 +35,23 @@ interface IAccessibilityServiceConnection { void setServiceInfo(in AccessibilityServiceInfo info); - boolean findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, + String[] findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, long accessibilityNodeId, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, long threadId, in Bundle arguments); - boolean findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId, + String[] findAccessibilityNodeInfosByText(int accessibilityWindowId, long accessibilityNodeId, String text, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); - boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId, + String[] findAccessibilityNodeInfosByViewId(int accessibilityWindowId, long accessibilityNodeId, String viewId, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); - boolean findFocus(int accessibilityWindowId, long accessibilityNodeId, int focusType, + String[] findFocus(int accessibilityWindowId, long accessibilityNodeId, int focusType, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); - boolean focusSearch(int accessibilityWindowId, long accessibilityNodeId, int direction, + String[] focusSearch(int accessibilityWindowId, long accessibilityNodeId, int direction, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId, diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index ee89ca8d55e2..cc95eb6f4ea2 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/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/core/java/android/annotation/AnyThread.java b/core/java/android/annotation/AnyThread.java index e173bd144832..ee36a42b3fc6 100644 --- a/core/java/android/annotation/AnyThread.java +++ b/core/java/android/annotation/AnyThread.java @@ -17,6 +17,7 @@ package android.annotation; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -45,6 +46,6 @@ import java.lang.annotation.Target; * @hide */ @Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE}) +@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) public @interface AnyThread { } diff --git a/core/java/android/annotation/BinderThread.java b/core/java/android/annotation/BinderThread.java index 6f85e0457e0e..ca5e14c2adb9 100644 --- a/core/java/android/annotation/BinderThread.java +++ b/core/java/android/annotation/BinderThread.java @@ -20,6 +20,7 @@ import java.lang.annotation.Target; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -37,6 +38,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; * {@hide} */ @Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE}) +@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) public @interface BinderThread { }
\ No newline at end of file diff --git a/core/java/android/annotation/IntDef.java b/core/java/android/annotation/IntDef.java index 3f9064e4dd73..f84a67655dc3 100644 --- a/core/java/android/annotation/IntDef.java +++ b/core/java/android/annotation/IntDef.java @@ -52,10 +52,12 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; @Target({ANNOTATION_TYPE}) public @interface IntDef { /** Defines the constant prefix for this element */ - String[] prefix() default ""; + String[] prefix() default {}; + /** Defines the constant suffix for this element */ + String[] suffix() default {}; /** Defines the allowed constants for this element */ - long[] value() default {}; + int[] value() default {}; /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ boolean flag() default false; diff --git a/core/java/android/annotation/LongDef.java b/core/java/android/annotation/LongDef.java new file mode 100644 index 000000000000..8723eef86328 --- /dev/null +++ b/core/java/android/annotation/LongDef.java @@ -0,0 +1,62 @@ +/* + * 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that the annotated long element represents + * a logical type and that its value should be one of the explicitly + * named constants. If the {@link #flag()} attribute is set to true, + * multiple constants can be combined. + * <p> + * <pre><code> + * @Retention(SOURCE) + * @LongDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + * public @interface NavigationMode {} + * public static final long NAVIGATION_MODE_STANDARD = 0; + * public static final long NAVIGATION_MODE_LIST = 1; + * public static final long NAVIGATION_MODE_TABS = 2; + * ... + * public abstract void setNavigationMode(@NavigationMode long mode); + * @NavigationMode + * public abstract long getNavigationMode(); + * </code></pre> + * For a flag, set the flag attribute: + * <pre><code> + * @LongDef( + * flag = true, + * value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + * </code></pre> + * + * @hide + */ +@Retention(SOURCE) +@Target({ANNOTATION_TYPE}) +public @interface LongDef { + /** Defines the constant prefix for this element */ + String[] prefix() default ""; + + /** Defines the allowed constants for this element */ + long[] value() default {}; + + /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ + boolean flag() default false; +} diff --git a/core/java/android/annotation/MainThread.java b/core/java/android/annotation/MainThread.java index d15cfcd94ba7..556fdb4e7742 100644 --- a/core/java/android/annotation/MainThread.java +++ b/core/java/android/annotation/MainThread.java @@ -17,6 +17,7 @@ package android.annotation; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -44,6 +45,6 @@ import java.lang.annotation.Target; * @hide */ @Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE}) +@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) public @interface MainThread { } diff --git a/core/java/android/annotation/NavigationRes.java b/core/java/android/annotation/NavigationRes.java new file mode 100644 index 000000000000..3af5ecff84a6 --- /dev/null +++ b/core/java/android/annotation/NavigationRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a navigation resource reference (e.g. {@code R.navigation.flow}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface NavigationRes { +} diff --git a/core/java/android/annotation/StringDef.java b/core/java/android/annotation/StringDef.java index d5157c3a1562..a37535b9c98e 100644 --- a/core/java/android/annotation/StringDef.java +++ b/core/java/android/annotation/StringDef.java @@ -46,6 +46,11 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; @Retention(SOURCE) @Target({ANNOTATION_TYPE}) public @interface StringDef { + /** Defines the constant prefix for this element */ + String[] prefix() default {}; + /** Defines the constant suffix for this element */ + String[] suffix() default {}; + /** Defines the allowed constants for this element */ String[] value() default {}; } diff --git a/core/java/android/annotation/UiThread.java b/core/java/android/annotation/UiThread.java index b2778966dacb..6d7eedc7d2e2 100644 --- a/core/java/android/annotation/UiThread.java +++ b/core/java/android/annotation/UiThread.java @@ -17,6 +17,7 @@ package android.annotation; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -45,6 +46,6 @@ import java.lang.annotation.Target; * @hide */ @Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE}) +@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) public @interface UiThread { } diff --git a/core/java/android/annotation/WorkerThread.java b/core/java/android/annotation/WorkerThread.java index 3eae7aa9635b..8c2a4d381ab1 100644 --- a/core/java/android/annotation/WorkerThread.java +++ b/core/java/android/annotation/WorkerThread.java @@ -20,6 +20,7 @@ import java.lang.annotation.Target; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -42,6 +43,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; * @hide */ @Retention(SOURCE) -@Target({METHOD,CONSTRUCTOR,TYPE}) +@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER}) public @interface WorkerThread { } diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index d31e6326670d..aa099eb19d37 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -134,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 @@ -192,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> @@ -914,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(); } @@ -988,6 +995,7 @@ public class Activity extends ContextThemeWrapper @CallSuper protected void onCreate(@Nullable Bundle savedInstanceState) { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState); + if (mLastNonConfigurationInstances != null) { mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders); } @@ -1865,7 +1873,7 @@ public class Activity extends ContextThemeWrapper if (isFinishing()) { if (mAutoFillResetNeeded) { - getAutofillManager().commit(); + getAutofillManager().onActivityFinished(); } else if (mIntent != null && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { // Activity was launched when user tapped a link in the Autofill Save UI - since @@ -2393,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(); } @@ -2402,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) { } @@ -3212,9 +3227,8 @@ public class Activity extends ContextThemeWrapper /** - * Moves the activity from - * {@link android.app.ActivityManager.StackId#FREEFORM_WORKSPACE_STACK_ID} to - * {@link android.app.ActivityManager.StackId#FULLSCREEN_WORKSPACE_STACK_ID} stack. + * Moves the activity from {@link WindowConfiguration#WINDOWING_MODE_FREEFORM} windowing mode to + * {@link WindowConfiguration#WINDOWING_MODE_FULLSCREEN}. * * @hide */ @@ -3223,14 +3237,6 @@ public class Activity extends ContextThemeWrapper ActivityManager.getService().exitFreeformMode(mToken); } - /** Returns the current stack Id for the window. - * @hide - */ - @Override - public int getWindowStackId() throws RemoteException { - return ActivityManager.getService().getActivityStackId(mToken); - } - /** * Puts the activity in picture-in-picture mode if the activity supports. * @see android.R.attr#supportsPictureInPicture @@ -5113,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); @@ -5138,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); @@ -5862,10 +5876,11 @@ public class Activity extends ContextThemeWrapper } /** - * Returns complete component name of this activity. + * Returns the complete component name of this activity. * * @return Returns the complete component name for this activity */ + @Override public ComponentName getComponentName() { return mComponent; @@ -6254,6 +6269,8 @@ public class Activity extends ContextThemeWrapper final AutofillManager afm = getAutofillManager(); if (afm != null) { afm.dump(prefix, writer); + } else { + writer.print(prefix); writer.println("No AutofillManager"); } } @@ -7053,7 +7070,13 @@ public class Activity extends ContextThemeWrapper mActivityTransitionState.enterReady(this); } - final void performRestart() { + /** + * Restart the activity. + * @param start Indicates whether the activity should also be started after restart. + * The option to not start immediately is needed in case a transaction with + * multiple lifecycle transitions is in progress. + */ + final void performRestart(boolean start) { mCanEnterPictureInPicture = true; mFragments.noteStateNotSaved(); @@ -7091,12 +7114,14 @@ public class Activity extends ContextThemeWrapper "Activity " + mComponent.toShortString() + " did not call through to super.onRestart()"); } - performStart(); + if (start) { + performStart(); + } } } final void performResume() { - performRestart(); + performRestart(true /* start */); mFragments.execPendingActions(); @@ -7296,24 +7321,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() { @@ -7324,25 +7350,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/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 11ef9f8243e6..e33b79e51fb6 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -17,6 +17,7 @@ package android.app; import android.Manifest; +import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -45,6 +46,7 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import android.os.BatteryStats; +import android.os.Binder; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -52,7 +54,6 @@ import android.os.Debug; import android.os.Handler; import android.os.IBinder; import android.os.Parcel; -import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.Process; import android.os.RemoteException; @@ -135,16 +136,6 @@ public class ActivityManager { private static final int FIRST_START_NON_FATAL_ERROR_CODE = 100; private static final int LAST_START_NON_FATAL_ERROR_CODE = 199; - /** - * System property to enable task snapshots. - * @hide - */ - public final static boolean ENABLE_TASK_SNAPSHOTS; - - static { - ENABLE_TASK_SNAPSHOTS = SystemProperties.getBoolean("persist.enable_task_snapshots", true); - } - static final class UidObserver extends IUidObserver.Stub { final OnUidImportanceListener mListener; final Context mContext; @@ -510,7 +501,7 @@ public class ActivityManager { public static final int PROCESS_STATE_SERVICE = 11; /** @hide Process is in the background running a receiver. Note that from the - * perspective of oom_adj receivers run at a higher foreground level, but for our + * perspective of oom_adj, receivers run at a higher foreground level, but for our * prioritization here that is not necessary and putting them below services means * many fewer changes in some process states as they receive broadcasts. */ public static final int PROCESS_STATE_RECEIVER = 12; @@ -528,11 +519,29 @@ public class ActivityManager { * process that contains activities. */ public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 16; + /** @hide Process is being cached for later use and has an activity that corresponds + * to an existing recent task. */ + public static final int PROCESS_STATE_CACHED_RECENT = 17; + /** @hide Process is being cached for later use and is empty. */ - public static final int PROCESS_STATE_CACHED_EMPTY = 17; + public static final int PROCESS_STATE_CACHED_EMPTY = 18; /** @hide Process does not exist. */ - public static final int PROCESS_STATE_NONEXISTENT = 18; + public static final int PROCESS_STATE_NONEXISTENT = 19; + + // NOTE: If PROCESS_STATEs are added or changed, then new fields must be added + // to frameworks/base/core/proto/android/app/activitymanager.proto and the following method must + // be updated to correctly map between them. + /** + * Maps ActivityManager.PROCESS_STATE_ values to ActivityManagerProto.ProcessState enum. + * + * @param amInt a process state of the form ActivityManager.PROCESS_STATE_ + * @return the value of the corresponding android.app.ActivityManagerProto's ProcessState enum. + * @hide + */ + public static final int processStateAmToProto(int amInt) { + return amInt * 100; + } /** @hide The lowest process state number */ public static final int MIN_PROCESS_STATE = PROCESS_STATE_PERSISTENT; @@ -665,289 +674,35 @@ public class ActivityManager { SystemProperties.getBoolean("debug.force_low_ram", false); /** @hide */ + @TestApi public static class StackId { - /** Invalid stack ID. */ - public static final int INVALID_STACK_ID = -1; - - /** First static stack ID. */ - public static final int FIRST_STATIC_STACK_ID = 0; - - /** Home activity stack ID. */ - public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID; - - /** ID of stack where fullscreen activities are normally launched into. */ - public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1; - - /** ID of stack where freeform/resized activities are normally launched into. */ - public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1; - - /** ID of stack that occupies a dedicated region of the screen. */ - public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1; - - /** ID of stack that always on top (always visible) when it exist. */ - public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1; - - /** ID of stack that contains the Recents activity. */ - public static final int RECENTS_STACK_ID = PINNED_STACK_ID + 1; - /** ID of stack that contains activities launched by the assistant. */ - public static final int ASSISTANT_STACK_ID = RECENTS_STACK_ID + 1; - - /** Last static stack stack ID. */ - public static final int LAST_STATIC_STACK_ID = ASSISTANT_STACK_ID; - - /** Start of ID range used by stacks that are created dynamically. */ - public static final int FIRST_DYNAMIC_STACK_ID = LAST_STATIC_STACK_ID + 1; - - public static boolean isStaticStack(int stackId) { - return stackId >= FIRST_STATIC_STACK_ID && stackId <= LAST_STATIC_STACK_ID; - } - - public static boolean isDynamicStack(int stackId) { - return stackId >= FIRST_DYNAMIC_STACK_ID; - } - - /** - * Returns true if the activities contained in the input stack display a shadow around - * their border. - */ - public static boolean hasWindowShadow(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID || stackId == PINNED_STACK_ID; - } - - /** - * Returns true if the activities contained in the input stack display a decor view. - */ - public static boolean hasWindowDecor(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID; - } - - /** - * Returns true if the tasks contained in the stack can be resized independently of the - * stack. - */ - public static boolean isTaskResizeAllowed(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID; + private StackId() { } - /** Returns true if the task bounds should persist across power cycles. */ - public static boolean persistTaskBounds(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID; - } - - /** - * Returns true if dynamic stacks are allowed to be visible behind the input stack. - */ - public static boolean isDynamicStacksVisibleBehindAllowed(int stackId) { - return stackId == PINNED_STACK_ID || stackId == ASSISTANT_STACK_ID; - } - - /** - * Returns true if we try to maintain focus in the current stack when the top activity - * finishes. - */ - public static boolean keepFocusInStackIfPossible(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID - || stackId == DOCKED_STACK_ID || stackId == PINNED_STACK_ID; - } - - /** - * Returns true if Stack size is affected by the docked stack changing size. - */ - public static boolean isResizeableByDockedStack(int stackId) { - return isStaticStack(stackId) && stackId != DOCKED_STACK_ID - && stackId != PINNED_STACK_ID && stackId != ASSISTANT_STACK_ID; - } - - /** - * Returns true if the size of tasks in the input stack are affected by the docked stack - * changing size. - */ - public static boolean isTaskResizeableByDockedStack(int stackId) { - return isStaticStack(stackId) && stackId != FREEFORM_WORKSPACE_STACK_ID - && stackId != DOCKED_STACK_ID && stackId != PINNED_STACK_ID - && stackId != ASSISTANT_STACK_ID; - } - - /** - * Returns true if the input stack is affected by drag resizing. - */ - public static boolean isStackAffectedByDragResizing(int stackId) { - return isStaticStack(stackId) && stackId != PINNED_STACK_ID - && stackId != ASSISTANT_STACK_ID; - } - - /** - * Returns true if the windows of tasks being moved to the target stack from the source - * stack should be replaced, meaning that window manager will keep the old window around - * until the new is ready. - */ - public static boolean replaceWindowsOnTaskMove(int sourceStackId, int targetStackId) { - return sourceStackId == FREEFORM_WORKSPACE_STACK_ID - || targetStackId == FREEFORM_WORKSPACE_STACK_ID; - } - - /** - * Return whether a stackId is a stack containing floating windows. Floating windows - * are laid out differently as they are allowed to extend past the display bounds - * without overscan insets. - */ - public static boolean tasksAreFloating(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID - || stackId == PINNED_STACK_ID; - } - - /** - * Return whether a stackId is a stack that be a backdrop to a translucent activity. These - * are generally fullscreen stacks. - */ - public static boolean isBackdropToTranslucentActivity(int stackId) { - return stackId == FULLSCREEN_WORKSPACE_STACK_ID - || stackId == ASSISTANT_STACK_ID; - } - - /** - * Returns true if animation specs should be constructed for app transition that moves - * the task to the specified stack. - */ - public static boolean useAnimationSpecForAppTransition(int stackId) { - // TODO: INVALID_STACK_ID is also animated because we don't persist stack id's across - // reboots. - return stackId == FREEFORM_WORKSPACE_STACK_ID - || stackId == FULLSCREEN_WORKSPACE_STACK_ID - || stackId == ASSISTANT_STACK_ID - || stackId == DOCKED_STACK_ID - || stackId == INVALID_STACK_ID; - } - - /** - * Returns true if the windows in the stack can receive input keys. - */ - public static boolean canReceiveKeys(int stackId) { - return stackId != PINNED_STACK_ID; - } - - /** - * Returns true if the stack can be visible above lockscreen. - */ - public static boolean isAllowedOverLockscreen(int stackId) { - return stackId == HOME_STACK_ID || stackId == FULLSCREEN_WORKSPACE_STACK_ID || - stackId == ASSISTANT_STACK_ID; - } - - /** - * Returns true if activities from stasks in the given {@param stackId} are allowed to - * enter picture-in-picture. - */ - public static boolean isAllowedToEnterPictureInPicture(int stackId) { - return stackId != HOME_STACK_ID && stackId != ASSISTANT_STACK_ID && - stackId != RECENTS_STACK_ID; - } - - public static boolean isAlwaysOnTop(int stackId) { - return stackId == PINNED_STACK_ID; - } - - /** - * Returns true if the top task in the task is allowed to return home when finished and - * there are other tasks in the stack. - */ - public static boolean allowTopTaskToReturnHome(int stackId) { - return stackId != PINNED_STACK_ID; - } - - /** - * Returns true if the stack should be resized to match the bounds specified by - * {@link ActivityOptions#setLaunchBounds} when launching an activity into the stack. - */ - public static boolean resizeStackWithLaunchBounds(int stackId) { - return stackId == PINNED_STACK_ID; - } - - /** - * Returns true if any visible windows belonging to apps in this stack should be kept on - * screen when the app is killed due to something like the low memory killer. - */ - public static boolean keepVisibleDeadAppWindowOnScreen(int stackId) { - return stackId != PINNED_STACK_ID; - } - - /** - * Returns true if the backdrop on the client side should match the frame of the window. - * Returns false, if the backdrop should be fullscreen. - */ - public static boolean useWindowFrameForBackdrop(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID || stackId == PINNED_STACK_ID; - } - - /** - * Returns true if a window from the specified stack with {@param stackId} are normally - * fullscreen, i. e. they can become the top opaque fullscreen window, meaning that it - * controls system bars, lockscreen occluded/dismissing state, screen rotation animation, - * etc. - */ - public static boolean normallyFullscreenWindows(int stackId) { - return stackId != PINNED_STACK_ID && stackId != FREEFORM_WORKSPACE_STACK_ID - && stackId != DOCKED_STACK_ID; - } - - /** - * Returns true if the input stack id should only be present on a device that supports - * multi-window mode. - * @see android.app.ActivityManager#supportsMultiWindow - */ - public static boolean isMultiWindowStack(int stackId) { - return stackId == PINNED_STACK_ID || stackId == FREEFORM_WORKSPACE_STACK_ID - || stackId == DOCKED_STACK_ID; - } - - /** - * Returns true if the input {@param stackId} is HOME_STACK_ID or RECENTS_STACK_ID - */ - public static boolean isHomeOrRecentsStack(int stackId) { - return stackId == HOME_STACK_ID || stackId == RECENTS_STACK_ID; - } - - /** - * Returns true if this stack may be scaled without resizing, and windows within may need - * to be configured as such. - */ - public static boolean windowsAreScaleable(int stackId) { - return stackId == PINNED_STACK_ID; - } - - /** - * Returns true if windows in this stack should be given move animations by default. - */ - public static boolean hasMovementAnimations(int stackId) { - return stackId != PINNED_STACK_ID; - } + /** Invalid stack ID. */ + public static final int INVALID_STACK_ID = -1; - /** Returns true if the input stack and its content can affect the device orientation. */ - public static boolean canSpecifyOrientation(int stackId) { - return stackId == HOME_STACK_ID - || stackId == RECENTS_STACK_ID - || stackId == FULLSCREEN_WORKSPACE_STACK_ID - || stackId == ASSISTANT_STACK_ID - || isDynamicStack(stackId); - } } /** - * 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 @@ -1168,6 +923,7 @@ public class ActivityManager { * E.g. freeform, split-screen, picture-in-picture. * @hide */ + @TestApi static public boolean supportsMultiWindow(Context context) { // On watches, multi-window is used to present essential system UI, and thus it must be // supported regardless of device memory characteristics. @@ -1182,6 +938,7 @@ public class ActivityManager { * Returns true if the system supports split screen multi-window. * @hide */ + @TestApi static public boolean supportsSplitScreenMultiWindow(Context context) { return supportsMultiWindow(context) && Resources.getSystem().getBoolean( @@ -1206,11 +963,14 @@ public class ActivityManager { ATTR_TASKDESCRIPTION_PREFIX + "color"; private static final String ATTR_TASKDESCRIPTIONCOLOR_BACKGROUND = ATTR_TASKDESCRIPTION_PREFIX + "colorBackground"; - private static final String ATTR_TASKDESCRIPTIONICONFILENAME = + private static final String ATTR_TASKDESCRIPTIONICON_FILENAME = ATTR_TASKDESCRIPTION_PREFIX + "icon_filename"; + private static final String ATTR_TASKDESCRIPTIONICON_RESOURCE = + ATTR_TASKDESCRIPTION_PREFIX + "icon_resource"; private String mLabel; private Bitmap mIcon; + private int mIconRes; private String mIconFilename; private int mColorPrimary; private int mColorBackground; @@ -1224,9 +984,27 @@ public class ActivityManager { * @param icon An icon that represents the current state of this task. * @param colorPrimary A color to override the theme's primary color. This color must be * opaque. + * @deprecated use TaskDescription constructor with icon resource instead */ + @Deprecated public TaskDescription(String label, Bitmap icon, int colorPrimary) { - this(label, icon, null, colorPrimary, 0, 0, 0); + this(label, icon, 0, null, colorPrimary, 0, 0, 0); + if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { + throw new RuntimeException("A TaskDescription's primary color should be opaque"); + } + } + + /** + * Creates the TaskDescription to the specified values. + * + * @param label A label and description of the current state of this task. + * @param iconRes A drawable resource of an icon that represents the current state of this + * activity. + * @param colorPrimary A color to override the theme's primary color. This color must be + * opaque. + */ + public TaskDescription(String label, @DrawableRes int iconRes, int colorPrimary) { + this(label, null, iconRes, null, colorPrimary, 0, 0, 0); if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { throw new RuntimeException("A TaskDescription's primary color should be opaque"); } @@ -1237,9 +1015,22 @@ public class ActivityManager { * * @param label A label and description of the current state of this activity. * @param icon An icon that represents the current state of this activity. + * @deprecated use TaskDescription constructor with icon resource instead */ + @Deprecated public TaskDescription(String label, Bitmap icon) { - this(label, icon, null, 0, 0, 0, 0); + this(label, icon, 0, null, 0, 0, 0, 0); + } + + /** + * Creates the TaskDescription to the specified values. + * + * @param label A label and description of the current state of this activity. + * @param iconRes A drawable resource of an icon that represents the current state of this + * activity. + */ + public TaskDescription(String label, @DrawableRes int iconRes) { + this(label, null, iconRes, null, 0, 0, 0, 0); } /** @@ -1248,21 +1039,22 @@ public class ActivityManager { * @param label A label and description of the current state of this activity. */ public TaskDescription(String label) { - this(label, null, null, 0, 0, 0, 0); + this(label, null, 0, null, 0, 0, 0, 0); } /** * Creates an empty TaskDescription. */ public TaskDescription() { - this(null, null, null, 0, 0, 0, 0); + this(null, null, 0, null, 0, 0, 0, 0); } /** @hide */ - public TaskDescription(String label, Bitmap icon, String iconFilename, int colorPrimary, - int colorBackground, int statusBarColor, int navigationBarColor) { + public TaskDescription(String label, Bitmap bitmap, int iconRes, String iconFilename, + int colorPrimary, int colorBackground, int statusBarColor, int navigationBarColor) { mLabel = label; - mIcon = icon; + mIcon = bitmap; + mIconRes = iconRes; mIconFilename = iconFilename; mColorPrimary = colorPrimary; mColorBackground = colorBackground; @@ -1284,6 +1076,7 @@ public class ActivityManager { public void copyFrom(TaskDescription other) { mLabel = other.mLabel; mIcon = other.mIcon; + mIconRes = other.mIconRes; mIconFilename = other.mIconFilename; mColorPrimary = other.mColorPrimary; mColorBackground = other.mColorBackground; @@ -1299,6 +1092,7 @@ public class ActivityManager { public void copyFromPreserveHiddenFields(TaskDescription other) { mLabel = other.mLabel; mIcon = other.mIcon; + mIconRes = other.mIconRes; mIconFilename = other.mIconFilename; mColorPrimary = other.mColorPrimary; if (other.mColorBackground != 0) { @@ -1371,6 +1165,14 @@ public class ActivityManager { } /** + * Sets the icon resource for this task description. + * @hide + */ + public void setIcon(int iconRes) { + mIconRes = iconRes; + } + + /** * Moves the icon bitmap reference from an actual Bitmap to a file containing the * bitmap. * @hide @@ -1398,6 +1200,13 @@ public class ActivityManager { } /** @hide */ + @TestApi + public int getIconResource() { + return mIconRes; + } + + /** @hide */ + @TestApi public String getIconFilename() { return mIconFilename; } @@ -1463,7 +1272,10 @@ public class ActivityManager { Integer.toHexString(mColorBackground)); } if (mIconFilename != null) { - out.attribute(null, ATTR_TASKDESCRIPTIONICONFILENAME, mIconFilename); + out.attribute(null, ATTR_TASKDESCRIPTIONICON_FILENAME, mIconFilename); + } + if (mIconRes != 0) { + out.attribute(null, ATTR_TASKDESCRIPTIONICON_RESOURCE, Integer.toString(mIconRes)); } } @@ -1475,8 +1287,10 @@ public class ActivityManager { setPrimaryColor((int) Long.parseLong(attrValue, 16)); } else if (ATTR_TASKDESCRIPTIONCOLOR_BACKGROUND.equals(attrName)) { setBackgroundColor((int) Long.parseLong(attrValue, 16)); - } else if (ATTR_TASKDESCRIPTIONICONFILENAME.equals(attrName)) { + } else if (ATTR_TASKDESCRIPTIONICON_FILENAME.equals(attrName)) { setIconFilename(attrValue); + } else if (ATTR_TASKDESCRIPTIONICON_RESOURCE.equals(attrName)) { + setIcon(Integer.parseInt(attrValue, 10)); } } @@ -1499,6 +1313,7 @@ public class ActivityManager { dest.writeInt(1); mIcon.writeToParcel(dest, 0); } + dest.writeInt(mIconRes); dest.writeInt(mColorPrimary); dest.writeInt(mColorBackground); dest.writeInt(mStatusBarColor); @@ -1514,6 +1329,7 @@ public class ActivityManager { public void readFromParcel(Parcel source) { mLabel = source.readInt() > 0 ? source.readString() : null; mIcon = source.readInt() > 0 ? Bitmap.CREATOR.createFromParcel(source) : null; + mIconRes = source.readInt(); mColorPrimary = source.readInt(); mColorBackground = source.readInt(); mStatusBarColor = source.readInt(); @@ -1534,8 +1350,8 @@ public class ActivityManager { @Override public String toString() { return "TaskDescription Label: " + mLabel + " Icon: " + mIcon + - " IconFilename: " + mIconFilename + " colorPrimary: " + mColorPrimary + - " colorBackground: " + mColorBackground + + " IconRes: " + mIconRes + " IconFilename: " + mIconFilename + + " colorPrimary: " + mColorPrimary + " colorBackground: " + mColorBackground + " statusBarColor: " + mColorBackground + " navigationBarColor: " + mNavigationBarColor; } @@ -1661,6 +1477,12 @@ public class ActivityManager { */ public int resizeMode; + /** + * The current configuration this task is in. + * @hide + */ + final public Configuration configuration = new Configuration(); + public RecentTaskInfo() { } @@ -1691,7 +1513,6 @@ public class ActivityManager { } dest.writeInt(stackId); dest.writeInt(userId); - dest.writeLong(firstActiveTime); dest.writeLong(lastActiveTime); dest.writeInt(affiliatedTaskId); dest.writeInt(affiliatedTaskColor); @@ -1706,6 +1527,7 @@ public class ActivityManager { } dest.writeInt(supportsSplitScreenMultiWindow ? 1 : 0); dest.writeInt(resizeMode); + configuration.writeToParcel(dest, flags); } public void readFromParcel(Parcel source) { @@ -1719,7 +1541,6 @@ public class ActivityManager { TaskDescription.CREATOR.createFromParcel(source) : null; stackId = source.readInt(); userId = source.readInt(); - firstActiveTime = source.readLong(); lastActiveTime = source.readLong(); affiliatedTaskId = source.readInt(); affiliatedTaskColor = source.readInt(); @@ -1730,6 +1551,7 @@ public class ActivityManager { Rect.CREATOR.createFromParcel(source) : null; supportsSplitScreenMultiWindow = source.readInt() == 1; resizeMode = source.readInt(); + configuration.readFromParcel(source); } public static final Creator<RecentTaskInfo> CREATOR @@ -1761,31 +1583,6 @@ public class ActivityManager { public static final int RECENT_IGNORE_UNAVAILABLE = 0x0002; /** - * Provides a list that contains recent tasks for all - * profiles of a user. - * @hide - */ - public static final int RECENT_INCLUDE_PROFILES = 0x0004; - - /** - * Ignores all tasks that are on the home stack. - * @hide - */ - public static final int RECENT_IGNORE_HOME_AND_RECENTS_STACK_TASKS = 0x0008; - - /** - * Ignores the top task in the docked stack. - * @hide - */ - public static final int RECENT_INGORE_DOCKED_STACK_TOP_TASK = 0x0010; - - /** - * Ignores all tasks that are on the pinned stack. - * @hide - */ - public static final int RECENT_INGORE_PINNED_STACK_TASKS = 0x0020; - - /** * <p></p>Return a list of the tasks that the user has recently launched, with * the most recent being first and older ones after in order. * @@ -1820,33 +1617,10 @@ public class ActivityManager { public List<RecentTaskInfo> getRecentTasks(int maxNum, int flags) throws SecurityException { try { - return getService().getRecentTasks(maxNum, - flags, UserHandle.myUserId()).getList(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** - * Same as {@link #getRecentTasks(int, int)} but returns the recent tasks for a - * specific user. It requires holding - * the {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission. - * @param maxNum The maximum number of entries to return in the list. The - * actual number returned may be smaller, depending on how many tasks the - * user has started and the maximum number the system can remember. - * @param flags Information about what to return. May be any combination - * of {@link #RECENT_WITH_EXCLUDED} and {@link #RECENT_IGNORE_UNAVAILABLE}. - * - * @return Returns a list of RecentTaskInfo records describing each of - * the recent tasks. Most recently activated tasks go first. - * - * @hide - */ - public List<RecentTaskInfo> getRecentTasksForUser(int maxNum, int flags, int userId) - throws SecurityException { - try { - return getService().getRecentTasks(maxNum, - flags, userId).getList(); + if (maxNum < 0) { + throw new IllegalArgumentException("The requested number of tasks should be >= 0"); + } + return getService().getRecentTasks(maxNum, flags, UserHandle.myUserId()).getList(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1924,6 +1698,12 @@ public class ActivityManager { */ public int resizeMode; + /** + * The full configuration the task is currently running in. + * @hide + */ + final public Configuration configuration = new Configuration(); + public RunningTaskInfo() { } @@ -1948,6 +1728,7 @@ public class ActivityManager { dest.writeInt(numRunning); dest.writeInt(supportsSplitScreenMultiWindow ? 1 : 0); dest.writeInt(resizeMode); + configuration.writeToParcel(dest, flags); } public void readFromParcel(Parcel source) { @@ -1965,6 +1746,7 @@ public class ActivityManager { numRunning = source.readInt(); supportsSplitScreenMultiWindow = source.readInt() != 0; resizeMode = source.readInt(); + configuration.readFromParcel(source); } public static final Creator<RunningTaskInfo> CREATOR = new Creator<RunningTaskInfo>() { @@ -2124,185 +1906,106 @@ public class ActivityManager { public List<RunningTaskInfo> getRunningTasks(int maxNum) throws SecurityException { try { - return getService().getTasks(maxNum, 0); + return getService().getTasks(maxNum); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** - * Completely remove the given task. - * - * @param taskId Identifier of the task to be removed. - * @return Returns true if the given task was found and removed. - * + * Sets the windowing mode for a specific task. Only works on tasks of type + * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD} + * @param taskId The id of the task to set the windowing mode for. + * @param windowingMode The windowing mode to set for the task. + * @param toTop If the task should be moved to the top once the windowing mode changes. * @hide */ - public boolean removeTask(int taskId) throws SecurityException { + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void setTaskWindowingMode(int taskId, int windowingMode, boolean toTop) + throws SecurityException { try { - return getService().removeTask(taskId); + getService().setTaskWindowingMode(taskId, windowingMode, toTop); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** - * Metadata related to the {@link TaskThumbnail}. - * + * 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. + * @param showRecents If the recents activity should be shown on the other side of the task + * going into split-screen mode. * @hide */ - public static class TaskThumbnailInfo implements Parcelable { - /** @hide */ - public static final String ATTR_TASK_THUMBNAILINFO_PREFIX = "task_thumbnailinfo_"; - private static final String ATTR_TASK_WIDTH = - ATTR_TASK_THUMBNAILINFO_PREFIX + "task_width"; - private static final String ATTR_TASK_HEIGHT = - ATTR_TASK_THUMBNAILINFO_PREFIX + "task_height"; - private static final String ATTR_SCREEN_ORIENTATION = - ATTR_TASK_THUMBNAILINFO_PREFIX + "screen_orientation"; - - public int taskWidth; - public int taskHeight; - public int screenOrientation = Configuration.ORIENTATION_UNDEFINED; - - public TaskThumbnailInfo() { - // Do nothing - } - - private TaskThumbnailInfo(Parcel source) { - readFromParcel(source); - } - - /** - * Resets this info state to the initial state. - * @hide - */ - public void reset() { - taskWidth = 0; - taskHeight = 0; - screenOrientation = Configuration.ORIENTATION_UNDEFINED; - } - - /** - * Copies from another ThumbnailInfo. - */ - public void copyFrom(TaskThumbnailInfo o) { - taskWidth = o.taskWidth; - taskHeight = o.taskHeight; - screenOrientation = o.screenOrientation; - } - - /** @hide */ - public void saveToXml(XmlSerializer out) throws IOException { - out.attribute(null, ATTR_TASK_WIDTH, Integer.toString(taskWidth)); - out.attribute(null, ATTR_TASK_HEIGHT, Integer.toString(taskHeight)); - out.attribute(null, ATTR_SCREEN_ORIENTATION, Integer.toString(screenOrientation)); - } - - /** @hide */ - public void restoreFromXml(String attrName, String attrValue) { - if (ATTR_TASK_WIDTH.equals(attrName)) { - taskWidth = Integer.parseInt(attrValue); - } else if (ATTR_TASK_HEIGHT.equals(attrName)) { - taskHeight = Integer.parseInt(attrValue); - } else if (ATTR_SCREEN_ORIENTATION.equals(attrName)) { - screenOrientation = Integer.parseInt(attrValue); - } - } - - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(taskWidth); - dest.writeInt(taskHeight); - dest.writeInt(screenOrientation); - } - - public void readFromParcel(Parcel source) { - taskWidth = source.readInt(); - taskHeight = source.readInt(); - screenOrientation = source.readInt(); + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void setTaskWindowingModeSplitScreenPrimary(int taskId, int createMode, boolean toTop, + boolean animate, Rect initialBounds, boolean showRecents) throws SecurityException { + try { + getService().setTaskWindowingModeSplitScreenPrimary(taskId, createMode, toTop, animate, + initialBounds, showRecents); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - - public static final Creator<TaskThumbnailInfo> CREATOR = new Creator<TaskThumbnailInfo>() { - public TaskThumbnailInfo createFromParcel(Parcel source) { - return new TaskThumbnailInfo(source); - } - public TaskThumbnailInfo[] newArray(int size) { - return new TaskThumbnailInfo[size]; - } - }; } - /** @hide */ - public static class TaskThumbnail implements Parcelable { - public Bitmap mainThumbnail; - public ParcelFileDescriptor thumbnailFileDescriptor; - public TaskThumbnailInfo thumbnailInfo; - - public TaskThumbnail() { - } - - private TaskThumbnail(Parcel source) { - readFromParcel(source); - } - - public int describeContents() { - if (thumbnailFileDescriptor != null) { - return thumbnailFileDescriptor.describeContents(); - } - return 0; + /** + * 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. + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void resizeStack(int stackId, Rect bounds) throws SecurityException { + try { + getService().resizeStack(stackId, bounds, false /* allowResizeInDockedMode */, + false /* preserveWindows */, false /* animate */, -1 /* animationDuration */); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } + } - public void writeToParcel(Parcel dest, int flags) { - if (mainThumbnail != null) { - dest.writeInt(1); - mainThumbnail.writeToParcel(dest, flags); - } else { - dest.writeInt(0); - } - if (thumbnailFileDescriptor != null) { - dest.writeInt(1); - thumbnailFileDescriptor.writeToParcel(dest, flags); - } else { - dest.writeInt(0); - } - if (thumbnailInfo != null) { - dest.writeInt(1); - thumbnailInfo.writeToParcel(dest, flags); - } else { - dest.writeInt(0); - } + /** + * Removes stacks in the windowing modes from the system if they are of activity type + * ACTIVITY_TYPE_STANDARD or ACTIVITY_TYPE_UNDEFINED + * + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void removeStacksInWindowingModes(int[] windowingModes) throws SecurityException { + try { + getService().removeStacksInWindowingModes(windowingModes); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } + } - public void readFromParcel(Parcel source) { - if (source.readInt() != 0) { - mainThumbnail = Bitmap.CREATOR.createFromParcel(source); - } else { - mainThumbnail = null; - } - if (source.readInt() != 0) { - thumbnailFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(source); - } else { - thumbnailFileDescriptor = null; - } - if (source.readInt() != 0) { - thumbnailInfo = TaskThumbnailInfo.CREATOR.createFromParcel(source); - } else { - thumbnailInfo = null; - } + /** + * Removes stack of the activity types from the system. + * + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void removeStacksWithActivityTypes(int[] activityTypes) throws SecurityException { + try { + getService().removeStacksWithActivityTypes(activityTypes); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - - public static final Creator<TaskThumbnail> CREATOR = new Creator<TaskThumbnail>() { - public TaskThumbnail createFromParcel(Parcel source) { - return new TaskThumbnail(source); - } - public TaskThumbnail[] newArray(int size) { - return new TaskThumbnail[size]; - } - }; } /** @@ -2402,15 +2105,6 @@ public class ActivityManager { } /** @hide */ - public TaskThumbnail getTaskThumbnail(int id) throws SecurityException { - try { - return getService().getTaskThumbnail(id); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** @hide */ @IntDef(flag = true, prefix = { "MOVE_TASK_" }, value = { MOVE_TASK_WITH_HOME, MOVE_TASK_NO_USER_ACTION, @@ -2792,6 +2486,11 @@ public class ActivityManager { public boolean visible; // Index of the stack in the display's stack list, can be used for comparison of stack order public int position; + /** + * The full configuration the stack is currently running in. + * @hide + */ + final public Configuration configuration = new Configuration(); @Override public int describeContents() { @@ -2826,6 +2525,7 @@ public class ActivityManager { } else { dest.writeInt(0); } + configuration.writeToParcel(dest, flags); } public void readFromParcel(Parcel source) { @@ -2853,6 +2553,7 @@ public class ActivityManager { if (source.readInt() > 0) { topActivity = ComponentName.readFromParcel(source); } + configuration.readFromParcel(source); } public static final Creator<StackInfo> CREATOR = new Creator<StackInfo>() { @@ -2880,6 +2581,8 @@ public class ActivityManager { sb.append(" displayId="); sb.append(displayId); sb.append(" userId="); sb.append(userId); sb.append("\n"); + sb.append(" configuration="); sb.append(configuration); + sb.append("\n"); prefix = prefix + " "; for (int i = 0; i < taskIds.length; ++i) { sb.append(prefix); sb.append("taskId="); sb.append(taskIds[i]); @@ -2910,7 +2613,7 @@ public class ActivityManager { Manifest.permission.ACCESS_INSTANT_APPS}) public boolean clearApplicationUserData(String packageName, IPackageDataObserver observer) { try { - return getService().clearApplicationUserData(packageName, + return getService().clearApplicationUserData(packageName, false, observer, UserHandle.myUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -2922,7 +2625,8 @@ public class ActivityManager { * the user choosing to clear the app's data from within the device settings UI. It * erases all dynamic data associated with the app -- its private data and data in its * private area on external storage -- but does not remove the installed application - * itself, nor any OBB files. + * itself, nor any OBB files. It also revokes all runtime permissions that the app has acquired, + * clears all notifications and removes all Uri grants related to this application. * * @return {@code true} if the application successfully requested that the application's * data be erased; {@code false} otherwise. @@ -3240,10 +2944,10 @@ public class ActivityManager { /** * Constant for {@link #importance}: This process is running an * application that can not save its state, and thus can't be killed - * while in the background. - * @hide + * while in the background. This will be used with apps that have + * {@link android.R.attr#cantSaveState} set on their application tag. */ - public static final int IMPORTANCE_CANT_SAVE_STATE= 270; + public static final int IMPORTANCE_CANT_SAVE_STATE = 270; /** * Constant for {@link #importance}: This process is contains services @@ -3291,7 +2995,7 @@ public class ActivityManager { return IMPORTANCE_CACHED; } else if (procState >= PROCESS_STATE_SERVICE) { return IMPORTANCE_SERVICE; - } else if (procState > PROCESS_STATE_HEAVY_WEIGHT) { + } else if (procState == PROCESS_STATE_HEAVY_WEIGHT) { return IMPORTANCE_CANT_SAVE_STATE; } else if (procState >= PROCESS_STATE_TRANSIENT_BACKGROUND) { return IMPORTANCE_PERCEPTIBLE; @@ -3347,7 +3051,7 @@ public class ActivityManager { return PROCESS_STATE_HOME; } else if (importance >= IMPORTANCE_SERVICE) { return PROCESS_STATE_SERVICE; - } else if (importance > IMPORTANCE_CANT_SAVE_STATE) { + } else if (importance == IMPORTANCE_CANT_SAVE_STATE) { return PROCESS_STATE_HEAVY_WEIGHT; } else if (importance >= IMPORTANCE_PERCEPTIBLE) { return PROCESS_STATE_TRANSIENT_BACKGROUND; @@ -4231,21 +3935,36 @@ public class ActivityManager { IBinder service = ServiceManager.checkService(name); if (service == null) { pw.println(" (Service not found)"); + pw.flush(); return; } - TransferPipe tp = null; - try { - pw.flush(); - tp = new TransferPipe(); - tp.setBufferPrefix(" "); - service.dumpAsync(tp.getWriteFd().getFileDescriptor(), args); - tp.go(fd, 10000); - } catch (Throwable e) { - if (tp != null) { - tp.kill(); + pw.flush(); + if (service instanceof Binder) { + // If this is a local object, it doesn't make sense to do an async dump with it, + // just directly dump. + try { + service.dump(fd, args); + } catch (Throwable e) { + pw.println("Failure dumping service:"); + e.printStackTrace(pw); + pw.flush(); + } + } else { + // Otherwise, it is remote, do the dump asynchronously to avoid blocking. + TransferPipe tp = null; + try { + pw.flush(); + tp = new TransferPipe(); + tp.setBufferPrefix(" "); + service.dumpAsync(tp.getWriteFd().getFileDescriptor(), args); + tp.go(fd, 10000); + } catch (Throwable e) { + if (tp != null) { + tp.kill(); + } + pw.println("Failure dumping service:"); + e.printStackTrace(pw); } - pw.println("Failure dumping service:"); - e.printStackTrace(pw); } } @@ -4295,28 +4014,6 @@ public class ActivityManager { } /** - * @hide - */ - public void startLockTaskMode(int taskId) { - try { - getService().startLockTaskModeById(taskId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** - * @hide - */ - public void stopLockTaskMode() { - try { - getService().stopLockTaskMode(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** * Return whether currently in lock task mode. When in this mode * no new tasks can be created or switched to. * diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index c8d983933fc6..a9c4d3705afc 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -40,12 +40,6 @@ import java.util.List; public abstract class ActivityManagerInternal { /** - * Type for {@link #notifyAppTransitionStarting}: The transition was started because we had - * the surface saved. - */ - public static final int APP_TRANSITION_SAVED_SURFACE = 0; - - /** * Type for {@link #notifyAppTransitionStarting}: The transition was started because we drew * the splash screen. */ @@ -70,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. */ @@ -268,4 +283,25 @@ public abstract class ActivityManagerInternal { * @param token The IApplicationToken for the activity */ public abstract void setFocusedActivity(IBinder token); + + /** + * Set a uid that is allowed to bypass stopped app switches, launching an app + * whenever it wants. + * + * @param type Type of the caller -- unique string the caller supplies to identify itself + * and disambiguate with other calles. + * @param uid The uid of the app to be allowed, or -1 to clear the uid for this type. + * @param userId The user it is allowed for. + */ + public abstract void setAllowAppSwitches(@NonNull String type, int uid, int userId); + + /** + * @return true if runtime was restarted, false if it's normal boot + */ + public abstract boolean isRuntimeRestarted(); + + /** + * Returns {@code true} if {@code uid} is running an activity from {@code packageName}. + */ + public abstract boolean hasRunningActivity(int uid, @Nullable String packageName); } diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 0bffc9e6cee5..4a21f5c424d5 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -16,12 +16,14 @@ package android.app; -import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; -import static android.app.ActivityManager.StackId.INVALID_STACK_ID; +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; import android.annotation.Nullable; import android.annotation.TestApi; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -157,6 +159,12 @@ public class ActivityOptions { private static final String KEY_ANIM_SPECS = "android:activity.animSpecs"; /** + * Whether the activity should be launched into LockTask mode. + * @see #setLockTaskMode(boolean) + */ + private static final String KEY_LOCK_TASK_MODE = "android:activity.lockTaskMode"; + + /** * The display id the activity should be launched into. * @see #setLaunchDisplayId(int) * @hide @@ -164,10 +172,16 @@ public class ActivityOptions { private static final String KEY_LAUNCH_DISPLAY_ID = "android.activity.launchDisplayId"; /** - * The stack id the activity should be launched into. + * The windowing mode the activity should be launched into. + * @hide + */ + private static final String KEY_LAUNCH_WINDOWING_MODE = "android.activity.windowingMode"; + + /** + * The activity type the activity should be launched as. * @hide */ - private static final String KEY_LAUNCH_STACK_ID = "android.activity.launchStackId"; + private static final String KEY_LAUNCH_ACTIVITY_TYPE = "android.activity.activityType"; /** * The task id the activity should be launched into. @@ -189,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 @@ -271,10 +286,14 @@ public class ActivityOptions { private int mResultCode; private int mExitCoordinatorIndex; private PendingIntent mUsageTimeReport; + private boolean mLockTaskMode = false; private int mLaunchDisplayId = INVALID_DISPLAY; - private int mLaunchStackId = INVALID_STACK_ID; + @WindowConfiguration.WindowingMode + private int mLaunchWindowingMode = WINDOWING_MODE_UNDEFINED; + @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; @@ -859,12 +878,15 @@ public class ActivityOptions { mExitCoordinatorIndex = opts.getInt(KEY_EXIT_COORDINATOR_INDEX); break; } + mLockTaskMode = opts.getBoolean(KEY_LOCK_TASK_MODE, false); mLaunchDisplayId = opts.getInt(KEY_LAUNCH_DISPLAY_ID, INVALID_DISPLAY); - mLaunchStackId = opts.getInt(KEY_LAUNCH_STACK_ID, INVALID_STACK_ID); + mLaunchWindowingMode = opts.getInt(KEY_LAUNCH_WINDOWING_MODE, WINDOWING_MODE_UNDEFINED); + mLaunchActivityType = opts.getInt(KEY_LAUNCH_ACTIVITY_TYPE, ACTIVITY_TYPE_UNDEFINED); 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)) { @@ -1044,6 +1066,37 @@ public class ActivityOptions { } /** + * Gets whether the activity is to be launched into LockTask mode. + * @return {@code true} if the activity is to be launched into LockTask mode. + * @see Activity#startLockTask() + * @see android.app.admin.DevicePolicyManager#setLockTaskPackages(ComponentName, String[]) + */ + public boolean getLockTaskMode() { + return mLockTaskMode; + } + + /** + * Sets whether the activity is to be launched into LockTask mode. + * + * Use this option to start an activity in LockTask mode. Note that only apps permitted by + * {@link android.app.admin.DevicePolicyManager} can run in LockTask mode. Therefore, if + * {@link android.app.admin.DevicePolicyManager#isLockTaskPermitted(String)} returns + * {@code false} for the package of the target activity, a {@link SecurityException} will be + * thrown during {@link Context#startActivity(Intent, Bundle)}. + * + * Defaults to {@code false} if not set. + * + * @param lockTaskMode {@code true} if the activity is to be launched into LockTask mode. + * @return {@code this} {@link ActivityOptions} instance. + * @see Activity#startLockTask() + * @see android.app.admin.DevicePolicyManager#setLockTaskPackages(ComponentName, String[]) + */ + public ActivityOptions setLockTaskMode(boolean lockTaskMode) { + mLockTaskMode = lockTaskMode; + return this; + } + + /** * Gets the id of the display where activity should be launched. * @return The id of the display where activity should be launched, * {@link android.view.Display#INVALID_DISPLAY} if not set. @@ -1070,14 +1123,34 @@ public class ActivityOptions { } /** @hide */ - public int getLaunchStackId() { - return mLaunchStackId; + public int getLaunchWindowingMode() { + return mLaunchWindowingMode; + } + + /** + * Sets the windowing mode the activity should launch into. If the input windowing mode is + * {@link android.app.WindowConfiguration#WINDOWING_MODE_SPLIT_SCREEN_SECONDARY} and the device + * isn't currently in split-screen windowing mode, then the activity will be launched in + * {@link android.app.WindowConfiguration#WINDOWING_MODE_FULLSCREEN} windowing mode. For clarity + * on this you can use + * {@link android.app.WindowConfiguration#WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY} + * + * @hide + */ + @TestApi + public void setLaunchWindowingMode(int windowingMode) { + mLaunchWindowingMode = windowingMode; + } + + /** @hide */ + public int getLaunchActivityType() { + return mLaunchActivityType; } /** @hide */ @TestApi - public void setLaunchStackId(int launchStackId) { - mLaunchStackId = launchStackId; + public void setLaunchActivityType(int activityType) { + mLaunchActivityType = activityType; } /** @@ -1123,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 */ @@ -1216,6 +1289,7 @@ public class ActivityOptions { mExitCoordinatorIndex = otherOptions.mExitCoordinatorIndex; break; } + mLockTaskMode = otherOptions.mLockTaskMode; mAnimSpecs = otherOptions.mAnimSpecs; mAnimationFinishedListener = otherOptions.mAnimationFinishedListener; mSpecsFuture = otherOptions.mSpecsFuture; @@ -1290,12 +1364,14 @@ public class ActivityOptions { b.putInt(KEY_EXIT_COORDINATOR_INDEX, mExitCoordinatorIndex); break; } + b.putBoolean(KEY_LOCK_TASK_MODE, mLockTaskMode); b.putInt(KEY_LAUNCH_DISPLAY_ID, mLaunchDisplayId); - b.putInt(KEY_LAUNCH_STACK_ID, mLaunchStackId); + b.putInt(KEY_LAUNCH_WINDOWING_MODE, mLaunchWindowingMode); + b.putInt(KEY_LAUNCH_ACTIVITY_TYPE, mLaunchActivityType); 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/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 45f7eba2af02..5369adf4aad1 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -16,6 +16,13 @@ package android.app; +import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY; +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME; +import static android.app.servertransaction.ActivityLifecycleItem.ON_START; +import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; +import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE; import static android.view.Display.INVALID_DISPLAY; import android.annotation.NonNull; @@ -23,6 +30,12 @@ import android.annotation.Nullable; import android.app.assist.AssistContent; import android.app.assist.AssistStructure; import android.app.backup.BackupAgent; +import android.app.servertransaction.ActivityLifecycleItem.LifecycleState; +import android.app.servertransaction.ActivityResultItem; +import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.PendingTransactionActions; +import android.app.servertransaction.PendingTransactionActions.StopInfo; +import android.app.servertransaction.TransactionExecutor; import android.content.BroadcastReceiver; import android.content.ComponentCallbacks2; import android.content.ComponentName; @@ -82,7 +95,6 @@ import android.os.StrictMode; import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; -import android.os.TransactionTooLargeException; import android.os.UserHandle; import android.provider.BlockedNumberContract; import android.provider.CalendarContract; @@ -100,12 +112,12 @@ import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.util.LogPrinter; -import android.util.LogWriter; import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Slog; import android.util.SparseIntArray; import android.util.SuperNotCalledException; +import android.util.proto.ProtoOutputStream; import android.view.ContextThemeWrapper; import android.view.Display; import android.view.ThreadedRenderer; @@ -119,6 +131,7 @@ import android.view.WindowManagerGlobal; import android.webkit.WebView; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IVoiceInteractor; import com.android.internal.content.ReferrerIntent; import com.android.internal.os.BinderInternal; @@ -126,9 +139,9 @@ import com.android.internal.os.RuntimeInit; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; -import com.android.internal.util.IndentingPrintWriter; import com.android.org.conscrypt.OpenSSLSocketImpl; import com.android.org.conscrypt.TrustedCertificateStore; +import com.android.server.am.proto.MemInfoProto; import dalvik.system.BaseDexClassLoader; import dalvik.system.CloseGuard; @@ -149,6 +162,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.InetAddress; import java.text.DateFormat; import java.util.ArrayList; @@ -173,7 +187,7 @@ final class RemoteServiceException extends AndroidRuntimeException { * * {@hide} */ -public final class ActivityThread { +public final class ActivityThread extends ClientTransactionHandler { /** @hide */ public static final String TAG = "ActivityThread"; private static final android.graphics.Bitmap.Config THUMBNAIL_FORMAT = Bitmap.Config.RGB_565; @@ -185,7 +199,7 @@ public final class ActivityThread { private static final boolean DEBUG_BACKUP = false; public static final boolean DEBUG_CONFIGURATION = false; private static final boolean DEBUG_SERVICE = false; - private static final boolean DEBUG_MEMORY_TRIM = false; + public static final boolean DEBUG_MEMORY_TRIM = false; private static final boolean DEBUG_PROVIDER = false; private static final boolean DEBUG_ORDER = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; @@ -201,10 +215,6 @@ public final class ActivityThread { /** Type for IActivityManager.serviceDoneExecuting: done stopping (destroying) service */ public static final int SERVICE_DONE_EXECUTING_STOP = 2; - // Details for pausing activity. - private static final int USER_LEAVING = 1; - private static final int DONT_REPORT = 2; - // Whether to invoke an activity callback after delivering new configuration. private static final boolean REPORT_TO_ACTIVITY = true; @@ -284,12 +294,8 @@ public final class ActivityThread { final ArrayList<ActivityClientRecord> mRelaunchingActivities = new ArrayList<>(); @GuardedBy("mResourcesManager") Configuration mPendingConfiguration = null; - // Because we merge activity relaunch operations we can't depend on the ordering provided by - // the handler messages. We need to introduce secondary ordering mechanism, which will allow - // us to drop certain events, if we know that they happened before relaunch we already executed. - // This represents the order of receiving the request from AM. - @GuardedBy("mResourcesManager") - int mLifecycleSeq = 0; + // An executor that performs multi-step transactions. + private final TransactionExecutor mTransactionExecutor = new TransactionExecutor(this); private final ResourcesManager mResourcesManager; @@ -337,8 +343,9 @@ public final class ActivityThread { Bundle mCoreSettings = null; - static final class ActivityClientRecord { - IBinder token; + /** Activity client record, used for bookkeeping for the real {@link Activity} instance. */ + public static final class ActivityClientRecord { + public IBinder token; int ident; Intent intent; String referrer; @@ -350,6 +357,7 @@ public final class ActivityThread { Activity parent; String embeddedID; Activity.NonConfigurationInstances lastNonConfigurationInstances; + // TODO(lifecycler): Use mLifecycleState instead. boolean paused; boolean stopped; boolean hideForNow; @@ -366,13 +374,13 @@ public final class ActivityThread { ActivityInfo activityInfo; CompatibilityInfo compatInfo; - LoadedApk packageInfo; + public LoadedApk packageInfo; List<ResultInfo> pendingResults; List<ReferrerIntent> pendingIntents; boolean startsNotResumed; - boolean isForward; + public final boolean isForward; int pendingConfigChanges; boolean onlyLocalRequest; @@ -380,15 +388,42 @@ public final class ActivityThread { WindowManager mPendingRemoveWindowManager; boolean mPreserveWindow; - // Set for relaunch requests, indicates the order number of the relaunch operation, so it - // can be compared with other lifecycle operations. - int relaunchSeq = 0; + @LifecycleState + private int mLifecycleState = PRE_ON_CREATE; - // Can only be accessed from the UI thread. This represents the latest processed message - // that is related to lifecycle events/ - int lastProcessedSeq = 0; + @VisibleForTesting + public ActivityClientRecord() { + this.isForward = false; + init(); + } - ActivityClientRecord() { + public ActivityClientRecord(IBinder token, Intent intent, int ident, + ActivityInfo info, Configuration overrideConfig, CompatibilityInfo compatInfo, + String referrer, IVoiceInteractor voiceInteractor, Bundle state, + PersistableBundle persistentState, List<ResultInfo> pendingResults, + List<ReferrerIntent> pendingNewIntents, boolean isForward, + ProfilerInfo profilerInfo, ClientTransactionHandler client) { + this.token = token; + this.ident = ident; + this.intent = intent; + this.referrer = referrer; + this.voiceInteractor = voiceInteractor; + this.activityInfo = info; + this.compatInfo = compatInfo; + this.state = state; + this.persistentState = persistentState; + this.pendingResults = pendingResults; + this.pendingIntents = pendingNewIntents; + this.isForward = isForward; + this.profilerInfo = profilerInfo; + this.overrideConfig = overrideConfig; + this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo, + compatInfo); + init(); + } + + /** Common initializer for all constructors. */ + private void init() { parent = null; embeddedID = null; paused = false; @@ -400,11 +435,43 @@ public final class ActivityThread { throw new IllegalStateException( "Received config update for non-existing activity"); } - activity.mMainThread.handleActivityConfigurationChanged( - new ActivityConfigChangeData(token, overrideConfig), newDisplayId); + activity.mMainThread.handleActivityConfigurationChanged(token, overrideConfig, + newDisplayId); }; } + /** Get the current lifecycle state. */ + public int getLifecycleState() { + return mLifecycleState; + } + + /** Update the current lifecycle state for internal bookkeeping. */ + public void setState(@LifecycleState int newLifecycleState) { + mLifecycleState = newLifecycleState; + switch (mLifecycleState) { + case ON_CREATE: + paused = true; + stopped = true; + break; + case ON_START: + paused = true; + stopped = false; + break; + case ON_RESUME: + paused = false; + stopped = false; + break; + case ON_PAUSE: + paused = true; + stopped = false; + break; + case ON_STOP: + paused = true; + stopped = true; + break; + } + } + public boolean isPreHoneycomb() { if (activity != null) { return activity.getApplicationInfo().targetSdkVersion @@ -468,16 +535,6 @@ public final class ActivityThread { } } - static final class NewIntentData { - List<ReferrerIntent> intents; - IBinder token; - boolean andPause; - public String toString() { - return "NewIntentData{intents=" + intents + " token=" + token - + " andPause=" + andPause +"}"; - } - } - static final class ReceiverData extends BroadcastReceiver.PendingResult { public ReceiverData(Intent intent, int resultCode, String resultData, Bundle resultExtras, boolean ordered, boolean sticky, IBinder token, int sendingUser) { @@ -643,14 +700,6 @@ public final class ActivityThread { String[] args; } - static final class ResultData { - IBinder token; - List<ResultInfo> results; - public String toString() { - return "ResultData{token=" + token + " results" + results + "}"; - } - } - static final class ContextCleanupInfo { ContextImpl context; String what; @@ -678,15 +727,6 @@ public final class ActivityThread { int flags; } - static final class ActivityConfigChangeData { - final IBinder activityToken; - final Configuration overrideConfig; - public ActivityConfigChangeData(IBinder token, Configuration config) { - activityToken = token; - overrideConfig = config; - } - } - private class ApplicationThread extends IApplicationThread.Stub { private static final String DB_INFO_FORMAT = " %8s %8s %14s %14s %s"; @@ -701,93 +741,10 @@ public final class ActivityThread { } } - public final void schedulePauseActivity(IBinder token, boolean finished, - boolean userLeaving, int configChanges, boolean dontReport) { - int seq = getLifecycleSeq(); - if (DEBUG_ORDER) Slog.d(TAG, "pauseActivity " + ActivityThread.this - + " operation received seq: " + seq); - sendMessage( - finished ? H.PAUSE_ACTIVITY_FINISHING : H.PAUSE_ACTIVITY, - token, - (userLeaving ? USER_LEAVING : 0) | (dontReport ? DONT_REPORT : 0), - configChanges, - seq); - } - - public final void scheduleStopActivity(IBinder token, boolean showWindow, - int configChanges) { - int seq = getLifecycleSeq(); - if (DEBUG_ORDER) Slog.d(TAG, "stopActivity " + ActivityThread.this - + " operation received seq: " + seq); - sendMessage( - showWindow ? H.STOP_ACTIVITY_SHOW : H.STOP_ACTIVITY_HIDE, - token, 0, configChanges, seq); - } - - public final void scheduleWindowVisibility(IBinder token, boolean showWindow) { - sendMessage( - showWindow ? H.SHOW_WINDOW : H.HIDE_WINDOW, - token); - } - public final void scheduleSleeping(IBinder token, boolean sleeping) { sendMessage(H.SLEEPING, token, sleeping ? 1 : 0); } - public final void scheduleResumeActivity(IBinder token, int processState, - boolean isForward, Bundle resumeArgs) { - int seq = getLifecycleSeq(); - if (DEBUG_ORDER) Slog.d(TAG, "resumeActivity " + ActivityThread.this - + " operation received seq: " + seq); - updateProcessState(processState, false); - sendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0, 0, seq); - } - - public final void scheduleSendResult(IBinder token, List<ResultInfo> results) { - ResultData res = new ResultData(); - res.token = token; - res.results = results; - sendMessage(H.SEND_RESULT, res); - } - - // we use token to identify this activity without having to send the - // activity itself back to the activity manager. (matters more with ipc) - @Override - public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, - ActivityInfo info, Configuration curConfig, Configuration overrideConfig, - CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, - int procState, Bundle state, PersistableBundle persistentState, - List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, - boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) { - - updateProcessState(procState, false); - - ActivityClientRecord r = new ActivityClientRecord(); - - r.token = token; - r.ident = ident; - r.intent = intent; - r.referrer = referrer; - r.voiceInteractor = voiceInteractor; - r.activityInfo = info; - r.compatInfo = compatInfo; - r.state = state; - r.persistentState = persistentState; - - r.pendingResults = pendingResults; - r.pendingIntents = pendingNewIntents; - - r.startsNotResumed = notResumed; - r.isForward = isForward; - - r.profilerInfo = profilerInfo; - - r.overrideConfig = overrideConfig; - updatePendingConfiguration(curConfig); - - sendMessage(H.LAUNCH_ACTIVITY, r); - } - @Override public final void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, @@ -797,22 +754,6 @@ public final class ActivityThread { configChanges, notResumed, config, overrideConfig, true, preserveWindow); } - public final void scheduleNewIntent( - List<ReferrerIntent> intents, IBinder token, boolean andPause) { - NewIntentData data = new NewIntentData(); - data.intents = intents; - data.token = token; - data.andPause = andPause; - - sendMessage(H.NEW_INTENT, data); - } - - public final void scheduleDestroyActivity(IBinder token, boolean finishing, - int configChanges) { - sendMessage(H.DESTROY_ACTIVITY, token, finishing ? 1 : 0, - configChanges); - } - public final void scheduleReceiver(Intent intent, ActivityInfo info, CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras, boolean sync, int sendingUser, int processState) { @@ -933,6 +874,13 @@ public final class ActivityThread { sendMessage(H.BIND_APPLICATION, data); } + public final void runIsolatedEntryPoint(String entryPoint, String[] entryPointArgs) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = entryPoint; + args.arg2 = entryPointArgs; + sendMessage(H.RUN_ISOLATED_ENTRY_POINT, args); + } + public final void scheduleExit() { sendMessage(H.EXIT_APPLICATION, null); } @@ -941,11 +889,6 @@ public final class ActivityThread { sendMessage(H.SUICIDE, null); } - public void scheduleConfigurationChanged(Configuration config) { - updatePendingConfiguration(config); - sendMessage(H.CONFIGURATION_CHANGED, config); - } - public void scheduleApplicationInfoChanged(ApplicationInfo ai) { sendMessage(H.APPLICATION_INFO_CHANGED, ai); } @@ -1008,20 +951,6 @@ public final class ActivityThread { } @Override - public void scheduleActivityConfigurationChanged( - IBinder token, Configuration overrideConfig) { - sendMessage(H.ACTIVITY_CONFIGURATION_CHANGED, - new ActivityConfigChangeData(token, overrideConfig)); - } - - @Override - public void scheduleActivityMovedToDisplay(IBinder token, int displayId, - Configuration overrideConfig) { - sendMessage(H.ACTIVITY_MOVED_TO_DISPLAY, - new ActivityConfigChangeData(token, overrideConfig), displayId); - } - - @Override public void profilerControl(boolean start, ProfilerInfo profilerInfo, int profileType) { sendMessage(H.PROFILER_CONTROL, profilerInfo, start ? 1 : 0, profileType); } @@ -1419,26 +1348,6 @@ public final class ActivityThread { } @Override - public void scheduleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode, - Configuration overrideConfig) throws RemoteException { - SomeArgs args = SomeArgs.obtain(); - args.arg1 = token; - args.arg2 = overrideConfig; - args.argi1 = isInMultiWindowMode ? 1 : 0; - sendMessage(H.MULTI_WINDOW_MODE_CHANGED, args); - } - - @Override - public void schedulePictureInPictureModeChanged(IBinder token, boolean isInPipMode, - Configuration overrideConfig) throws RemoteException { - SomeArgs args = SomeArgs.obtain(); - args.arg1 = token; - args.arg2 = overrideConfig; - args.argi1 = isInPipMode ? 1 : 0; - sendMessage(H.PICTURE_IN_PICTURE_MODE_CHANGED, args); - } - - @Override public void scheduleLocalVoiceInteractionStarted(IBinder token, IVoiceInteractor voiceInteractor) throws RemoteException { SomeArgs args = SomeArgs.obtain(); @@ -1451,28 +1360,26 @@ public final class ActivityThread { public void handleTrustStorageUpdate() { NetworkSecurityPolicy.getInstance().handleTrustStorageUpdate(); } - } - private int getLifecycleSeq() { - synchronized (mResourcesManager) { - return mLifecycleSeq++; + @Override + public void scheduleTransaction(ClientTransaction transaction) throws RemoteException { + ActivityThread.this.scheduleTransaction(transaction); } } - private class H extends Handler { - public static final int LAUNCH_ACTIVITY = 100; - public static final int PAUSE_ACTIVITY = 101; - public static final int PAUSE_ACTIVITY_FINISHING= 102; - public static final int STOP_ACTIVITY_SHOW = 103; - public static final int STOP_ACTIVITY_HIDE = 104; - public static final int SHOW_WINDOW = 105; - public static final int HIDE_WINDOW = 106; - public static final int RESUME_ACTIVITY = 107; - public static final int SEND_RESULT = 108; - public static final int DESTROY_ACTIVITY = 109; + @Override + public void updatePendingConfiguration(Configuration config) { + mAppThread.updatePendingConfiguration(config); + } + + @Override + public void updateProcessState(int processState, boolean fromIpc) { + mAppThread.updateProcessState(processState, fromIpc); + } + + class H extends Handler { public static final int BIND_APPLICATION = 110; public static final int EXIT_APPLICATION = 111; - public static final int NEW_INTENT = 112; public static final int RECEIVER = 113; public static final int CREATE_SERVICE = 114; public static final int SERVICE_ARGS = 115; @@ -1485,7 +1392,6 @@ public final class ActivityThread { public static final int UNBIND_SERVICE = 122; public static final int DUMP_SERVICE = 123; public static final int LOW_MEMORY = 124; - public static final int ACTIVITY_CONFIGURATION_CHANGED = 125; public static final int RELAUNCH_ACTIVITY = 126; public static final int PROFILER_CONTROL = 127; public static final int CREATE_BACKUP_AGENT = 128; @@ -1510,29 +1416,17 @@ public final class ActivityThread { public static final int ENTER_ANIMATION_COMPLETE = 149; public static final int START_BINDER_TRACKING = 150; public static final int STOP_BINDER_TRACKING_AND_DUMP = 151; - public static final int MULTI_WINDOW_MODE_CHANGED = 152; - public static final int PICTURE_IN_PICTURE_MODE_CHANGED = 153; public static final int LOCAL_VOICE_INTERACTION_STARTED = 154; public static final int ATTACH_AGENT = 155; public static final int APPLICATION_INFO_CHANGED = 156; - public static final int ACTIVITY_MOVED_TO_DISPLAY = 157; + public static final int RUN_ISOLATED_ENTRY_POINT = 158; + public static final int EXECUTE_TRANSACTION = 159; String codeToString(int code) { if (DEBUG_MESSAGES) { switch (code) { - case LAUNCH_ACTIVITY: return "LAUNCH_ACTIVITY"; - case PAUSE_ACTIVITY: return "PAUSE_ACTIVITY"; - case PAUSE_ACTIVITY_FINISHING: return "PAUSE_ACTIVITY_FINISHING"; - case STOP_ACTIVITY_SHOW: return "STOP_ACTIVITY_SHOW"; - case STOP_ACTIVITY_HIDE: return "STOP_ACTIVITY_HIDE"; - case SHOW_WINDOW: return "SHOW_WINDOW"; - case HIDE_WINDOW: return "HIDE_WINDOW"; - case RESUME_ACTIVITY: return "RESUME_ACTIVITY"; - case SEND_RESULT: return "SEND_RESULT"; - case DESTROY_ACTIVITY: return "DESTROY_ACTIVITY"; case BIND_APPLICATION: return "BIND_APPLICATION"; case EXIT_APPLICATION: return "EXIT_APPLICATION"; - case NEW_INTENT: return "NEW_INTENT"; case RECEIVER: return "RECEIVER"; case CREATE_SERVICE: return "CREATE_SERVICE"; case SERVICE_ARGS: return "SERVICE_ARGS"; @@ -1544,8 +1438,6 @@ public final class ActivityThread { case UNBIND_SERVICE: return "UNBIND_SERVICE"; case DUMP_SERVICE: return "DUMP_SERVICE"; case LOW_MEMORY: return "LOW_MEMORY"; - case ACTIVITY_CONFIGURATION_CHANGED: return "ACTIVITY_CONFIGURATION_CHANGED"; - case ACTIVITY_MOVED_TO_DISPLAY: return "ACTIVITY_MOVED_TO_DISPLAY"; case RELAUNCH_ACTIVITY: return "RELAUNCH_ACTIVITY"; case PROFILER_CONTROL: return "PROFILER_CONTROL"; case CREATE_BACKUP_AGENT: return "CREATE_BACKUP_AGENT"; @@ -1568,11 +1460,11 @@ public final class ActivityThread { case INSTALL_PROVIDER: return "INSTALL_PROVIDER"; case ON_NEW_ACTIVITY_OPTIONS: return "ON_NEW_ACTIVITY_OPTIONS"; case ENTER_ANIMATION_COMPLETE: return "ENTER_ANIMATION_COMPLETE"; - case MULTI_WINDOW_MODE_CHANGED: return "MULTI_WINDOW_MODE_CHANGED"; - case PICTURE_IN_PICTURE_MODE_CHANGED: return "PICTURE_IN_PICTURE_MODE_CHANGED"; case LOCAL_VOICE_INTERACTION_STARTED: return "LOCAL_VOICE_INTERACTION_STARTED"; case ATTACH_AGENT: return "ATTACH_AGENT"; case APPLICATION_INFO_CHANGED: return "APPLICATION_INFO_CHANGED"; + case RUN_ISOLATED_ENTRY_POINT: return "RUN_ISOLATED_ENTRY_POINT"; + case EXECUTE_TRANSACTION: return "EXECUTE_TRANSACTION"; } } return Integer.toString(code); @@ -1580,76 +1472,12 @@ public final class ActivityThread { public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { - case LAUNCH_ACTIVITY: { - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); - final ActivityClientRecord r = (ActivityClientRecord) msg.obj; - - r.packageInfo = getPackageInfoNoCheck( - r.activityInfo.applicationInfo, r.compatInfo); - handleLaunchActivity(r, null, "LAUNCH_ACTIVITY"); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - } break; case RELAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart"); ActivityClientRecord r = (ActivityClientRecord)msg.obj; handleRelaunchActivity(r); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; - case PAUSE_ACTIVITY: { - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); - SomeArgs args = (SomeArgs) msg.obj; - handlePauseActivity((IBinder) args.arg1, false, - (args.argi1 & USER_LEAVING) != 0, args.argi2, - (args.argi1 & DONT_REPORT) != 0, args.argi3); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - } break; - case PAUSE_ACTIVITY_FINISHING: { - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); - SomeArgs args = (SomeArgs) msg.obj; - handlePauseActivity((IBinder) args.arg1, true, (args.argi1 & USER_LEAVING) != 0, - args.argi2, (args.argi1 & DONT_REPORT) != 0, args.argi3); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - } break; - case STOP_ACTIVITY_SHOW: { - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStop"); - SomeArgs args = (SomeArgs) msg.obj; - handleStopActivity((IBinder) args.arg1, true, args.argi2, args.argi3); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - } break; - case STOP_ACTIVITY_HIDE: { - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStop"); - SomeArgs args = (SomeArgs) msg.obj; - handleStopActivity((IBinder) args.arg1, false, args.argi2, args.argi3); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - } break; - case SHOW_WINDOW: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityShowWindow"); - handleWindowVisibility((IBinder)msg.obj, true); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - break; - case HIDE_WINDOW: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityHideWindow"); - handleWindowVisibility((IBinder)msg.obj, false); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - break; - case RESUME_ACTIVITY: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume"); - SomeArgs args = (SomeArgs) msg.obj; - handleResumeActivity((IBinder) args.arg1, true, args.argi1 != 0, true, - args.argi3, "RESUME_ACTIVITY"); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - break; - case SEND_RESULT: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityDeliverResult"); - handleSendResult((ResultData)msg.obj); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - break; - case DESTROY_ACTIVITY: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityDestroy"); - handleDestroyActivity((IBinder)msg.obj, msg.arg1 != 0, - msg.arg2, false); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - break; case BIND_APPLICATION: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication"); AppBindData data = (AppBindData)msg.obj; @@ -1662,11 +1490,6 @@ public final class ActivityThread { } Looper.myLooper().quit(); break; - case NEW_INTENT: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityNewIntent"); - handleNewIntent((NewIntentData)msg.obj); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - break; case RECEIVER: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastReceiveComp"); handleReceiver((ReceiverData)msg.obj); @@ -1698,15 +1521,7 @@ public final class ActivityThread { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; case CONFIGURATION_CHANGED: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "configChanged"); - mCurDefaultDisplayDpi = ((Configuration)msg.obj).densityDpi; - mUpdatingSystemConfig = true; - try { - handleConfigurationChanged((Configuration) msg.obj, null); - } finally { - mUpdatingSystemConfig = false; - } - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + handleConfigurationChanged((Configuration) msg.obj); break; case CLEAN_UP_CONTEXT: ContextCleanupInfo cci = (ContextCleanupInfo)msg.obj; @@ -1723,18 +1538,6 @@ public final class ActivityThread { handleLowMemory(); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; - case ACTIVITY_CONFIGURATION_CHANGED: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged"); - handleActivityConfigurationChanged((ActivityConfigChangeData) msg.obj, - INVALID_DISPLAY); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - break; - case ACTIVITY_MOVED_TO_DISPLAY: - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityMovedToDisplay"); - handleActivityConfigurationChanged((ActivityConfigChangeData) msg.obj, - msg.arg1 /* displayId */); - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - break; case PROFILER_CONTROL: handleProfilerControl(msg.arg1 != 0, (ProfilerInfo)msg.obj, msg.arg2); break; @@ -1818,16 +1621,6 @@ public final class ActivityThread { case STOP_BINDER_TRACKING_AND_DUMP: handleStopBinderTrackingAndDump((ParcelFileDescriptor) msg.obj); break; - case MULTI_WINDOW_MODE_CHANGED: - handleMultiWindowModeChanged((IBinder) ((SomeArgs) msg.obj).arg1, - ((SomeArgs) msg.obj).argi1 == 1, - (Configuration) ((SomeArgs) msg.obj).arg2); - break; - case PICTURE_IN_PICTURE_MODE_CHANGED: - handlePictureInPictureModeChanged((IBinder) ((SomeArgs) msg.obj).arg1, - ((SomeArgs) msg.obj).argi1 == 1, - (Configuration) ((SomeArgs) msg.obj).arg2); - break; case LOCAL_VOICE_INTERACTION_STARTED: handleLocalVoiceInteractionStarted((IBinder) ((SomeArgs) msg.obj).arg1, (IVoiceInteractor) ((SomeArgs) msg.obj).arg2); @@ -1843,6 +1636,13 @@ public final class ActivityThread { mUpdatingSystemConfig = false; } break; + case RUN_ISOLATED_ENTRY_POINT: + handleRunIsolatedEntryPoint((String) ((SomeArgs) msg.obj).arg1, + (String[]) ((SomeArgs) msg.obj).arg2); + break; + case EXECUTE_TRANSACTION: + mTransactionExecutor.execute(((ClientTransaction) msg.obj)); + break; } Object obj = msg.obj; if (obj instanceof SomeArgs) { @@ -2051,6 +1851,7 @@ public final class ActivityThread { registerPackage); } + @Override public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo) { return getPackageInfo(ai, compatInfo, null, false, true, false); @@ -2516,6 +2317,167 @@ public final class ActivityThread { } } + /** + * Dump heap info to proto. + * + * @param hasSwappedOutPss determines whether to use dirtySwap or dirtySwapPss + */ + private static void dumpHeap(ProtoOutputStream proto, long fieldId, String name, + int pss, int cleanPss, int sharedDirty, int privateDirty, + int sharedClean, int privateClean, + boolean hasSwappedOutPss, int dirtySwap, int dirtySwapPss) { + final long token = proto.start(fieldId); + + proto.write(MemInfoProto.NativeProcess.MemoryInfo.NAME, name); + proto.write(MemInfoProto.NativeProcess.MemoryInfo.TOTAL_PSS_KB, pss); + proto.write(MemInfoProto.NativeProcess.MemoryInfo.CLEAN_PSS_KB, cleanPss); + proto.write(MemInfoProto.NativeProcess.MemoryInfo.SHARED_DIRTY_KB, sharedDirty); + proto.write(MemInfoProto.NativeProcess.MemoryInfo.PRIVATE_DIRTY_KB, privateDirty); + proto.write(MemInfoProto.NativeProcess.MemoryInfo.SHARED_CLEAN_KB, sharedClean); + proto.write(MemInfoProto.NativeProcess.MemoryInfo.PRIVATE_CLEAN_KB, privateClean); + if (hasSwappedOutPss) { + proto.write(MemInfoProto.NativeProcess.MemoryInfo.DIRTY_SWAP_PSS_KB, dirtySwapPss); + } else { + proto.write(MemInfoProto.NativeProcess.MemoryInfo.DIRTY_SWAP_KB, dirtySwap); + } + + proto.end(token); + } + + /** + * Dump mem info data to proto. + */ + public static void dumpMemInfoTable(ProtoOutputStream proto, Debug.MemoryInfo memInfo, + boolean dumpDalvik, boolean dumpSummaryOnly, + long nativeMax, long nativeAllocated, long nativeFree, + long dalvikMax, long dalvikAllocated, long dalvikFree) { + + if (!dumpSummaryOnly) { + final long nhToken = proto.start(MemInfoProto.NativeProcess.NATIVE_HEAP); + dumpHeap(proto, MemInfoProto.NativeProcess.HeapInfo.MEM_INFO, "Native Heap", + memInfo.nativePss, memInfo.nativeSwappablePss, memInfo.nativeSharedDirty, + memInfo.nativePrivateDirty, memInfo.nativeSharedClean, + memInfo.nativePrivateClean, memInfo.hasSwappedOutPss, + memInfo.nativeSwappedOut, memInfo.nativeSwappedOutPss); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_SIZE_KB, nativeMax); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_ALLOC_KB, nativeAllocated); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_FREE_KB, nativeFree); + proto.end(nhToken); + + final long dvToken = proto.start(MemInfoProto.NativeProcess.DALVIK_HEAP); + dumpHeap(proto, MemInfoProto.NativeProcess.HeapInfo.MEM_INFO, "Dalvik Heap", + memInfo.dalvikPss, memInfo.dalvikSwappablePss, memInfo.dalvikSharedDirty, + memInfo.dalvikPrivateDirty, memInfo.dalvikSharedClean, + memInfo.dalvikPrivateClean, memInfo.hasSwappedOutPss, + memInfo.dalvikSwappedOut, memInfo.dalvikSwappedOutPss); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_SIZE_KB, dalvikMax); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_ALLOC_KB, dalvikAllocated); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_FREE_KB, dalvikFree); + proto.end(dvToken); + + int otherPss = memInfo.otherPss; + int otherSwappablePss = memInfo.otherSwappablePss; + int otherSharedDirty = memInfo.otherSharedDirty; + int otherPrivateDirty = memInfo.otherPrivateDirty; + int otherSharedClean = memInfo.otherSharedClean; + int otherPrivateClean = memInfo.otherPrivateClean; + int otherSwappedOut = memInfo.otherSwappedOut; + int otherSwappedOutPss = memInfo.otherSwappedOutPss; + + for (int i = 0; i < Debug.MemoryInfo.NUM_OTHER_STATS; i++) { + final int myPss = memInfo.getOtherPss(i); + final int mySwappablePss = memInfo.getOtherSwappablePss(i); + final int mySharedDirty = memInfo.getOtherSharedDirty(i); + final int myPrivateDirty = memInfo.getOtherPrivateDirty(i); + final int mySharedClean = memInfo.getOtherSharedClean(i); + final int myPrivateClean = memInfo.getOtherPrivateClean(i); + final int mySwappedOut = memInfo.getOtherSwappedOut(i); + final int mySwappedOutPss = memInfo.getOtherSwappedOutPss(i); + if (myPss != 0 || mySharedDirty != 0 || myPrivateDirty != 0 + || mySharedClean != 0 || myPrivateClean != 0 + || (memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut) != 0) { + dumpHeap(proto, MemInfoProto.NativeProcess.OTHER_HEAPS, + Debug.MemoryInfo.getOtherLabel(i), + myPss, mySwappablePss, mySharedDirty, myPrivateDirty, + mySharedClean, myPrivateClean, + memInfo.hasSwappedOutPss, mySwappedOut, mySwappedOutPss); + + otherPss -= myPss; + otherSwappablePss -= mySwappablePss; + otherSharedDirty -= mySharedDirty; + otherPrivateDirty -= myPrivateDirty; + otherSharedClean -= mySharedClean; + otherPrivateClean -= myPrivateClean; + otherSwappedOut -= mySwappedOut; + otherSwappedOutPss -= mySwappedOutPss; + } + } + + dumpHeap(proto, MemInfoProto.NativeProcess.UNKNOWN_HEAP, "Unknown", + otherPss, otherSwappablePss, + otherSharedDirty, otherPrivateDirty, otherSharedClean, otherPrivateClean, + memInfo.hasSwappedOutPss, otherSwappedOut, otherSwappedOutPss); + final long tToken = proto.start(MemInfoProto.NativeProcess.TOTAL_HEAP); + dumpHeap(proto, MemInfoProto.NativeProcess.HeapInfo.MEM_INFO, "TOTAL", + memInfo.getTotalPss(), memInfo.getTotalSwappablePss(), + memInfo.getTotalSharedDirty(), memInfo.getTotalPrivateDirty(), + memInfo.getTotalSharedClean(), memInfo.getTotalPrivateClean(), + memInfo.hasSwappedOutPss, memInfo.getTotalSwappedOut(), + memInfo.getTotalSwappedOutPss()); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_SIZE_KB, nativeMax + dalvikMax); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_ALLOC_KB, + nativeAllocated + dalvikAllocated); + proto.write(MemInfoProto.NativeProcess.HeapInfo.HEAP_FREE_KB, nativeFree + dalvikFree); + proto.end(tToken); + + if (dumpDalvik) { + for (int i = Debug.MemoryInfo.NUM_OTHER_STATS; + i < Debug.MemoryInfo.NUM_OTHER_STATS + Debug.MemoryInfo.NUM_DVK_STATS; + i++) { + final int myPss = memInfo.getOtherPss(i); + final int mySwappablePss = memInfo.getOtherSwappablePss(i); + final int mySharedDirty = memInfo.getOtherSharedDirty(i); + final int myPrivateDirty = memInfo.getOtherPrivateDirty(i); + final int mySharedClean = memInfo.getOtherSharedClean(i); + final int myPrivateClean = memInfo.getOtherPrivateClean(i); + final int mySwappedOut = memInfo.getOtherSwappedOut(i); + final int mySwappedOutPss = memInfo.getOtherSwappedOutPss(i); + if (myPss != 0 || mySharedDirty != 0 || myPrivateDirty != 0 + || mySharedClean != 0 || myPrivateClean != 0 + || (memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut) != 0) { + dumpHeap(proto, MemInfoProto.NativeProcess.DALVIK_DETAILS, + Debug.MemoryInfo.getOtherLabel(i), + myPss, mySwappablePss, mySharedDirty, myPrivateDirty, + mySharedClean, myPrivateClean, + memInfo.hasSwappedOutPss, mySwappedOut, mySwappedOutPss); + } + } + } + } + + final long asToken = proto.start(MemInfoProto.NativeProcess.APP_SUMMARY); + proto.write(MemInfoProto.NativeProcess.AppSummary.JAVA_HEAP_PSS_KB, + memInfo.getSummaryJavaHeap()); + proto.write(MemInfoProto.NativeProcess.AppSummary.NATIVE_HEAP_PSS_KB, + memInfo.getSummaryNativeHeap()); + proto.write(MemInfoProto.NativeProcess.AppSummary.CODE_PSS_KB, memInfo.getSummaryCode()); + proto.write(MemInfoProto.NativeProcess.AppSummary.STACK_PSS_KB, memInfo.getSummaryStack()); + proto.write(MemInfoProto.NativeProcess.AppSummary.GRAPHICS_PSS_KB, + memInfo.getSummaryGraphics()); + proto.write(MemInfoProto.NativeProcess.AppSummary.PRIVATE_OTHER_PSS_KB, + memInfo.getSummaryPrivateOther()); + proto.write(MemInfoProto.NativeProcess.AppSummary.SYSTEM_PSS_KB, + memInfo.getSummarySystem()); + if (memInfo.hasSwappedOutPss) { + proto.write(MemInfoProto.NativeProcess.AppSummary.TOTAL_SWAP_PSS, + memInfo.getSummaryTotalSwapPss()); + } else { + proto.write(MemInfoProto.NativeProcess.AppSummary.TOTAL_SWAP_PSS, + memInfo.getSummaryTotalSwap()); + } + proto.end(asToken); + } + public void registerOnActivityPausedListener(Activity activity, OnActivityPausedListener listener) { synchronized (mOnPauseListeners) { @@ -2573,13 +2535,21 @@ public final class ActivityThread { + ", comp=" + name + ", token=" + token); } - return performLaunchActivity(r, null); + // TODO(lifecycler): Can't switch to use #handleLaunchActivity() because it will try to + // call #reportSizeConfigurations(), but the server might not know anything about the + // activity if it was launched from LocalAcvitivyManager. + return performLaunchActivity(r); } public final Activity getActivity(IBinder token) { return mActivities.get(token).activity; } + @Override + public ActivityClientRecord getActivityClient(IBinder token) { + return mActivities.get(token); + } + public final void sendActivityResult( IBinder token, String id, int requestCode, int resultCode, Intent data) { @@ -2587,10 +2557,16 @@ public final class ActivityThread { + " req=" + requestCode + " res=" + resultCode + " data=" + data); ArrayList<ResultInfo> list = new ArrayList<ResultInfo>(); list.add(new ResultInfo(id, requestCode, resultCode, data)); - mAppThread.scheduleSendResult(token, list); + final ClientTransaction clientTransaction = ClientTransaction.obtain(mAppThread, token); + clientTransaction.addCallback(ActivityResultItem.obtain(list)); + try { + mAppThread.scheduleTransaction(clientTransaction); + } catch (RemoteException e) { + // Local scheduling + } } - private void sendMessage(int what, Object obj) { + void sendMessage(int what, Object obj) { sendMessage(what, obj, 0, 0, false); } @@ -2641,9 +2617,8 @@ public final class ActivityThread { sendMessage(H.CLEAN_UP_CONTEXT, cci); } - private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { - // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")"); - + /** Core implementation of activity launch. */ + private Activity performLaunchActivity(ActivityClientRecord r) { ActivityInfo aInfo = r.activityInfo; if (r.packageInfo == null) { r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, @@ -2713,9 +2688,6 @@ public final class ActivityThread { r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback); - if (customIntent != null) { - activity.mIntent = customIntent; - } r.lastNonConfigurationInstances = null; checkAndBlockForNetworkAccess(); activity.mStartedActivity = false; @@ -2736,37 +2708,8 @@ public final class ActivityThread { " did not call through to super.onCreate()"); } r.activity = activity; - r.stopped = true; - if (!r.activity.mFinished) { - activity.performStart(); - r.stopped = false; - } - if (!r.activity.mFinished) { - if (r.isPersistable()) { - if (r.state != null || r.persistentState != null) { - mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, - r.persistentState); - } - } else if (r.state != null) { - mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); - } - } - if (!r.activity.mFinished) { - activity.mCalled = false; - if (r.isPersistable()) { - mInstrumentation.callActivityOnPostCreate(activity, r.state, - r.persistentState); - } else { - mInstrumentation.callActivityOnPostCreate(activity, r.state); - } - if (!activity.mCalled) { - throw new SuperNotCalledException( - "Activity " + r.intent.getComponent().toShortString() + - " did not call through to super.onPostCreate()"); - } - } } - r.paused = true; + r.setState(ON_CREATE); mActivities.put(r.token, r); @@ -2784,6 +2727,60 @@ public final class ActivityThread { return activity; } + @Override + public void handleStartActivity(ActivityClientRecord r, + PendingTransactionActions pendingActions) { + final Activity activity = r.activity; + if (r.activity == null) { + // TODO(lifecycler): What do we do in this case? + return; + } + if (!r.stopped) { + throw new IllegalStateException("Can't start activity that is not stopped."); + } + if (r.activity.mFinished) { + // TODO(lifecycler): How can this happen? + return; + } + + // Start + activity.performStart(); + r.setState(ON_START); + + if (pendingActions == null) { + // No more work to do. + return; + } + + // Restore instance state + if (pendingActions.shouldRestoreInstanceState()) { + if (r.isPersistable()) { + if (r.state != null || r.persistentState != null) { + mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, + r.persistentState); + } + } else if (r.state != null) { + mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); + } + } + + // Call postOnCreate() + if (pendingActions.shouldCallOnPostCreate()) { + activity.mCalled = false; + if (r.isPersistable()) { + mInstrumentation.callActivityOnPostCreate(activity, r.state, + r.persistentState); + } else { + mInstrumentation.callActivityOnPostCreate(activity, r.state); + } + if (!activity.mCalled) { + throw new SuperNotCalledException( + "Activity " + r.intent.getComponent().toShortString() + + " did not call through to super.onPostCreate()"); + } + } + } + /** * Checks if {@link #mNetworkBlockSeq} is {@link #INVALID_PROC_STATE_SEQ} and if so, returns * immediately. Otherwise, makes a blocking call to ActivityManagerService to wait for the @@ -2830,7 +2827,12 @@ public final class ActivityThread { return appContext; } - private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) { + /** + * Extended implementation of activity launch. Used when server requests a launch or relaunch. + */ + @Override + public Activity handleLaunchActivity(ActivityClientRecord r, + PendingTransactionActions pendingActions) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); @@ -2853,44 +2855,28 @@ public final class ActivityThread { } WindowManagerGlobal.initialize(); - Activity a = performLaunchActivity(r, customIntent); + final Activity a = performLaunchActivity(r); if (a != null) { r.createdConfig = new Configuration(mConfiguration); reportSizeConfigurations(r); - Bundle oldState = r.state; - handleResumeActivity(r.token, false, r.isForward, - !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason); - - if (!r.activity.mFinished && r.startsNotResumed) { - // The activity manager actually wants this one to start out paused, because it - // needs to be visible but isn't in the foreground. We accomplish this by going - // through the normal startup (because activities expect to go through onResume() - // the first time they run, before their window is displayed), and then pausing it. - // However, in this case we do -not- need to do the full pause cycle (of freezing - // and such) because the activity manager assumes it can just retain the current - // state it has. - performPauseActivityIfNeeded(r, reason); - - // We need to keep around the original state, in case we need to be created again. - // But we only do this for pre-Honeycomb apps, which always save their state when - // pausing, so we can not have them save their state when restarting from a paused - // state. For HC and later, we want to (and can) let the state be saved as the - // normal part of stopping the activity. - if (r.isPreHoneycomb()) { - r.state = oldState; - } + if (!r.activity.mFinished && pendingActions != null) { + pendingActions.setOldState(r.state); + pendingActions.setRestoreInstanceState(true); + pendingActions.setCallOnPostCreate(true); } } else { // If there was an error, for any reason, tell the activity manager to stop us. try { ActivityManager.getService() - .finishActivity(r.token, Activity.RESULT_CANCELED, null, - Activity.DONT_FINISH_TASK_WITH_ACTIVITY); + .finishActivity(r.token, Activity.RESULT_CANCELED, null, + Activity.DONT_FINISH_TASK_WITH_ACTIVITY); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } + + return a; } private void reportSizeConfigurations(ActivityClientRecord r) { @@ -2960,8 +2946,9 @@ public final class ActivityThread { } } - private void handleNewIntent(NewIntentData data) { - performNewIntents(data.token, data.intents, data.andPause); + @Override + public void handleNewIntent(IBinder token, List<ReferrerIntent> intents, boolean andPause) { + performNewIntents(token, intents, andPause); } public void handleRequestAssistContextExtras(RequestAssistContextExtras cmd) { @@ -3082,7 +3069,8 @@ public final class ActivityThread { } } - private void handleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode, + @Override + public void handleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode, Configuration overrideConfig) { final ActivityClientRecord r = mActivities.get(token); if (r != null) { @@ -3094,7 +3082,8 @@ public final class ActivityThread { } } - private void handlePictureInPictureModeChanged(IBinder token, boolean isInPipMode, + @Override + public void handlePictureInPictureModeChanged(IBinder token, boolean isInPipMode, Configuration overrideConfig) { final ActivityClientRecord r = mActivities.get(token); if (r != null) { @@ -3531,8 +3520,7 @@ public final class ActivityThread { //Slog.i(TAG, "Running services: " + mServices); } - public final ActivityClientRecord performResumeActivity(IBinder token, - boolean clearHide, String reason) { + ActivityClientRecord performResumeActivity(IBinder token, boolean clearHide, String reason) { ActivityClientRecord r = mActivities.get(token); if (localLOGV) Slog.v(TAG, "Performing resume of " + r + " finished=" + r.activity.mFinished); @@ -3572,10 +3560,9 @@ public final class ActivityThread { EventLog.writeEvent(LOG_AM_ON_RESUME_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName(), reason); - r.paused = false; - r.stopped = false; r.state = null; r.persistentState = null; + r.setState(ON_RESUME); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( @@ -3605,20 +3592,16 @@ public final class ActivityThread { r.mPendingRemoveWindowManager = null; } - final void handleResumeActivity(IBinder token, - boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { - ActivityClientRecord r = mActivities.get(token); - if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) { - return; - } - + @Override + public void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, + String reason) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); mSomeActivitiesChanged = true; // TODO Push resumeArgs into the activity for consideration - r = performResumeActivity(token, clearHide, reason); + final ActivityClientRecord r = performResumeActivity(token, clearHide, reason); if (r != null) { final Activity a = r.activity; @@ -3730,16 +3713,6 @@ public final class ActivityThread { Looper.myQueue().addIdleHandler(new Idler()); } r.onlyLocalRequest = false; - - // Tell the activity manager we have resumed. - if (reallyResume) { - try { - ActivityManager.getService().activityResumed(token); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } - } - } else { // If an exception was thrown when trying to resume, then // just end this activity. @@ -3809,21 +3782,18 @@ public final class ActivityThread { return thumbnail; } - private void handlePauseActivity(IBinder token, boolean finished, - boolean userLeaving, int configChanges, boolean dontReport, int seq) { + @Override + public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving, + int configChanges, boolean dontReport, PendingTransactionActions pendingActions) { ActivityClientRecord r = mActivities.get(token); - if (DEBUG_ORDER) Slog.d(TAG, "handlePauseActivity " + r + ", seq: " + seq); - if (!checkAndUpdateLifecycleSeq(seq, r, "pauseActivity")) { - return; - } if (r != null) { - //Slog.v(TAG, "userLeaving=" + userLeaving + " handling pause of " + r); if (userLeaving) { performUserLeavingActivity(r); } r.activity.mConfigChangeFlags |= configChanges; - performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity"); + performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity", + pendingActions); // Make sure any pending writes are now committed. if (r.isPreHoneycomb()) { @@ -3847,13 +3817,15 @@ public final class ActivityThread { } final Bundle performPauseActivity(IBinder token, boolean finished, - boolean saveState, String reason) { + boolean saveState, String reason, PendingTransactionActions pendingActions) { ActivityClientRecord r = mActivities.get(token); - return r != null ? performPauseActivity(r, finished, saveState, reason) : null; + return r != null + ? performPauseActivity(r, finished, saveState, reason, pendingActions) + : null; } - final Bundle performPauseActivity(ActivityClientRecord r, boolean finished, - boolean saveState, String reason) { + private Bundle performPauseActivity(ActivityClientRecord r, boolean finished, boolean saveState, + String reason, PendingTransactionActions pendingActions) { if (r.paused) { if (r.activity.mFinished) { // If we are finishing, we won't call onResume() in certain cases. @@ -3887,6 +3859,18 @@ public final class ActivityThread { listeners.get(i).onPaused(r.activity); } + final Bundle oldState = pendingActions != null ? pendingActions.getOldState() : null; + if (oldState != null) { + // We need to keep around the original state, in case we need to be created again. + // But we only do this for pre-Honeycomb apps, which always save their state when + // pausing, so we can not have them save their state when restarting from a paused + // state. For HC and later, we want to (and can) let the state be saved as the + // normal part of stopping the activity. + if (r.isPreHoneycomb()) { + r.state = oldState; + } + } + return !r.activity.mFinished && saveState ? r.state : null; } @@ -3913,7 +3897,7 @@ public final class ActivityThread { + safeToComponentShortString(r.intent) + ": " + e.toString(), e); } } - r.paused = true; + r.setState(ON_PAUSE); } final void performStopActivity(IBinder token, boolean saveState, String reason) { @@ -3921,37 +3905,6 @@ public final class ActivityThread { performStopActivityInner(r, null, false, saveState, reason); } - private static class StopInfo implements Runnable { - ActivityClientRecord activity; - Bundle state; - PersistableBundle persistentState; - CharSequence description; - - @Override public void run() { - // Tell activity manager we have been stopped. - try { - if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity); - ActivityManager.getService().activityStopped( - activity.token, state, persistentState, description); - } catch (RemoteException ex) { - // Dump statistics about bundle to help developers debug - final LogWriter writer = new LogWriter(Log.WARN, TAG); - final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); - pw.println("Bundle stats:"); - Bundle.dumpStats(pw, state); - pw.println("PersistableBundle stats:"); - Bundle.dumpStats(pw, persistentState); - - if (ex instanceof TransactionTooLargeException - && activity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) { - Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex); - return; - } - throw ex.rethrowFromSystemServer(); - } - } - } - private static final class ProviderRefCount { public final ContentProviderHolder holder; public final ProviderClientRecord client; @@ -3982,8 +3935,8 @@ public final class ActivityThread { * For the client, we want to call onStop()/onStart() to indicate when * the activity's UI visibility changes. */ - private void performStopActivityInner(ActivityClientRecord r, - StopInfo info, boolean keepShown, boolean saveState, String reason) { + private void performStopActivityInner(ActivityClientRecord r, StopInfo info, boolean keepShown, + boolean saveState, String reason) { if (localLOGV) Slog.v(TAG, "Performing stop of " + r); if (r != null) { if (!keepShown && r.stopped) { @@ -4008,7 +3961,7 @@ public final class ActivityThread { // First create a thumbnail for the activity... // For now, don't create the thumbnail here; we are // doing that by doing a screen snapshot. - info.description = r.activity.onCreateDescription(); + info.setDescription(r.activity.onCreateDescription()); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( @@ -4038,7 +3991,7 @@ public final class ActivityThread { + ": " + e.toString(), e); } } - r.stopped = true; + r.setState(ON_STOP); EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName(), reason); } @@ -4073,15 +4026,14 @@ public final class ActivityThread { } } - private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) { - ActivityClientRecord r = mActivities.get(token); - if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) { - return; - } + @Override + public void handleStopActivity(IBinder token, boolean show, int configChanges, + PendingTransactionActions pendingActions) { + final ActivityClientRecord r = mActivities.get(token); r.activity.mConfigChangeFlags |= configChanges; - StopInfo info = new StopInfo(); - performStopActivityInner(r, info, show, true, "handleStopActivity"); + final StopInfo stopInfo = new StopInfo(); + performStopActivityInner(r, stopInfo, show, true, "handleStopActivity"); if (localLOGV) Slog.v( TAG, "Finishing stop of " + r + ": show=" + show @@ -4094,41 +4046,37 @@ public final class ActivityThread { QueuedWork.waitToFinish(); } - // Schedule the call to tell the activity manager we have - // stopped. We don't do this immediately, because we want to - // have a chance for any other pending work (in particular memory - // trim requests) to complete before you tell the activity - // manager to proceed and allow us to go fully into the background. - info.activity = r; - info.state = r.state; - info.persistentState = r.persistentState; - mH.post(info); + stopInfo.setActivity(r); + stopInfo.setState(r.state); + stopInfo.setPersistentState(r.persistentState); + pendingActions.setStopInfo(stopInfo); mSomeActivitiesChanged = true; } - private static boolean checkAndUpdateLifecycleSeq(int seq, ActivityClientRecord r, - String action) { - if (r == null) { - return true; - } - if (seq < r.lastProcessedSeq) { - if (DEBUG_ORDER) Slog.d(TAG, action + " for " + r + " ignored, because seq=" + seq - + " < mCurrentLifecycleSeq=" + r.lastProcessedSeq); - return false; - } - r.lastProcessedSeq = seq; - return true; + /** + * Schedule the call to tell the activity manager we have stopped. We don't do this + * immediately, because we want to have a chance for any other pending work (in particular + * memory trim requests) to complete before you tell the activity manager to proceed and allow + * us to go fully into the background. + */ + @Override + public void reportStop(PendingTransactionActions pendingActions) { + mH.post(pendingActions.getStopInfo()); } - final void performRestartActivity(IBinder token) { + @Override + public void performRestartActivity(IBinder token, boolean start) { ActivityClientRecord r = mActivities.get(token); if (r.stopped) { - r.activity.performRestart(); - r.stopped = false; + r.activity.performRestart(start); + if (start) { + r.setState(ON_START); + } } } - private void handleWindowVisibility(IBinder token, boolean show) { + @Override + public void handleWindowVisibility(IBinder token, boolean show) { ActivityClientRecord r = mActivities.get(token); if (r == null) { @@ -4143,8 +4091,8 @@ public final class ActivityThread { // we are back active so skip it. unscheduleGcIdler(); - r.activity.performRestart(); - r.stopped = false; + r.activity.performRestart(true /* start */); + r.setState(ON_START); } if (r.activity.mDecor != null) { if (false) Slog.v( @@ -4182,7 +4130,7 @@ public final class ActivityThread { + ": " + e.toString(), e); } } - r.stopped = true; + r.setState(ON_STOP); EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName(), "sleeping"); } @@ -4200,8 +4148,8 @@ public final class ActivityThread { } } else { if (r.stopped && r.activity.mVisibleFromServer) { - r.activity.performRestart(); - r.stopped = false; + r.activity.performRestart(true /* start */); + r.setState(ON_START); } } } @@ -4274,8 +4222,9 @@ public final class ActivityThread { } } - private void handleSendResult(ResultData res) { - ActivityClientRecord r = mActivities.get(res.token); + @Override + public void handleSendResult(IBinder token, List<ResultInfo> results) { + ActivityClientRecord r = mActivities.get(token); if (DEBUG_RESULTS) Slog.v(TAG, "Handling send result to " + r); if (r != null) { final boolean resumed = !r.paused; @@ -4309,7 +4258,7 @@ public final class ActivityThread { } } checkAndBlockForNetworkAccess(); - deliverResults(r, res.results); + deliverResults(r, results); if (resumed) { r.activity.performResume(); r.activity.mTemporaryPause = false; @@ -4317,11 +4266,8 @@ public final class ActivityThread { } } - public final ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing) { - return performDestroyActivity(token, finishing, 0, false); - } - - private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing, + /** Core implementation of activity destroy call. */ + ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance) { ActivityClientRecord r = mActivities.get(token); Class<? extends Activity> activityClass = null; @@ -4348,7 +4294,7 @@ public final class ActivityThread { + ": " + e.toString(), e); } } - r.stopped = true; + r.setState(ON_STOP); EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName(), "destroy"); } @@ -4385,6 +4331,7 @@ public final class ActivityThread { + ": " + e.toString(), e); } } + r.setState(ON_DESTROY); } mActivities.remove(token); StrictMode.decrementExpectedActivityCount(activityClass); @@ -4396,8 +4343,9 @@ public final class ActivityThread { return component == null ? "[Unknown]" : component.toShortString(); } - private void handleDestroyActivity(IBinder token, boolean finishing, - int configChanges, boolean getNonConfigInstance) { + @Override + public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges, + boolean getNonConfigInstance) { ActivityClientRecord r = performDestroyActivity(token, finishing, configChanges, getNonConfigInstance); if (r != null) { @@ -4544,10 +4492,7 @@ public final class ActivityThread { target.overrideConfig = overrideConfig; } target.pendingConfigChanges |= configChanges; - target.relaunchSeq = getLifecycleSeq(); } - if (DEBUG_ORDER) Slog.d(TAG, "relaunchActivity " + ActivityThread.this + ", target " - + target + " operation received seq: " + target.relaunchSeq); } private void handleRelaunchActivity(ActivityClientRecord tmp) { @@ -4592,12 +4537,6 @@ public final class ActivityThread { } } - if (tmp.lastProcessedSeq > tmp.relaunchSeq) { - Slog.wtf(TAG, "For some reason target: " + tmp + " has lower sequence: " - + tmp.relaunchSeq + " than current sequence: " + tmp.lastProcessedSeq); - } else { - tmp.lastProcessedSeq = tmp.relaunchSeq; - } if (tmp.createdConfig != null) { // If the activity manager is passing us its current config, // assume that is really what we want regardless of what we @@ -4638,9 +4577,6 @@ public final class ActivityThread { r.activity.mConfigChangeFlags |= configChanges; r.onlyLocalRequest = tmp.onlyLocalRequest; r.mPreserveWindow = tmp.mPreserveWindow; - r.lastProcessedSeq = tmp.lastProcessedSeq; - r.relaunchSeq = tmp.relaunchSeq; - Intent currentIntent = r.activity.mIntent; r.activity.mChangingConfigurations = true; @@ -4666,7 +4602,8 @@ public final class ActivityThread { // Need to ensure state is saved. if (!r.paused) { - performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity"); + performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity", + null /* pendingActions */); } if (r.state == null && !r.stopped && !r.isPreHoneycomb()) { callCallActivityOnSaveInstanceState(r); @@ -4696,7 +4633,15 @@ public final class ActivityThread { r.startsNotResumed = tmp.startsNotResumed; r.overrideConfig = tmp.overrideConfig; - handleLaunchActivity(r, currentIntent, "handleRelaunchActivity"); + // TODO(lifecycler): Move relaunch to lifecycler. + PendingTransactionActions pendingActions = new PendingTransactionActions(); + handleLaunchActivity(r, pendingActions); + handleStartActivity(r, pendingActions); + handleResumeActivity(r.token, false /* clearHide */, r.isForward, "relaunch"); + if (r.startsNotResumed) { + performPauseActivity(r, false /* finished */, r.isPreHoneycomb(), "relaunch", + pendingActions); + } if (!tmp.onlyLocalRequest) { try { @@ -4968,7 +4913,20 @@ public final class ActivityThread { return config; } - final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) { + @Override + public void handleConfigurationChanged(Configuration config) { + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "configChanged"); + mCurDefaultDisplayDpi = config.densityDpi; + mUpdatingSystemConfig = true; + try { + handleConfigurationChanged(config, null /* compat */); + } finally { + mUpdatingSystemConfig = false; + } + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + } + + private void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) { int configDiff = 0; @@ -5099,12 +5057,15 @@ public final class ActivityThread { /** * Handle new activity configuration and/or move to a different display. - * @param data Configuration update data. + * @param activityToken Target activity token. + * @param overrideConfig Activity override config. * @param displayId Id of the display where activity was moved to, -1 if there was no move and * value didn't change. */ - void handleActivityConfigurationChanged(ActivityConfigChangeData data, int displayId) { - ActivityClientRecord r = mActivities.get(data.activityToken); + @Override + public void handleActivityConfigurationChanged(IBinder activityToken, + Configuration overrideConfig, int displayId) { + ActivityClientRecord r = mActivities.get(activityToken); // Check input params. if (r == null || r.activity == null) { if (DEBUG_CONFIGURATION) Slog.w(TAG, "Not found target activity to report to: " + r); @@ -5114,14 +5075,14 @@ public final class ActivityThread { && displayId != r.activity.getDisplay().getDisplayId(); // Perform updates. - r.overrideConfig = data.overrideConfig; + r.overrideConfig = overrideConfig; final ViewRootImpl viewRoot = r.activity.mDecor != null ? r.activity.mDecor.getViewRootImpl() : null; if (movedToDifferentDisplay) { if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity moved to display, activity:" + r.activityInfo.name + ", displayId=" + displayId - + ", config=" + data.overrideConfig); + + ", config=" + overrideConfig); final Configuration reportedConfig = performConfigurationChangedForActivity(r, mCompatConfiguration, displayId, true /* movedToDifferentDisplay */); @@ -5130,7 +5091,7 @@ public final class ActivityThread { } } else { if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity config changed: " - + r.activityInfo.name + ", config=" + data.overrideConfig); + + r.activityInfo.name + ", config=" + overrideConfig); performConfigurationChangedForActivity(r, mCompatConfiguration); } // Notify the ViewRootImpl instance about configuration changes. It may have initiated this @@ -5366,7 +5327,7 @@ public final class ActivityThread { } } - GraphicsEnvironment.chooseDriver(context); + GraphicsEnvironment.getInstance().setup(context); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } @@ -5519,32 +5480,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 @@ -5641,7 +5578,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 @@ -5663,7 +5605,16 @@ public final class ActivityThread { // Continue loading instrumentation. if (ii != null) { - final ApplicationInfo instrApp = new ApplicationInfo(); + ApplicationInfo instrApp; + try { + instrApp = getPackageManager().getApplicationInfo(ii.packageName, 0, + UserHandle.myUserId()); + } catch (RemoteException e) { + instrApp = null; + } + if (instrApp == null) { + instrApp = new ApplicationInfo(); + } ii.copyTo(instrApp); instrApp.initForUser(UserHandle.myUserId()); final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo, @@ -6312,6 +6263,17 @@ public final class ActivityThread { return retHolder; } + private void handleRunIsolatedEntryPoint(String entryPoint, String[] entryPointArgs) { + try { + Method main = Class.forName(entryPoint).getMethod("main", String[].class); + main.invoke(null, new Object[]{entryPointArgs}); + } catch (ReflectiveOperationException e) { + throw new AndroidRuntimeException("runIsolatedEntryPoint failed", e); + } + // The process will be empty after this method returns; exit the VM now. + System.exit(0); + } + private void attach(boolean system) { sCurrentActivityThread = this; mSystemThread = system; diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java index 2813e8b9707e..382719b4a305 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -33,6 +33,7 @@ import android.os.WorkSource; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; +import android.util.proto.ProtoOutputStream; import libcore.util.ZoneInfoDB; @@ -48,7 +49,7 @@ import java.lang.annotation.RetentionPolicy; * if it is not already running. Registered alarms are retained while the * device is asleep (and can optionally wake the device up if they go off * during that time), but will be cleared if it is turned off and rebooted. - * + * * <p>The Alarm Manager holds a CPU wake lock as long as the alarm receiver's * onReceive() method is executing. This guarantees that the phone will not sleep * until you have finished handling the broadcast. Once onReceive() returns, the @@ -296,7 +297,7 @@ public class AlarmManager { * {@link Intent#EXTRA_ALARM_COUNT Intent.EXTRA_ALARM_COUNT} that indicates * how many past alarm events have been accumulated into this intent * broadcast. Recurring alarms that have gone undelivered because the - * phone was asleep may have a count greater than one when delivered. + * phone was asleep may have a count greater than one when delivered. * * <div class="note"> * <p> @@ -396,10 +397,10 @@ public class AlarmManager { * set a recurring alarm for the top of every hour but the phone was asleep * from 7:45 until 8:45, an alarm will be sent as soon as the phone awakens, * then the next alarm will be sent at 9:00. - * - * <p>If your application wants to allow the delivery times to drift in + * + * <p>If your application wants to allow the delivery times to drift in * order to guarantee that at least a certain time interval always elapses - * between alarms, then the approach to take is to use one-time alarms, + * between alarms, then the approach to take is to use one-time alarms, * scheduling the next one yourself when handling each alarm delivery. * * <p class="note"> @@ -567,9 +568,21 @@ public class AlarmManager { } /** - * Schedule an alarm that represents an alarm clock. + * Schedule an alarm that represents an alarm clock, which will be used to notify the user + * when it goes off. The expectation is that when this alarm triggers, the application will + * further wake up the device to tell the user about the alarm -- turning on the screen, + * playing a sound, vibrating, etc. As such, the system will typically also use the + * information supplied here to tell the user about this upcoming alarm if appropriate. * - * The system may choose to display information about this alarm to the user. + * <p>Due to the nature of this kind of alarm, similar to {@link #setExactAndAllowWhileIdle}, + * these alarms will be allowed to trigger even if the system is in a low-power idle + * (a.k.a. doze) mode. The system may also do some prep-work when it sees that such an + * alarm coming up, to reduce the amount of background work that could happen if this + * causes the device to fully wake up -- this is to avoid situations such as a large number + * of devices having an alarm set at the same time in the morning, all waking up at that + * time and suddenly swamping the network with pending background work. As such, these + * types of alarms can be extremely expensive on battery use and should only be used for + * their intended purpose.</p> * * <p> * This method is like {@link #setExact(int, long, PendingIntent)}, but implies @@ -782,9 +795,9 @@ public class AlarmManager { /** * Like {@link #set(int, long, PendingIntent)}, but this alarm will be allowed to execute - * even when the system is in low-power idle modes. This type of alarm must <b>only</b> - * be used for situations where it is actually required that the alarm go off while in - * idle -- a reasonable example would be for a calendar notification that should make a + * even when the system is in low-power idle (a.k.a. doze) modes. This type of alarm must + * <b>only</b> be used for situations where it is actually required that the alarm go off while + * in idle -- a reasonable example would be for a calendar notification that should make a * sound so the user is aware of it. When the alarm is dispatched, the app will also be * added to the system's temporary whitelist for approximately 10 seconds to allow that * application to acquire further wake locks in which to complete its work.</p> @@ -1056,7 +1069,7 @@ public class AlarmManager { /** * Creates a new alarm clock description. * - * @param triggerTime time at which the underlying alarm is triggered in wall time + * @param triggerTime time at which the underlying alarm is triggered in wall time * milliseconds since the epoch * @param showIntent an intent that can be used to show or edit details of * the alarm clock. @@ -1089,7 +1102,7 @@ public class AlarmManager { * Returns an intent that can be used to show or edit details of the alarm clock in * the application that scheduled it. * - * <p class="note">Beware that any application can retrieve and send this intent, + * <p class="note">Beware that any application can retrieve and send this intent, * potentially with additional fields filled in. See * {@link PendingIntent#send(android.content.Context, int, android.content.Intent) * PendingIntent.send()} and {@link android.content.Intent#fillIn Intent.fillIn()} @@ -1121,5 +1134,13 @@ public class AlarmManager { return new AlarmClockInfo[size]; } }; + + /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + proto.write(AlarmClockInfoProto.TRIGGER_TIME_MS, mTriggerTime); + mShowIntent.writeToProto(proto, AlarmClockInfoProto.SHOW_INTENT); + proto.end(token); + } } } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index b331d84010d0..c1a51044e349 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -252,8 +252,14 @@ public class AppOpsManager { public static final int OP_INSTANT_APP_START_FOREGROUND = 68; /** @hide Answer incoming phone calls */ 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 Request package deletion through package installer */ + public static final int OP_REQUEST_DELETE_PACKAGES = 72; /** @hide */ - public static final int _NUM_OP = 70; + public static final int _NUM_OP = 73; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -406,6 +412,7 @@ public class AppOpsManager { OP_CAMERA, // Body sensors OP_BODY_SENSORS, + OP_REQUEST_DELETE_PACKAGES, // APPOP PERMISSIONS OP_ACCESS_NOTIFICATIONS, @@ -492,7 +499,10 @@ public class AppOpsManager { OP_REQUEST_INSTALL_PACKAGES, OP_PICTURE_IN_PICTURE, OP_INSTANT_APP_START_FOREGROUND, - OP_ANSWER_PHONE_CALLS + OP_ANSWER_PHONE_CALLS, + OP_RUN_ANY_IN_BACKGROUND, + OP_CHANGE_WIFI_STATE, + OP_REQUEST_DELETE_PACKAGES, }; /** @@ -570,6 +580,9 @@ public class AppOpsManager { OPSTR_PICTURE_IN_PICTURE, OPSTR_INSTANT_APP_START_FOREGROUND, OPSTR_ANSWER_PHONE_CALLS, + null, // OP_RUN_ANY_IN_BACKGROUND + null, // OP_CHANGE_WIFI_STATE + null, // OP_REQUEST_DELETE_PACKAGES }; /** @@ -647,6 +660,9 @@ public class AppOpsManager { "PICTURE_IN_PICTURE", "INSTANT_APP_START_FOREGROUND", "ANSWER_PHONE_CALLS", + "RUN_ANY_IN_BACKGROUND", + "CHANGE_WIFI_STATE", + "REQUEST_DELETE_PACKAGES", }; /** @@ -724,6 +740,9 @@ public class AppOpsManager { null, // no permission for entering picture-in-picture on hide 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, + Manifest.permission.REQUEST_DELETE_PACKAGES, }; /** @@ -802,6 +821,9 @@ public class AppOpsManager { null, // ENTER_PICTURE_IN_PICTURE_ON_HIDE null, // INSTANT_APP_START_FOREGROUND null, // ANSWER_PHONE_CALLS + null, // OP_RUN_ANY_IN_BACKGROUND + null, // OP_CHANGE_WIFI_STATE + null, // REQUEST_DELETE_PACKAGES }; /** @@ -879,6 +901,9 @@ public class AppOpsManager { false, // ENTER_PICTURE_IN_PICTURE_ON_HIDE false, // INSTANT_APP_START_FOREGROUND false, // ANSWER_PHONE_CALLS + false, // OP_RUN_ANY_IN_BACKGROUND + false, // OP_CHANGE_WIFI_STATE + false, // OP_REQUEST_DELETE_PACKAGES }; /** @@ -955,6 +980,9 @@ public class AppOpsManager { AppOpsManager.MODE_ALLOWED, // OP_PICTURE_IN_PICTURE 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 + AppOpsManager.MODE_ALLOWED, // REQUEST_DELETE_PACKAGES }; /** @@ -1035,6 +1063,9 @@ public class AppOpsManager { false, // OP_PICTURE_IN_PICTURE false, false, // ANSWER_PHONE_CALLS + false, // OP_RUN_ANY_IN_BACKGROUND + false, // OP_CHANGE_WIFI_STATE + false, // OP_REQUEST_DELETE_PACKAGES }; /** diff --git a/core/java/android/app/ApplicationLoaders.java b/core/java/android/app/ApplicationLoaders.java index b7c1f4e082e2..725704422290 100644 --- a/core/java/android/app/ApplicationLoaders.java +++ b/core/java/android/app/ApplicationLoaders.java @@ -17,9 +17,12 @@ package android.app; import android.os.Build; +import android.os.GraphicsEnvironment; import android.os.Trace; import android.util.ArrayMap; + import com.android.internal.os.ClassLoaderFactory; + import dalvik.system.PathClassLoader; /** @hide */ @@ -72,8 +75,9 @@ public class ApplicationLoaders { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setupVulkanLayerPath"); - setupVulkanLayerPath(classloader, librarySearchPath); + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setLayerPaths"); + GraphicsEnvironment.getInstance().setLayerPaths( + classloader, librarySearchPath, libraryPermittedPath); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); mLoaders.put(cacheKey, classloader); @@ -105,8 +109,6 @@ public class ApplicationLoaders { cacheKey, null /* classLoaderName */); } - private static native void setupVulkanLayerPath(ClassLoader classLoader, String librarySearchPath); - /** * Adds a new path the classpath of the given loader. * @throws IllegalStateException if the provided class loader is not a {@link PathClassLoader}. diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 0eafdec6bb0f..1dbdb59ebcce 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -35,7 +35,6 @@ import android.content.pm.FeatureInfo; import android.content.pm.IOnPermissionsChangeListener; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageDeleteObserver; -import android.content.pm.IPackageInstallObserver; import android.content.pm.IPackageManager; import android.content.pm.IPackageMoveObserver; import android.content.pm.IPackageStatsObserver; @@ -56,6 +55,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.SharedLibraryInfo; import android.content.pm.VerifierDeviceIdentity; import android.content.pm.VersionedPackage; +import android.content.pm.dex.ArtManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.Bitmap; @@ -122,6 +122,8 @@ public class ApplicationPackageManager extends PackageManager { private UserManager mUserManager; @GuardedBy("mLock") private PackageInstaller mInstaller; + @GuardedBy("mLock") + private ArtManager mArtManager; @GuardedBy("mDelegates") private final ArrayList<MoveCallbackDelegate> mDelegates = new ArrayList<>(); @@ -1681,21 +1683,8 @@ public class ApplicationPackageManager extends PackageManager { } @Override - public void installPackage(Uri packageURI, IPackageInstallObserver observer, int flags, - String installerPackageName) { - installCommon(packageURI, new LegacyPackageInstallObserver(observer), flags, - installerPackageName, mContext.getUserId()); - } - - @Override - public void installPackage(Uri packageURI, PackageInstallObserver observer, - int flags, String installerPackageName) { - installCommon(packageURI, observer, flags, installerPackageName, mContext.getUserId()); - } - - private void installCommon(Uri packageURI, - PackageInstallObserver observer, int flags, String installerPackageName, - int userId) { + public void installPackage(Uri packageURI, + PackageInstallObserver observer, int flags, String installerPackageName) { if (!"file".equals(packageURI.getScheme())) { throw new UnsupportedOperationException("Only file:// URIs are supported"); } @@ -1703,7 +1692,7 @@ public class ApplicationPackageManager extends PackageManager { final String originPath = packageURI.getPath(); try { mPM.installPackageAsUser(originPath, observer.getBinder(), flags, installerPackageName, - userId); + mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2476,7 +2465,8 @@ public class ApplicationPackageManager extends PackageManager { if (itemInfo.showUserIcon != UserHandle.USER_NULL) { Bitmap bitmap = getUserManager().getUserIcon(itemInfo.showUserIcon); if (bitmap == null) { - return UserIcons.getDefaultUserIcon(itemInfo.showUserIcon, /* light= */ false); + return UserIcons.getDefaultUserIcon( + mContext.getResources(), itemInfo.showUserIcon, /* light= */ false); } return new BitmapDrawable(bitmap); } @@ -2763,4 +2753,18 @@ public class ApplicationPackageManager extends PackageManager { throw e.rethrowAsRuntimeException(); } } + + @Override + public ArtManager getArtManager() { + synchronized (mLock) { + if (mArtManager == null) { + try { + mArtManager = new ArtManager(mPM.getArtManager()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return mArtManager; + } + } } diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java new file mode 100644 index 000000000000..ef66af0c60f4 --- /dev/null +++ b/core/java/android/app/ClientTransactionHandler.java @@ -0,0 +1,124 @@ +/* + * 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.app; + +import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.PendingTransactionActions; +import android.content.pm.ApplicationInfo; +import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; +import android.os.IBinder; + +import com.android.internal.content.ReferrerIntent; + +import java.util.List; + +/** + * Defines operations that a {@link android.app.servertransaction.ClientTransaction} or its items + * can perform on client. + * @hide + */ +public abstract class ClientTransactionHandler { + + // Schedule phase related logic and handlers. + + /** Prepare and schedule transaction for execution. */ + void scheduleTransaction(ClientTransaction transaction) { + transaction.preExecute(this); + sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction); + } + + abstract void sendMessage(int what, Object obj); + + + // Prepare phase related logic and handlers. Methods that inform about about pending changes or + // do other internal bookkeeping. + + /** Set pending config in case it will be updated by other transaction item. */ + public abstract void updatePendingConfiguration(Configuration config); + + /** Set current process state. */ + public abstract void updateProcessState(int processState, boolean fromIpc); + + + // Execute phase related logic and handlers. Methods here execute actual lifecycle transactions + // and deliver callbacks. + + /** Destroy the activity. */ + public abstract void handleDestroyActivity(IBinder token, boolean finishing, int configChanges, + boolean getNonConfigInstance); + + /** Pause the activity. */ + public abstract void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving, + int configChanges, boolean dontReport, PendingTransactionActions pendingActions); + + /** Resume the activity. */ + public abstract void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, + String reason); + + /** Stop the activity. */ + public abstract void handleStopActivity(IBinder token, boolean show, int configChanges, + PendingTransactionActions pendingActions); + + /** Report that activity was stopped to server. */ + public abstract void reportStop(PendingTransactionActions pendingActions); + + /** Restart the activity after it was stopped. */ + public abstract void performRestartActivity(IBinder token, boolean start); + + /** Deliver activity (override) configuration change. */ + public abstract void handleActivityConfigurationChanged(IBinder activityToken, + Configuration overrideConfig, int displayId); + + /** Deliver result from another activity. */ + public abstract void handleSendResult(IBinder token, List<ResultInfo> results); + + /** Deliver multi-window mode change notification. */ + public abstract void handleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode, + Configuration overrideConfig); + + /** Deliver new intent. */ + public abstract void handleNewIntent(IBinder token, List<ReferrerIntent> intents, + boolean andPause); + + /** Deliver picture-in-picture mode change notification. */ + public abstract void handlePictureInPictureModeChanged(IBinder token, boolean isInPipMode, + Configuration overrideConfig); + + /** Update window visibility. */ + public abstract void handleWindowVisibility(IBinder token, boolean show); + + /** Perform activity launch. */ + public abstract Activity handleLaunchActivity(ActivityThread.ActivityClientRecord r, + PendingTransactionActions pendingActions); + + /** Perform activity start. */ + public abstract void handleStartActivity(ActivityThread.ActivityClientRecord r, + PendingTransactionActions pendingActions); + + /** Get package info. */ + public abstract LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, + CompatibilityInfo compatInfo); + + /** Deliver app configuration change notification. */ + public abstract void handleConfigurationChanged(Configuration config); + + /** + * Get {@link android.app.ActivityThread.ActivityClientRecord} instance that corresponds to the + * provided token. + */ + public abstract ActivityThread.ActivityClientRecord getActivityClient(IBinder token); +} diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 5f3432264ca0..b0d020a7e328 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -59,11 +59,10 @@ import android.os.IBinder; import android.os.Looper; import android.os.Process; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; -import android.os.storage.IStorageManager; +import android.os.storage.StorageManager; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; @@ -2457,7 +2456,8 @@ class ContextImpl extends Context { * unable to create, they are filtered by replacing with {@code null}. */ private File[] ensureExternalDirsExistOrFilter(File[] dirs) { - File[] result = new File[dirs.length]; + final StorageManager sm = getSystemService(StorageManager.class); + final File[] result = new File[dirs.length]; for (int i = 0; i < dirs.length; i++) { File dir = dirs[i]; if (!dir.exists()) { @@ -2466,15 +2466,8 @@ class ContextImpl extends Context { if (!dir.exists()) { // Failing to mkdir() may be okay, since we might not have // enough permissions; ask vold to create on our behalf. - final IStorageManager storageManager = IStorageManager.Stub.asInterface( - ServiceManager.getService("mount")); try { - final int res = storageManager.mkdirs( - getPackageName(), dir.getAbsolutePath()); - if (res != 0) { - Log.w(TAG, "Failed to ensure " + dir + ": " + res); - dir = null; - } + sm.mkdirs(dir); } catch (Exception e) { Log.w(TAG, "Failed to ensure " + dir + ": " + e); dir = null; diff --git a/core/java/android/app/DialogFragment.java b/core/java/android/app/DialogFragment.java index 7e0e4d827477..a0fb6eeb9e15 100644 --- a/core/java/android/app/DialogFragment.java +++ b/core/java/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/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index 93773454424e..a92684b5d304 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/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/core/java/android/app/FragmentBreadCrumbs.java b/core/java/android/app/FragmentBreadCrumbs.java index d0aa0fd605bb..e3e47ae652ca 100644 --- a/core/java/android/app/FragmentBreadCrumbs.java +++ b/core/java/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/core/java/android/app/FragmentContainer.java b/core/java/android/app/FragmentContainer.java index f8836bc829a0..a1dd32ffe95d 100644 --- a/core/java/android/app/FragmentContainer.java +++ b/core/java/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/core/java/android/app/FragmentController.java b/core/java/android/app/FragmentController.java index cff94d8cbf2d..cbb58d402bca 100644 --- a/core/java/android/app/FragmentController.java +++ b/core/java/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/core/java/android/app/FragmentHostCallback.java b/core/java/android/app/FragmentHostCallback.java index 5ef23e630573..1edc68eda808 100644 --- a/core/java/android/app/FragmentHostCallback.java +++ b/core/java/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/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java index 0d5cd0214f37..12e60b874702 100644 --- a/core/java/android/app/FragmentManager.java +++ b/core/java/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/core/java/android/app/FragmentManagerNonConfig.java b/core/java/android/app/FragmentManagerNonConfig.java index 50d3797dd59d..beb1a15adae3 100644 --- a/core/java/android/app/FragmentManagerNonConfig.java +++ b/core/java/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/core/java/android/app/FragmentTransaction.java b/core/java/android/app/FragmentTransaction.java index c910e9035a3f..0f4a7fb500a3 100644 --- a/core/java/android/app/FragmentTransaction.java +++ b/core/java/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/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 18117481b0ea..85bf6aa75f56 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -22,6 +22,7 @@ import android.app.ContentProviderHolder; import android.app.IApplicationThread; import android.app.IActivityController; import android.app.IAppTask; +import android.app.IAssistDataReceiver; import android.app.IInstrumentationWatcher; import android.app.IProcessObserver; import android.app.IServiceConnection; @@ -82,7 +83,7 @@ interface IActivityManager { // below block of transactions. // Since these transactions are also called from native code, these must be kept in sync with - // the ones in frameworks/native/include/binder/IActivityManager.h + // the ones in frameworks/native/libs/binder/include/binder/IActivityManager.h // =============== Beginning of transactions used on native side as well ====================== ParcelFileDescriptor openContentUri(in String uriString); // =============== End of transactions used on native side as well ============================ @@ -115,7 +116,9 @@ interface IActivityManager { in PersistableBundle persistentState, in CharSequence description); String getCallingPackage(in IBinder token); ComponentName getCallingActivity(in IBinder token); - List<ActivityManager.RunningTaskInfo> getTasks(int maxNum, int flags); + List<ActivityManager.RunningTaskInfo> getTasks(int maxNum); + List<ActivityManager.RunningTaskInfo> getFilteredTasks(int maxNum, int ignoreActivityType, + int ignoreWindowingMode); void moveTaskToFront(int task, int flags, in Bundle options); void moveTaskBackwards(int task); int getTaskForActivity(in IBinder token, in boolean onlyRoot); @@ -178,8 +181,8 @@ interface IActivityManager { * SIGUSR1 is delivered. All others are ignored. */ void signalPersistentProcesses(int signal); - ParceledListSlice getRecentTasks(int maxNum, - int flags, int userId); + + ParceledListSlice getRecentTasks(int maxNum, int flags, int userId); oneway void serviceDoneExecuting(in IBinder token, int type, int startId, int res); oneway void activityDestroyed(in IBinder token); IIntentSender getIntentSender(int type, in String packageName, in IBinder token, @@ -204,12 +207,11 @@ interface IActivityManager { boolean moveActivityTaskToBack(in IBinder token, boolean nonRoot); void getMemoryInfo(out ActivityManager.MemoryInfo outInfo); List<ActivityManager.ProcessErrorStateInfo> getProcessesInErrorState(); - boolean clearApplicationUserData(in String packageName, + boolean clearApplicationUserData(in String packageName, boolean keepState, in IPackageDataObserver observer, int userId); void forceStopPackage(in String packageName, int userId); boolean killPids(in int[] pids, in String reason, boolean secure); List<ActivityManager.RunningServiceInfo> getServices(int maxNum, int flags); - ActivityManager.TaskThumbnail getTaskThumbnail(int taskId); ActivityManager.TaskDescription getTaskDescription(int taskId); // Retrieve running application processes in the system List<ActivityManager.RunningAppProcessInfo> getRunningAppProcesses(); @@ -323,6 +325,7 @@ interface IActivityManager { int getLaunchedFromUid(in IBinder activityToken); void unstableProviderDied(in IBinder connection); boolean isIntentSenderAnActivity(in IIntentSender sender); + boolean isIntentSenderAForegroundService(in IIntentSender sender); int startActivityAsUser(in IApplicationThread caller, in String callingPackage, in Intent intent, in String resolvedType, in IBinder resultTo, in String resultWho, int requestCode, int flags, in ProfilerInfo profilerInfo, @@ -361,6 +364,15 @@ interface IActivityManager { void killUid(int appId, int userId, in String reason); void setUserIsMonkey(boolean monkey); void hang(in IBinder who, boolean allowRestart); + + /** + * Sets the windowing mode for a specific task. Only works on tasks of type + * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD} + * @param taskId The id of the task to set the windowing mode for. + * @param windowingMode The windowing mode to set for the task. + * @param toTop If the task should be moved to the top once the windowing mode changes. + */ + void setTaskWindowingMode(int taskId, int windowingMode, boolean toTop); void moveTaskToStack(int taskId, int stackId, boolean toTop); /** * Resizes the input stack id to the given bounds. @@ -380,7 +392,8 @@ interface IActivityManager { boolean preserveWindows, boolean animate, int animationDuration); List<ActivityManager.StackInfo> getAllStackInfos(); void setFocusedStack(int stackId); - ActivityManager.StackInfo getStackInfo(int stackId); + ActivityManager.StackInfo getFocusedStackInfo(); + ActivityManager.StackInfo getStackInfo(int windowingMode, int activityType); boolean convertFromTranslucent(in IBinder token); boolean convertToTranslucent(in IBinder token, in Bundle options); void notifyActivityDrawn(in IBinder token); @@ -399,9 +412,8 @@ interface IActivityManager { // Start of L transactions String getTagForIntentSender(in IIntentSender sender, in String prefix); boolean startUserInBackground(int userid); - void startLockTaskModeById(int taskId); void startLockTaskModeByToken(in IBinder token); - void stopLockTaskMode(); + void stopLockTaskModeByToken(in IBinder token); boolean isInLockTaskMode(); void setTaskDescription(in IBinder token, in ActivityManager.TaskDescription values); int startVoiceActivity(in String callingPackage, int callingPid, int callingUid, @@ -410,6 +422,9 @@ interface IActivityManager { in Bundle options, int userId); int startAssistantActivity(in String callingPackage, int callingPid, int callingUid, in Intent intent, in String resolvedType, in Bundle options, int userId); + int startRecentsActivity(in IAssistDataReceiver assistDataReceiver, in Bundle options, + in Bundle activityOptions, int userId); + int startActivityFromRecents(int taskId, in Bundle options); Bundle getActivityOptions(in IBinder token); List<IBinder> getAppTasks(in String callingPackage); void startSystemLockTaskMode(int taskId); @@ -417,7 +432,6 @@ interface IActivityManager { void finishVoiceTask(in IVoiceInteractionSession session); boolean isTopOfTask(in IBinder token); void notifyLaunchTaskBehindComplete(in IBinder token); - int startActivityFromRecents(int taskId, in Bundle options); void notifyEnterAnimationComplete(in IBinder token); int startActivityAsCaller(in IApplicationThread caller, in String callingPackage, in Intent intent, in String resolvedType, in IBinder resultTo, in String resultWho, @@ -436,14 +450,12 @@ interface IActivityManager { int checkPermissionWithToken(in String permission, int pid, int uid, in IBinder callerToken); void registerTaskStackListener(in ITaskStackListener listener); + void unregisterTaskStackListener(in ITaskStackListener listener); - - // Start of M transactions void notifyCleartextNetwork(int uid, in byte[] firstPacket); int createStackOnDisplay(int displayId); - int getFocusedStackId(); void setTaskResizeable(int taskId, int resizeableMode); - boolean requestAssistContextExtras(int requestType, in IResultReceiver receiver, + boolean requestAssistContextExtras(int requestType, in IAssistDataReceiver receiver, in Bundle receiverExtras, in IBinder activityToken, boolean focused, boolean newSessionId); void resizeTask(int taskId, in Rect bounds, int resizeMode); @@ -461,7 +473,7 @@ interface IActivityManager { /** * Notify the system that the keyguard is going away. * - * @param flags See {@link android.view.WindowManagerPolicy#KEYGUARD_GOING_AWAY_FLAG_TO_SHADE} + * @param flags See {@link android.view.WindowManagerPolicyConstants#KEYGUARD_GOING_AWAY_FLAG_TO_SHADE} * etc. */ void keyguardGoingAway(int flags); @@ -486,12 +498,23 @@ interface IActivityManager { * different stack. */ void positionTaskInStack(int taskId, int stackId, int position); - int getActivityStackId(in IBinder token); void exitFreeformMode(in IBinder token); void reportSizeConfigurations(in IBinder token, in int[] horizontalSizeConfiguration, in int[] verticalSizeConfigurations, in int[] smallestWidthConfigurations); - boolean moveTaskToDockedStack(int taskId, int createMode, boolean toTop, boolean animate, - in Rect initialBounds); + boolean setTaskWindowingModeSplitScreenPrimary(int taskId, int createMode, boolean toTop, + boolean animate, in Rect initialBounds, boolean showRecents); + /** + * Dismisses split-screen multi-window mode. + * {@param toTop} If true the current primary split-screen stack will be placed or left on top. + */ + void dismissSplitScreenMode(boolean toTop); + /** + * Dismisses PiP + * @param animate True if the dismissal should be animated. + * @param animationDuration The duration of the resize animation in milliseconds or -1 if the + * default animation duration should be used. + */ + void dismissPip(boolean animate, int animationDuration); void suppressResizeConfigChanges(boolean suppress); void moveTasksToFullscreenStack(int fromStackId, boolean onTop); boolean moveTopActivityToPinnedStack(int stackId, in Rect bounds); @@ -542,6 +565,13 @@ interface IActivityManager { void notifyPinnedStackAnimationStarted(); void notifyPinnedStackAnimationEnded(); void removeStack(int stackId); + /** + * Removes stacks in the input windowing modes from the system if they are of activity type + * ACTIVITY_TYPE_STANDARD or ACTIVITY_TYPE_UNDEFINED + */ + void removeStacksInWindowingModes(in int[] windowingModes); + /** Removes stack of the activity types from the system. */ + void removeStacksWithActivityTypes(in int[] activityTypes); void makePackageIdle(String packageName, int userId); int getMemoryTrimLevel(); /** @@ -555,11 +585,6 @@ interface IActivityManager { */ void resizePinnedStack(in Rect pinnedBounds, in Rect tempPinnedTaskBounds); boolean isVrModePackageEnabled(in ComponentName packageName); - /** - * Moves all tasks from the docked stack in the fullscreen stack and puts the top task of the - * fullscreen stack into the docked stack. - */ - void swapDockedAndFullscreenStack(); void notifyLockedProfile(int userId); void startConfirmDeviceCredentialIntent(in Intent intent, in Bundle options); void sendIdleJobTrigger(); @@ -592,9 +617,8 @@ interface IActivityManager { * @return Returns true if the configuration was updated. */ boolean updateDisplayOverrideConfiguration(in Configuration values, int displayId); - void unregisterTaskStackListener(ITaskStackListener listener); void moveStackToDisplay(int stackId, int displayId); - boolean requestAutofillData(in IResultReceiver receiver, in Bundle receiverExtras, + boolean requestAutofillData(in IAssistDataReceiver receiver, in Bundle receiverExtras, in IBinder activityToken, int flags); void dismissKeyguard(in IBinder token, in IKeyguardDismissCallback callback); int restartUserInBackground(int userId); @@ -631,7 +655,10 @@ interface IActivityManager { /** * Add a bare uid to the background restrictions whitelist. Only the system uid may call this. */ - void backgroundWhitelistUid(int uid); + void backgroundWhitelistUid(int uid); + + // Start of P transactions + void updateLockTaskFeatures(int userId, int flags); // WARNING: when these transactions are updated, check if they are any callers on the native // side. If so, make sure they are using the correct transaction ids and arguments. @@ -640,4 +667,10 @@ interface IActivityManager { void setShowWhenLocked(in IBinder token, boolean showWhenLocked); void setTurnScreenOn(in IBinder token, boolean turnScreenOn); + + /** + * Similar to {@link #startUserInBackground(int userId), but with a listener to report + * user unlock progress. + */ + boolean startUserInBackgroundWithListener(int userid, IProgressListener unlockProgressListener); } diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl index aeed7e1287d2..b25d7782652f 100644 --- a/core/java/android/app/IApplicationThread.aidl +++ b/core/java/android/app/IApplicationThread.aidl @@ -20,6 +20,7 @@ import android.app.IInstrumentationWatcher; import android.app.IUiAutomationConnection; import android.app.ProfilerInfo; import android.app.ResultInfo; +import android.app.servertransaction.ClientTransaction; import android.content.ComponentName; import android.content.IIntentReceiver; import android.content.Intent; @@ -52,24 +53,6 @@ import java.util.Map; * {@hide} */ oneway interface IApplicationThread { - void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving, - int configChanges, boolean dontReport); - void scheduleStopActivity(IBinder token, boolean showWindow, - int configChanges); - void scheduleWindowVisibility(IBinder token, boolean showWindow); - void scheduleResumeActivity(IBinder token, int procState, boolean isForward, - in Bundle resumeArgs); - void scheduleSendResult(IBinder token, in List<ResultInfo> results); - void scheduleLaunchActivity(in Intent intent, IBinder token, int ident, - in ActivityInfo info, in Configuration curConfig, in Configuration overrideConfig, - in CompatibilityInfo compatInfo, in String referrer, IVoiceInteractor voiceInteractor, - int procState, in Bundle state, in PersistableBundle persistentState, - in List<ResultInfo> pendingResults, in List<ReferrerIntent> pendingNewIntents, - boolean notResumed, boolean isForward, in ProfilerInfo profilerInfo); - void scheduleNewIntent( - in List<ReferrerIntent> intent, IBinder token, boolean andPause); - void scheduleDestroyActivity(IBinder token, boolean finished, - int configChanges); void scheduleReceiver(in Intent intent, in ActivityInfo info, in CompatibilityInfo compatInfo, int resultCode, in String data, in Bundle extras, boolean sync, @@ -85,8 +68,8 @@ oneway interface IApplicationThread { boolean restrictedBackupMode, boolean persistent, in Configuration config, in CompatibilityInfo compatInfo, in Map services, in Bundle coreSettings, in String buildSerial); + void runIsolatedEntryPoint(in String entryPoint, in String[] entryPointArgs); void scheduleExit(); - void scheduleConfigurationChanged(in Configuration config); void scheduleServiceArgs(IBinder token, in ParceledListSlice args); void updateTimeZone(); void processInBackground(); @@ -100,9 +83,6 @@ oneway interface IApplicationThread { int resultCode, in String data, in Bundle extras, boolean ordered, boolean sticky, int sendingUser, int processState); void scheduleLowMemory(); - void scheduleActivityConfigurationChanged(IBinder token, in Configuration overrideConfig); - void scheduleActivityMovedToDisplay(IBinder token, int displayId, - in Configuration overrideConfig); void scheduleRelaunchActivity(IBinder token, in List<ResultInfo> pendingResults, in List<ReferrerIntent> pendingNewIntents, int configChanges, boolean notResumed, in Configuration config, in Configuration overrideConfig, boolean preserveWindow); @@ -145,14 +125,11 @@ oneway interface IApplicationThread { void notifyCleartextNetwork(in byte[] firstPacket); void startBinderTracking(); void stopBinderTrackingAndDump(in ParcelFileDescriptor fd); - void scheduleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode, - in Configuration newConfig); - void schedulePictureInPictureModeChanged(IBinder token, boolean isInPictureInPictureMode, - in Configuration newConfig); void scheduleLocalVoiceInteractionStarted(IBinder token, IVoiceInteractor voiceInteractor); void handleTrustStorageUpdate(); void attachAgent(String path); void scheduleApplicationInfoChanged(in ApplicationInfo ai); void setNetworkBlockSeq(long procStateSeq); + void scheduleTransaction(in ClientTransaction transaction); } diff --git a/core/java/android/app/IAssistDataReceiver.aidl b/core/java/android/app/IAssistDataReceiver.aidl new file mode 100644 index 000000000000..2d5daf97a1c4 --- /dev/null +++ b/core/java/android/app/IAssistDataReceiver.aidl @@ -0,0 +1,26 @@ +/* + * 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.app; + +import android.graphics.Bitmap; +import android.os.Bundle; + +/** @hide */ +oneway interface IAssistDataReceiver { + void onHandleAssistData(in Bundle resultData); + void onHandleAssistScreenshot(in Bitmap screenshot); +} diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl index a07374b80408..4a85efd9e4c2 100644 --- a/core/java/android/app/IBackupAgent.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -79,7 +79,7 @@ oneway interface IBackupAgent { * passed here as a convenience to the agent. */ void doRestore(in ParcelFileDescriptor data, - int appVersionCode, in ParcelFileDescriptor newState, + long appVersionCode, in ParcelFileDescriptor newState, int token, IBackupManager callbackBinder); /** diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 0c80deaba910..d1aacad30a64 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -61,6 +61,8 @@ interface INotificationManager void createNotificationChannelsForPackage(String pkg, int uid, in ParceledListSlice channelsList); ParceledListSlice getNotificationChannelGroupsForPackage(String pkg, int uid, boolean includeDeleted); NotificationChannelGroup getNotificationChannelGroupForPackage(String groupId, String pkg, int uid); + NotificationChannelGroup getPopulatedNotificationChannelGroupForPackage(String pkg, int uid, String groupId, boolean includeDeleted); + void updateNotificationChannelGroupForPackage(String pkg, int uid, in NotificationChannelGroup group); void updateNotificationChannelForPackage(String pkg, int uid, in NotificationChannel channel); NotificationChannel getNotificationChannel(String pkg, String channelId); NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, boolean includeDeleted); @@ -70,6 +72,7 @@ interface INotificationManager int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted); int getDeletedChannelCount(String pkg, int uid); void deleteNotificationChannelGroup(String pkg, String channelGroupId); + NotificationChannelGroup getNotificationChannelGroup(String pkg, String channelGroupId); ParceledListSlice getNotificationChannelGroups(String pkg); boolean onlyHasDefaultChannel(String pkg, int uid); @@ -103,6 +106,7 @@ interface INotificationManager void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim); void setInterruptionFilter(String pkg, int interruptionFilter); + void updateNotificationChannelGroupFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannelGroup group); void updateNotificationChannelFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannel channel); ParceledListSlice getNotificationChannelsFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user); ParceledListSlice getNotificationChannelGroupsFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user); diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl index a56965bdbd4d..2e1e9889eadc 100644 --- a/core/java/android/app/ITaskStackListener.aidl +++ b/core/java/android/app/ITaskStackListener.aidl @@ -30,7 +30,7 @@ oneway interface ITaskStackListener { void onTaskStackChanged(); /** Called whenever an Activity is moved to the pinned stack from another stack. */ - void onActivityPinned(String packageName, int userId, int taskId); + void onActivityPinned(String packageName, int userId, int taskId, int stackId); /** Called whenever an Activity is moved from the pinned stack to another stack. */ void onActivityUnpinned(); diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl index b26117d3d31c..d01938b123b1 100644 --- a/core/java/android/app/IUiAutomationConnection.aidl +++ b/core/java/android/app/IUiAutomationConnection.aidl @@ -18,6 +18,7 @@ package android.app; import android.accessibilityservice.IAccessibilityServiceClient; import android.graphics.Bitmap; +import android.graphics.Rect; import android.view.InputEvent; import android.view.WindowContentFrameStats; import android.view.WindowAnimationFrameStats; @@ -37,7 +38,7 @@ interface IUiAutomationConnection { void disconnect(); boolean injectInputEvent(in InputEvent event, boolean sync); boolean setRotation(int rotation); - Bitmap takeScreenshot(int width, int height); + Bitmap takeScreenshot(in Rect crop, int rotation); boolean clearWindowContentFrameStats(int windowId); WindowContentFrameStats getWindowContentFrameStats(int windowId); void clearWindowAnimationFrameStats(); diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index e260967f92d0..d49e11f47fea 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/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/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index 54f74b15c501..d0f84c8ee0bb 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -382,20 +382,16 @@ public class KeyguardManager { } /** + * @deprecated Use {@link #isKeyguardLocked()} instead. + * * If keyguard screen is showing or in restricted key input mode (i.e. in * keyguard password emergency screen). When in such mode, certain keys, * such as the Home key and the right soft keys, don't work. * * @return true if in keyguard restricted input mode. - * - * @see android.view.WindowManagerPolicy#inKeyguardRestrictedKeyInputMode */ public boolean inKeyguardRestrictedInputMode() { - try { - return mWM.inKeyguardRestrictedInputMode(); - } catch (RemoteException ex) { - return false; - } + return isKeyguardLocked(); } /** diff --git a/core/java/android/app/ListFragment.java b/core/java/android/app/ListFragment.java index 0b96d84d216a..90b77b398f44 100644 --- a/core/java/android/app/ListFragment.java +++ b/core/java/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/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index f6d9710dae69..ebd101494588 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -638,8 +638,7 @@ public final class LoadedApk { final String defaultSearchPaths = System.getProperty("java.library.path"); final boolean treatVendorApkAsUnbundled = !defaultSearchPaths.contains("/vendor/lib"); if (mApplicationInfo.getCodePath() != null - && mApplicationInfo.getCodePath().startsWith("/vendor/") - && treatVendorApkAsUnbundled) { + && mApplicationInfo.isVendor() && treatVendorApkAsUnbundled) { isBundledApp = false; } @@ -1647,9 +1646,12 @@ public final class LoadedApk { if (dead) { mConnection.onBindingDied(name); } - // If there is a new service, it is now connected. + // If there is a new viable service, it is now connected. if (service != null) { mConnection.onServiceConnected(name, service); + } else { + // The binding machinery worked, but the remote returned null from onBind(). + mConnection.onNullBinding(name); } } diff --git a/core/java/android/app/LoaderManager.java b/core/java/android/app/LoaderManager.java index 56dfc589b0a7..7969684ab5a9 100644 --- a/core/java/android/app/LoaderManager.java +++ b/core/java/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/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java index 3b273bc1c4b6..998ac5f2a487 100644 --- a/core/java/android/app/LocalActivityManager.java +++ b/core/java/android/app/LocalActivityManager.java @@ -22,6 +22,7 @@ import android.os.Binder; import android.os.Bundle; import android.util.Log; import android.view.Window; + import com.android.internal.content.ReferrerIntent; import java.util.ArrayList; @@ -161,12 +162,12 @@ public class LocalActivityManager { case CREATED: if (desiredState == STARTED) { if (localLOGV) Log.v(TAG, r.id + ": restarting"); - mActivityThread.performRestartActivity(r); + mActivityThread.performRestartActivity(r, true /* start */); r.curState = STARTED; } if (desiredState == RESUMED) { if (localLOGV) Log.v(TAG, r.id + ": restarting and resuming"); - mActivityThread.performRestartActivity(r); + mActivityThread.performRestartActivity(r, true /* start */); mActivityThread.performResumeActivity(r, true, "moveToState-CREATED"); r.curState = RESUMED; } @@ -207,7 +208,7 @@ public class LocalActivityManager { private void performPause(LocalActivityRecord r, boolean finishing) { final boolean needState = r.instanceState == null; final Bundle instanceState = mActivityThread.performPauseActivity( - r, finishing, needState, "performPause"); + r, finishing, needState, "performPause", null /* pendingActions */); if (needState) { r.instanceState = instanceState; } @@ -361,7 +362,8 @@ public class LocalActivityManager { performPause(r, finish); } if (localLOGV) Log.v(TAG, r.id + ": destroying"); - mActivityThread.performDestroyActivity(r, finish); + mActivityThread.performDestroyActivity(r, finish, 0 /* configChanges */, + false /* getNonConfigInstance */); r.activity = null; r.window = null; if (finish) { @@ -625,7 +627,8 @@ public class LocalActivityManager { for (int i=0; i<N; i++) { LocalActivityRecord r = mActivityArray.get(i); if (localLOGV) Log.v(TAG, r.id + ": destroying"); - mActivityThread.performDestroyActivity(r, finishing); + mActivityThread.performDestroyActivity(r, finishing, 0 /* configChanges */, + false /* getNonConfigInstance */); } mActivities.clear(); mActivityArray.clear(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 7caeca3da6f8..fb9efe6f321d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/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; @@ -67,6 +68,7 @@ import android.text.style.TextAppearanceSpan; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; +import android.util.proto.ProtoOutputStream; import android.view.Gravity; import android.view.NotificationHeaderView; import android.view.View; @@ -124,6 +126,13 @@ public class Notification implements Parcelable /** * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will + * contain a {@link NotificationChannelGroup#getId() group id} that can be used to narrow down + * what settings should be shown in the target app. + */ + public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID"; + + /** + * Optional extra for {@link #INTENT_CATEGORY_NOTIFICATION_PREFERENCES}. If provided, will * contain the tag provided to {@link NotificationManager#notify(String, int, Notification)} * that can be used to narrow down what settings should be shown in the target app. */ @@ -851,7 +860,7 @@ public class Notification implements Parcelable * * @hide */ - static public IBinder whitelistToken; + private IBinder mWhitelistToken; /** * Must be set by a process to start associating tokens with Notification objects @@ -1869,12 +1878,12 @@ public class Notification implements Parcelable { int version = parcel.readInt(); - whitelistToken = parcel.readStrongBinder(); - if (whitelistToken == null) { - whitelistToken = processWhitelistToken; + mWhitelistToken = parcel.readStrongBinder(); + if (mWhitelistToken == null) { + mWhitelistToken = processWhitelistToken; } // Propagate this token to all pending intents that are unmarshalled from the parcel. - parcel.setClassCookie(PendingIntent.class, whitelistToken); + parcel.setClassCookie(PendingIntent.class, mWhitelistToken); when = parcel.readLong(); creationTime = parcel.readLong(); @@ -1932,6 +1941,7 @@ public class Notification implements Parcelable mSortKey = parcel.readString(); extras = Bundle.setDefusable(parcel.readBundle(), true); // may be null + fixDuplicateExtras(); actions = parcel.createTypedArray(Action.CREATOR); // may be null @@ -1982,7 +1992,7 @@ public class Notification implements Parcelable * @hide */ public void cloneInto(Notification that, boolean heavy) { - that.whitelistToken = this.whitelistToken; + that.mWhitelistToken = this.mWhitelistToken; that.when = this.when; that.creationTime = this.creationTime; that.mSmallIcon = this.mSmallIcon; @@ -2212,7 +2222,7 @@ public class Notification implements Parcelable private void writeToParcelImpl(Parcel parcel, int flags) { parcel.writeInt(1); - parcel.writeStrongBinder(whitelistToken); + parcel.writeStrongBinder(mWhitelistToken); parcel.writeLong(when); parcel.writeLong(creationTime); if (mSmallIcon == null && icon != 0) { @@ -2380,6 +2390,33 @@ public class Notification implements Parcelable }; /** + * Parcelling creates multiple copies of objects in {@code extras}. Fix them. + * <p> + * For backwards compatibility {@code extras} holds some references to "real" member data such + * as {@link getLargeIcon()} which is mirrored by {@link #EXTRA_LARGE_ICON}. This is mostly + * fine as long as the object stays in one process. + * <p> + * However, once the notification goes into a parcel each reference gets marshalled separately, + * wasting memory. Especially with large images on Auto and TV, this is worth fixing. + */ + private void fixDuplicateExtras() { + if (extras != null) { + fixDuplicateExtra(mSmallIcon, EXTRA_SMALL_ICON); + fixDuplicateExtra(mLargeIcon, EXTRA_LARGE_ICON); + } + } + + /** + * If we find an extra that's exactly the same as one of the "real" fields but refers to a + * separate object, replace it with the field's version to avoid holding duplicate copies. + */ + private void fixDuplicateExtra(@Nullable Parcelable original, @NonNull String extraName) { + if (original != null && extras.getParcelable(extraName) != null) { + extras.putParcelable(extraName, original); + } + } + + /** * Sets the {@link #contentView} field to be a view with the standard "Latest Event" * layout. * @@ -2439,6 +2476,30 @@ public class Notification implements Parcelable notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, ai); } + /** + * @hide + */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + proto.write(NotificationProto.CHANNEL_ID, getChannelId()); + proto.write(NotificationProto.HAS_TICKER_TEXT, this.tickerText != null); + proto.write(NotificationProto.FLAGS, this.flags); + proto.write(NotificationProto.COLOR, this.color); + proto.write(NotificationProto.CATEGORY, this.category); + proto.write(NotificationProto.GROUP_KEY, this.mGroupKey); + proto.write(NotificationProto.SORT_KEY, this.mSortKey); + if (this.actions != null) { + proto.write(NotificationProto.ACTION_LENGTH, this.actions.length); + } + if (this.visibility >= VISIBILITY_SECRET && this.visibility <= VISIBILITY_PUBLIC) { + proto.write(NotificationProto.VISIBILITY, this.visibility); + } + if (publicVersion != null) { + publicVersion.writeToProto(proto, NotificationProto.PUBLIC_VERSION); + } + proto.end(token); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -3832,8 +3893,8 @@ public class Notification implements Parcelable contentView.setImageViewBitmap(R.id.profile_badge, profileBadge); contentView.setViewVisibility(R.id.profile_badge, View.VISIBLE); if (isColorized()) { - contentView.setDrawableParameters(R.id.profile_badge, false, -1, - getPrimaryTextColor(), PorterDuff.Mode.SRC_ATOP, -1); + contentView.setDrawableTint(R.id.profile_badge, false, + getPrimaryTextColor(), PorterDuff.Mode.SRC_ATOP); } } } @@ -3893,7 +3954,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); @@ -4103,11 +4164,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); @@ -4115,36 +4178,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(); - contentView.setDrawableParameters(R.id.reply_icon_action, + } + // 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 */, - -1, - contrastColor, - PorterDuff.Mode.SRC_ATOP, -1); - int iconColor = NotificationColorUtil.isColorLight(contrastColor) - ? Color.BLACK : Color.WHITE; - contentView.setDrawableParameters(R.id.reply_icon_action, - false /* targetBackground */, - -1, - iconColor, - PorterDuff.Mode.SRC_ATOP, -1); + contrastColor, 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); } @@ -4178,8 +4250,8 @@ public class Notification implements Parcelable private void bindExpandButton(RemoteViews contentView) { int color = getPrimaryHighlightColor(); - contentView.setDrawableParameters(R.id.expand_button, false, -1, color, - PorterDuff.Mode.SRC_ATOP, -1); + contentView.setDrawableTint(R.id.expand_button, false, color, + PorterDuff.Mode.SRC_ATOP); contentView.setInt(R.id.notification_header, "setOriginalNotificationColor", color); } @@ -4286,8 +4358,7 @@ public class Notification implements Parcelable mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon); } contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon); - contentView.setDrawableParameters(R.id.icon, false /* targetBackground */, - -1 /* alpha */, -1 /* colorFilter */, null /* mode */, mN.iconLevel); + contentView.setInt(R.id.icon, "setImageLevel", mN.iconLevel); processSmallIconColor(mN.mSmallIcon, contentView, ambient); } @@ -4677,8 +4748,8 @@ public class Notification implements Parcelable bgColor = mContext.getColor(oddAction ? R.color.notification_action_list : R.color.notification_action_list_dark); } - button.setDrawableParameters(R.id.button_holder, true, -1, bgColor, - PorterDuff.Mode.SRC_ATOP, -1); + button.setDrawableTint(R.id.button_holder, true, + bgColor, PorterDuff.Mode.SRC_ATOP); CharSequence title = action.title; ColorStateList[] outResultColor = null; if (isLegacy()) { @@ -4811,8 +4882,8 @@ public class Notification implements Parcelable boolean colorable = !isLegacy() || getColorUtil().isGrayscaleIcon(mContext, smallIcon); int color = ambient ? resolveAmbientColor() : getPrimaryHighlightColor(); if (colorable) { - contentView.setDrawableParameters(R.id.icon, false, -1, color, - PorterDuff.Mode.SRC_ATOP, -1); + contentView.setDrawableTint(R.id.icon, false, color, + PorterDuff.Mode.SRC_ATOP); } contentView.setInt(R.id.notification_header, "setOriginalIconColor", @@ -4828,8 +4899,8 @@ public class Notification implements Parcelable if (largeIcon != null && isLegacy() && getColorUtil().isGrayscaleIcon(mContext, largeIcon)) { // resolve color will fall back to the default when legacy - contentView.setDrawableParameters(R.id.icon, false, -1, resolveContrastColor(), - PorterDuff.Mode.SRC_ATOP, -1); + contentView.setDrawableTint(R.id.icon, false, resolveContrastColor(), + PorterDuff.Mode.SRC_ATOP); } } @@ -4979,6 +5050,8 @@ public class Notification implements Parcelable mN.flags |= FLAG_SHOW_LIGHTS; } + mN.allPendingIntents = null; + return mN; } @@ -6051,18 +6124,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); } /** @@ -6070,38 +6137,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; } @@ -6111,118 +6174,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; } /** @@ -6230,19 +6257,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) { @@ -6390,7 +6407,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) { @@ -6743,8 +6768,8 @@ public class Notification implements Parcelable : NotificationColorUtil.resolveColor(mBuilder.mContext, Notification.COLOR_DEFAULT); - button.setDrawableParameters(R.id.action0, false, -1, tintColor, - PorterDuff.Mode.SRC_ATOP, -1); + button.setDrawableTint(R.id.action0, false, tintColor, + PorterDuff.Mode.SRC_ATOP); if (!tombstone) { button.setOnClickPendingIntent(R.id.action0, action.actionIntent); } @@ -8483,6 +8508,8 @@ public class Notification implements Parcelable boolean ambient = false; CharSequence title; CharSequence text; + boolean hideLargeIcon; + public boolean alwaysShowReply; final StandardTemplateParams reset() { hasProgress = true; @@ -8507,6 +8534,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; @@ -8523,7 +8560,6 @@ public class Notification implements Parcelable text = extras.getCharSequence(EXTRA_TEXT); } this.text = b.processLegacyText(text, ambient); - return this; } } diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index 556acdcfff81..c06ad3f32cf4 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -28,6 +28,9 @@ import android.os.Parcelable; import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.text.TextUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.Preconditions; import com.android.internal.util.Preconditions; @@ -140,12 +143,15 @@ public final class NotificationChannel implements Parcelable { private boolean mLights; private int mLightColor = DEFAULT_LIGHT_COLOR; private long[] mVibration; + // Bitwise representation of fields that have been changed by the user, preventing the app from + // making changes to these fields. private int mUserLockedFields; private boolean mVibrationEnabled; private boolean mShowBadge = DEFAULT_SHOW_BADGE; private boolean mDeleted = DEFAULT_DELETED; private String mGroup; private AudioAttributes mAudioAttributes = Notification.AUDIO_ATTRIBUTES_DEFAULT; + // If this is a blockable system notification channel. private boolean mBlockableSystem = false; /** @@ -908,24 +914,55 @@ public final class NotificationChannel implements Parcelable { @Override public String toString() { - return "NotificationChannel{" + - "mId='" + mId + '\'' + - ", mName=" + mName + - ", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "") + - ", mImportance=" + mImportance + - ", mBypassDnd=" + mBypassDnd + - ", mLockscreenVisibility=" + mLockscreenVisibility + - ", mSound=" + mSound + - ", mLights=" + mLights + - ", mLightColor=" + mLightColor + - ", mVibration=" + Arrays.toString(mVibration) + - ", mUserLockedFields=" + mUserLockedFields + - ", mVibrationEnabled=" + mVibrationEnabled + - ", mShowBadge=" + mShowBadge + - ", mDeleted=" + mDeleted + - ", mGroup='" + mGroup + '\'' + - ", mAudioAttributes=" + mAudioAttributes + - ", mBlockableSystem=" + mBlockableSystem + - '}'; + return "NotificationChannel{" + + "mId='" + mId + '\'' + + ", mName=" + mName + + ", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "") + + ", mImportance=" + mImportance + + ", mBypassDnd=" + mBypassDnd + + ", mLockscreenVisibility=" + mLockscreenVisibility + + ", mSound=" + mSound + + ", mLights=" + mLights + + ", mLightColor=" + mLightColor + + ", mVibration=" + Arrays.toString(mVibration) + + ", mUserLockedFields=" + Integer.toHexString(mUserLockedFields) + + ", mVibrationEnabled=" + mVibrationEnabled + + ", mShowBadge=" + mShowBadge + + ", mDeleted=" + mDeleted + + ", mGroup='" + mGroup + '\'' + + ", mAudioAttributes=" + mAudioAttributes + + ", mBlockableSystem=" + mBlockableSystem + + '}'; + } + + /** @hide */ + public void toProto(ProtoOutputStream proto) { + proto.write(NotificationChannelProto.ID, mId); + proto.write(NotificationChannelProto.NAME, mName); + proto.write(NotificationChannelProto.DESCRIPTION, mDesc); + proto.write(NotificationChannelProto.IMPORTANCE, mImportance); + proto.write(NotificationChannelProto.CAN_BYPASS_DND, mBypassDnd); + proto.write(NotificationChannelProto.LOCKSCREEN_VISIBILITY, mLockscreenVisibility); + if (mSound != null) { + proto.write(NotificationChannelProto.SOUND, mSound.toString()); + } + proto.write(NotificationChannelProto.USE_LIGHTS, mLights); + proto.write(NotificationChannelProto.LIGHT_COLOR, mLightColor); + if (mVibration != null) { + for (long v : mVibration) { + proto.write(NotificationChannelProto.VIBRATION, v); + } + } + proto.write(NotificationChannelProto.USER_LOCKED_FIELDS, mUserLockedFields); + proto.write(NotificationChannelProto.IS_VIBRATION_ENABLED, mVibrationEnabled); + proto.write(NotificationChannelProto.SHOW_BADGE, mShowBadge); + proto.write(NotificationChannelProto.IS_DELETED, mDeleted); + proto.write(NotificationChannelProto.GROUP, mGroup); + if (mAudioAttributes != null) { + long aToken = proto.start(NotificationChannelProto.AUDIO_ATTRIBUTES); + mAudioAttributes.toProto(proto); + proto.end(aToken); + } + proto.write(NotificationChannelProto.IS_BLOCKABLE_SYSTEM, mBlockableSystem); } } diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java index 18ad9cf3d8e3..5cb7fb7a6707 100644 --- a/core/java/android/app/NotificationChannelGroup.java +++ b/core/java/android/app/NotificationChannelGroup.java @@ -16,13 +16,16 @@ package android.app; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.proto.ProtoOutputStream; import org.json.JSONException; import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; @@ -42,10 +45,14 @@ public final class NotificationChannelGroup implements Parcelable { private static final String TAG_GROUP = "channelGroup"; private static final String ATT_NAME = "name"; + private static final String ATT_DESC = "desc"; private static final String ATT_ID = "id"; + private static final String ATT_BLOCKED = "blocked"; private final String mId; private CharSequence mName; + private String mDescription; + private boolean mBlocked; private List<NotificationChannel> mChannels = new ArrayList<>(); /** @@ -73,7 +80,13 @@ public final class NotificationChannelGroup implements Parcelable { mId = null; } mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + if (in.readByte() != 0) { + mDescription = in.readString(); + } else { + mDescription = null; + } in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader()); + mBlocked = in.readBoolean(); } private String getTrimmedString(String input) { @@ -92,24 +105,38 @@ public final class NotificationChannelGroup implements Parcelable { dest.writeByte((byte) 0); } TextUtils.writeToParcel(mName, dest, flags); + if (mDescription != null) { + dest.writeByte((byte) 1); + dest.writeString(mDescription); + } else { + dest.writeByte((byte) 0); + } dest.writeParcelableList(mChannels, flags); + dest.writeBoolean(mBlocked); } /** - * Returns the id of this channel. + * Returns the id of this group. */ public String getId() { return mId; } /** - * Returns the user visible name of this channel. + * Returns the user visible name of this group. */ public CharSequence getName() { return mName; } /** + * Returns the user visible description of this group. + */ + public String getDescription() { + return mDescription; + } + + /** * Returns the list of channels that belong to this group */ public List<NotificationChannel> getChannels() { @@ -117,6 +144,32 @@ public final class NotificationChannelGroup implements Parcelable { } /** + * Returns whether or not notifications posted to {@link NotificationChannel channels} belonging + * to this group are blocked. + */ + public boolean isBlocked() { + return mBlocked; + } + + /** + * Sets the user visible description of this group. + * + * <p>The recommended maximum length is 300 characters; the value may be truncated if it is too + * long. + */ + public void setDescription(String description) { + mDescription = getTrimmedString(description); + } + + /** + * @hide + */ + @TestApi + public void setBlocked(boolean blocked) { + mBlocked = blocked; + } + + /** * @hide */ public void addChannel(NotificationChannel channel) { @@ -126,6 +179,28 @@ public final class NotificationChannelGroup implements Parcelable { /** * @hide */ + public void setChannels(List<NotificationChannel> channels) { + mChannels = channels; + } + + /** + * @hide + */ + public void populateFromXml(XmlPullParser parser) { + // Name, id, and importance are set in the constructor. + setDescription(parser.getAttributeValue(null, ATT_DESC)); + setBlocked(safeBool(parser, ATT_BLOCKED, false)); + } + + private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) { + final String value = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(value)) return defValue; + return Boolean.parseBoolean(value); + } + + /** + * @hide + */ public void writeXml(XmlSerializer out) throws IOException { out.startTag(null, TAG_GROUP); @@ -133,6 +208,10 @@ public final class NotificationChannelGroup implements Parcelable { if (getName() != null) { out.attribute(null, ATT_NAME, getName().toString()); } + if (getDescription() != null) { + out.attribute(null, ATT_DESC, getDescription().toString()); + } + out.attribute(null, ATT_BLOCKED, Boolean.toString(isBlocked())); out.endTag(null, TAG_GROUP); } @@ -145,6 +224,8 @@ public final class NotificationChannelGroup implements Parcelable { JSONObject record = new JSONObject(); record.put(ATT_ID, getId()); record.put(ATT_NAME, getName()); + record.put(ATT_DESC, getDescription()); + record.put(ATT_BLOCKED, isBlocked()); return record; } @@ -173,31 +254,57 @@ public final class NotificationChannelGroup implements Parcelable { NotificationChannelGroup that = (NotificationChannelGroup) o; + if (isBlocked() != that.isBlocked()) return false; if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false; if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) { return false; } - return true; - } - - @Override - public NotificationChannelGroup clone() { - return new NotificationChannelGroup(getId(), getName()); + if (getDescription() != null ? !getDescription().equals(that.getDescription()) + : that.getDescription() != null) { + return false; + } + return getChannels() != null ? getChannels().equals(that.getChannels()) + : that.getChannels() == null; } @Override public int hashCode() { int result = getId() != null ? getId().hashCode() : 0; result = 31 * result + (getName() != null ? getName().hashCode() : 0); + result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0); + result = 31 * result + (isBlocked() ? 1 : 0); + result = 31 * result + (getChannels() != null ? getChannels().hashCode() : 0); return result; } @Override + public NotificationChannelGroup clone() { + NotificationChannelGroup cloned = new NotificationChannelGroup(getId(), getName()); + cloned.setDescription(getDescription()); + cloned.setBlocked(isBlocked()); + cloned.setChannels(getChannels()); + return cloned; + } + + @Override public String toString() { - return "NotificationChannelGroup{" + - "mId='" + mId + '\'' + - ", mName=" + mName + - ", mChannels=" + mChannels + - '}'; + return "NotificationChannelGroup{" + + "mId='" + mId + '\'' + + ", mName=" + mName + + ", mDescription=" + (!TextUtils.isEmpty(mDescription) ? "hasDescription " : "") + + ", mBlocked=" + mBlocked + + ", mChannels=" + mChannels + + '}'; + } + + /** @hide */ + public void toProto(ProtoOutputStream proto) { + proto.write(NotificationChannelGroupProto.ID, mId); + proto.write(NotificationChannelGroupProto.NAME, mName.toString()); + proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription); + proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked); + for (NotificationChannel channel : mChannels) { + channel.toProto(proto); + } } } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 34343e9e106a..659cf169e994 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -41,6 +41,7 @@ import android.provider.Settings.Global; import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.util.Log; +import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -92,6 +93,58 @@ public class NotificationManager { private static boolean localLOGV = false; /** + * Intent that is broadcast when a {@link NotificationChannel} is blocked + * (when {@link NotificationChannel#getImportance()} is {@link #IMPORTANCE_NONE}) or unblocked + * (when {@link NotificationChannel#getImportance()} is anything other than + * {@link #IMPORTANCE_NONE}). + * + * This broadcast is only sent to the app that owns the channel that has changed. + * + * Input: nothing + * Output: {@link #EXTRA_BLOCK_STATE_CHANGED_ID} + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED = + "android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED"; + + /** + * Extra for {@link #ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED} or + * {@link #ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED} containing the id of the + * object which has a new blocked state. + * + * The value will be the {@link NotificationChannel#getId()} of the channel for + * {@link #ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED} and + * the {@link NotificationChannelGroup#getId()} of the group for + * {@link #ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED}. + */ + public static final String EXTRA_BLOCK_STATE_CHANGED_ID = + "android.app.extra.BLOCK_STATE_CHANGED_ID"; + + /** + * Extra for {@link #ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED} or + * {@link #ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED} containing the new blocked + * state as a boolean. + * + * The value will be {@code true} if this channel or group is now blocked and {@code false} if + * this channel or group is now unblocked. + */ + public static final String EXTRA_BLOCKED_STATE = "android.app.extra.BLOCKED_STATE"; + + + /** + * Intent that is broadcast when a {@link NotificationChannelGroup} is + * {@link NotificationChannelGroup#isBlocked() blocked} or unblocked. + * + * This broadcast is only sent to the app that owns the channel group that has changed. + * + * Input: nothing + * Output: {@link #EXTRA_BLOCK_STATE_CHANGED_ID} + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED = + "android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED"; + + /** * Intent that is broadcast when the state of {@link #getEffectsSuppressor()} changes. * This broadcast is only sent to registered receivers. * @@ -421,7 +474,7 @@ public class NotificationManager { * Creates a notification channel that notifications can be posted to. * * This can also be used to restore a deleted channel and to update an existing channel's - * name, description, and/or importance. + * name, description, group, and/or importance. * * <p>The name and description should only be changed if the locale changes * or in response to the user renaming this channel. For example, if a user has a channel @@ -431,6 +484,9 @@ public class NotificationManager { * <p>The importance of an existing channel will only be changed if the new importance is lower * than the current value and the user has not altered any settings on this channel. * + * <p>The group an existing channel will only be changed if the channel does not already + * belong to a group. + * * All other fields are ignored for channels that already exist. * * @param channel the channel to create. Note that the created channel may differ from this @@ -500,6 +556,20 @@ public class NotificationManager { } /** + * Returns the notification channel group settings for a given channel group id. + * + * The channel group must belong to your package, or null will be returned. + */ + public NotificationChannelGroup getNotificationChannelGroup(String channelGroupId) { + INotificationManager service = getService(); + try { + return service.getNotificationChannelGroup(mContext.getPackageName(), channelGroupId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Returns all notification channel groups belonging to the calling app. */ public List<NotificationChannelGroup> getNotificationChannelGroups() { @@ -754,10 +824,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 @@ -835,8 +905,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(); @@ -930,8 +998,14 @@ public class NotificationManager { public static final int PRIORITY_CATEGORY_CALLS = 1 << 3; /** Calls from repeat callers are prioritized. */ public static final int PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 4; + /** Alarms are prioritized */ + public static final int PRIORITY_CATEGORY_ALARMS = 1 << 5; + /** Media, system, game (catch-all for non-never suppressible sounds) are prioritized */ + public static final int PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER = 1 << 6; private static final int[] ALL_PRIORITY_CATEGORIES = { + PRIORITY_CATEGORY_ALARMS, + PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER, PRIORITY_CATEGORY_REMINDERS, PRIORITY_CATEGORY_EVENTS, PRIORITY_CATEGORY_MESSAGES, @@ -1058,6 +1132,27 @@ public class NotificationManager { + "]"; } + /** @hide */ + public void toProto(ProtoOutputStream proto, long fieldId) { + final long pToken = proto.start(fieldId); + + bitwiseToProtoEnum(proto, PolicyProto.PRIORITY_CATEGORIES, priorityCategories); + proto.write(PolicyProto.PRIORITY_CALL_SENDER, priorityCallSenders); + proto.write(PolicyProto.PRIORITY_MESSAGE_SENDER, priorityMessageSenders); + bitwiseToProtoEnum( + proto, PolicyProto.SUPPRESSED_VISUAL_EFFECTS, suppressedVisualEffects); + + proto.end(pToken); + } + + private static void bitwiseToProtoEnum(ProtoOutputStream proto, long fieldId, int data) { + for (int i = 1; data > 0; ++i, data >>>= 1) { + if ((data & 1) == 1) { + proto.write(fieldId, i); + } + } + } + public static String suppressedEffectsToString(int effects) { if (effects <= 0) return ""; final StringBuilder sb = new StringBuilder(); @@ -1110,6 +1205,9 @@ public class NotificationManager { case PRIORITY_CATEGORY_MESSAGES: return "PRIORITY_CATEGORY_MESSAGES"; case PRIORITY_CATEGORY_CALLS: return "PRIORITY_CATEGORY_CALLS"; case PRIORITY_CATEGORY_REPEAT_CALLERS: return "PRIORITY_CATEGORY_REPEAT_CALLERS"; + case PRIORITY_CATEGORY_ALARMS: return "PRIORITY_CATEGORY_ALARMS"; + case PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER: + return "PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER"; default: return "PRIORITY_CATEGORY_UNKNOWN_" + priorityCategory; } } diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index b7d3f578df27..8b76cc7c966b 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -20,20 +20,20 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; -import android.content.Intent; import android.content.IIntentReceiver; import android.content.IIntentSender; +import android.content.Intent; import android.content.IntentSender; import android.os.Bundle; -import android.os.Looper; -import android.os.RemoteException; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; -import android.os.Process; +import android.os.RemoteException; import android.os.UserHandle; import android.util.AndroidException; +import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1012,6 +1012,19 @@ public final class PendingIntent implements Parcelable { /** * @hide + * Check whether this PendingIntent will launch a foreground service + */ + public boolean isForegroundService() { + try { + return ActivityManager.getService() + .isIntentSenderAForegroundService(mTarget); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide * Return the Intent of this PendingIntent. */ public Intent getIntent() { @@ -1069,7 +1082,16 @@ public final class PendingIntent implements Parcelable { sb.append('}'); return sb.toString(); } - + + /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + if (mTarget != null) { + proto.write(PendingIntentProto.TARGET, mTarget.asBinder().toString()); + } + proto.end(token); + } + public int describeContents() { return 0; } @@ -1107,8 +1129,13 @@ public final class PendingIntent implements Parcelable { */ public static void writePendingIntentOrNullToParcel(@Nullable PendingIntent sender, @NonNull Parcel out) { - out.writeStrongBinder(sender != null ? sender.mTarget.asBinder() - : null); + out.writeStrongBinder(sender != null ? sender.mTarget.asBinder() : null); + if (sender != null) { + OnMarshaledListener listener = sOnMarshaledListener.get(); + if (listener != null) { + listener.onMarshaled(sender, out, 0 /* flags */); + } + } } /** diff --git a/core/java/android/app/ProfilerInfo.java b/core/java/android/app/ProfilerInfo.java index fad4798e3a3e..d5234278da7d 100644 --- a/core/java/android/app/ProfilerInfo.java +++ b/core/java/android/app/ProfilerInfo.java @@ -22,6 +22,7 @@ import android.os.Parcelable; import android.util.Slog; import java.io.IOException; +import java.util.Objects; /** * System private API for passing profiler settings. @@ -132,4 +133,32 @@ public class ProfilerInfo implements Parcelable { streamingOutput = in.readInt() != 0; agent = in.readString(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ProfilerInfo other = (ProfilerInfo) o; + // TODO: Also check #profileFd for equality. + return Objects.equals(profileFile, other.profileFile) + && autoStopProfiler == other.autoStopProfiler + && samplingInterval == other.samplingInterval + && streamingOutput == other.streamingOutput + && Objects.equals(agent, other.agent); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + Objects.hashCode(profileFile); + result = 31 * result + samplingInterval; + result = 31 * result + (autoStopProfiler ? 1 : 0); + result = 31 * result + (streamingOutput ? 1 : 0); + result = 31 * result + Objects.hashCode(agent); + return result; + } } diff --git a/core/java/android/app/ResultInfo.java b/core/java/android/app/ResultInfo.java index 5e0867c3607e..d5af08a655d8 100644 --- a/core/java/android/app/ResultInfo.java +++ b/core/java/android/app/ResultInfo.java @@ -20,6 +20,8 @@ import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; +import java.util.Objects; + /** * {@hide} */ @@ -79,4 +81,29 @@ public class ResultInfo implements Parcelable { mData = null; } } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof ResultInfo)) { + return false; + } + final ResultInfo other = (ResultInfo) obj; + final boolean intentsEqual = mData == null ? (other.mData == null) + : mData.filterEquals(other.mData); + return intentsEqual && Objects.equals(mResultWho, other.mResultWho) + && mResultCode == other.mResultCode + && mRequestCode == other.mRequestCode; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + mRequestCode; + result = 31 * result + mResultCode; + result = 31 * result + Objects.hashCode(mResultWho); + if (mData != null) { + result = 31 * result + mData.filterHashCode(); + } + return result; + } } diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 4a092140ed78..23c4166da104 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -14,15 +14,14 @@ * limitations under the License. */ - package android.app; import android.annotation.IntDef; import android.annotation.SystemService; import android.content.Context; import android.os.Binder; -import android.os.RemoteException; import android.os.IBinder; +import android.os.RemoteException; import android.os.ServiceManager; import android.util.Slog; import android.view.View; @@ -71,14 +70,19 @@ public class StatusBarManager { * Setting this flag disables quick settings completely, but does not disable expanding the * notification shade. */ - public static final int DISABLE2_QUICK_SETTINGS = 0x00000001; + public static final int DISABLE2_QUICK_SETTINGS = 1; + public static final int DISABLE2_SYSTEM_ICONS = 1 << 1; + public static final int DISABLE2_NOTIFICATION_SHADE = 1 << 2; + public static final int DISABLE2_GLOBAL_ACTIONS = 1 << 3; public static final int DISABLE2_NONE = 0x00000000; - public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS; + public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS | DISABLE2_SYSTEM_ICONS + | DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS; @IntDef(flag = true, - value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS}) + value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS, DISABLE2_SYSTEM_ICONS, + DISABLE2_NOTIFICATION_SHADE, DISABLE2_GLOBAL_ACTIONS}) @Retention(RetentionPolicy.SOURCE) public @interface Disable2Flags {} diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index ab70f0e71216..495fd3c4682e 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -22,6 +22,7 @@ import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; import android.app.job.IJobScheduler; import android.app.job.JobScheduler; +import android.app.slice.SliceManager; import android.app.timezone.RulesManager; import android.app.trust.TrustManager; import android.app.usage.IStorageStatsManager; @@ -41,6 +42,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,10 +84,11 @@ import android.net.INetworkPolicyManager; import android.net.IpSecManager; import android.net.NetworkPolicyManager; import android.net.NetworkScoreManager; -import android.net.nsd.INsdManager; -import android.net.nsd.NsdManager; +import android.net.NetworkWatchlistManager; import android.net.lowpan.ILowpanManager; import android.net.lowpan.LowpanManager; +import android.net.nsd.INsdManager; +import android.net.nsd.NsdManager; import android.net.wifi.IRttManager; import android.net.wifi.IWifiManager; import android.net.wifi.IWifiScanner; @@ -95,6 +99,8 @@ import android.net.wifi.aware.IWifiAwareManager; import android.net.wifi.aware.WifiAwareManager; import android.net.wifi.p2p.IWifiP2pManager; import android.net.wifi.p2p.WifiP2pManager; +import android.net.wifi.rtt.IWifiRttManager; +import android.net.wifi.rtt.WifiRttManager; import android.nfc.NfcManager; import android.os.BatteryManager; import android.os.BatteryStats; @@ -132,6 +138,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; @@ -148,6 +155,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; @@ -302,14 +310,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, @@ -446,6 +454,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 @@ -603,6 +618,17 @@ final class SystemServiceRegistry { ConnectivityThread.getInstanceLooper()); }}); + registerService(Context.WIFI_RTT_RANGING_SERVICE, WifiRttManager.class, + new CachedServiceFetcher<WifiRttManager>() { + @Override + public WifiRttManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getServiceOrThrow( + Context.WIFI_RTT_RANGING_SERVICE); + IWifiRttManager service = IWifiRttManager.Stub.asInterface(b); + return new WifiRttManager(ctx.getOuterContext(), service); + }}); + registerService(Context.ETHERNET_SERVICE, EthernetManager.class, new CachedServiceFetcher<EthernetManager>() { @Override @@ -850,6 +876,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 @@ -897,6 +934,28 @@ 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)); + } + }); + + registerService(Context.SLICE_SERVICE, SliceManager.class, + new CachedServiceFetcher<SliceManager>() { + @Override + public SliceManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + return new SliceManager(ctx.getOuterContext(), + ctx.mMainThread.getHandler()); + } + }); } /** diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java index 4674c9cd2389..895d12a7c341 100644 --- a/core/java/android/app/TaskStackListener.java +++ b/core/java/android/app/TaskStackListener.java @@ -31,7 +31,7 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override - public void onActivityPinned(String packageName, int userId, int taskId) + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) throws RemoteException { } @@ -77,7 +77,7 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override - public void onTaskRemovalStarted(int taskId) { + public void onTaskRemovalStarted(int taskId) throws RemoteException { } @Override @@ -91,11 +91,10 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override - public void onTaskProfileLocked(int taskId, int userId) { + public void onTaskProfileLocked(int taskId, int userId) throws RemoteException { } @Override - public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) - throws RemoteException { + public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) throws RemoteException { } } diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index c99de5ddb776..8f0168530273 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -26,6 +26,7 @@ import android.annotation.TestApi; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Point; +import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManagerGlobal; import android.os.IBinder; @@ -690,42 +691,15 @@ public final class UiAutomation { .getRealDisplay(Display.DEFAULT_DISPLAY); Point displaySize = new Point(); display.getRealSize(displaySize); - final int displayWidth = displaySize.x; - final int displayHeight = displaySize.y; - final float screenshotWidth; - final float screenshotHeight; - - final int rotation = display.getRotation(); - switch (rotation) { - case ROTATION_FREEZE_0: { - screenshotWidth = displayWidth; - screenshotHeight = displayHeight; - } break; - case ROTATION_FREEZE_90: { - screenshotWidth = displayHeight; - screenshotHeight = displayWidth; - } break; - case ROTATION_FREEZE_180: { - screenshotWidth = displayWidth; - screenshotHeight = displayHeight; - } break; - case ROTATION_FREEZE_270: { - screenshotWidth = displayHeight; - screenshotHeight = displayWidth; - } break; - default: { - throw new IllegalArgumentException("Invalid rotation: " - + rotation); - } - } + int rotation = display.getRotation(); // Take the screenshot Bitmap screenShot = null; try { // Calling out without a lock held. - screenShot = mUiAutomationConnection.takeScreenshot((int) screenshotWidth, - (int) screenshotHeight); + screenShot = mUiAutomationConnection.takeScreenshot( + new Rect(0, 0, displaySize.x, displaySize.y), rotation); if (screenShot == null) { return null; } @@ -734,21 +708,6 @@ public final class UiAutomation { return null; } - // Rotate the screenshot to the current orientation - if (rotation != ROTATION_FREEZE_0) { - Bitmap unrotatedScreenShot = Bitmap.createBitmap(displayWidth, displayHeight, - Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(unrotatedScreenShot); - canvas.translate(unrotatedScreenShot.getWidth() / 2, - unrotatedScreenShot.getHeight() / 2); - canvas.rotate(getDegreesForRotation(rotation)); - canvas.translate(- screenshotWidth / 2, - screenshotHeight / 2); - canvas.drawBitmap(screenShot, 0, 0, null); - canvas.setBitmap(null); - screenShot.recycle(); - screenShot = unrotatedScreenShot; - } - // Optimization screenShot.setHasAlpha(false); diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java index 5e414b837f79..d3828ab47883 100644 --- a/core/java/android/app/UiAutomationConnection.java +++ b/core/java/android/app/UiAutomationConnection.java @@ -21,6 +21,7 @@ import android.accessibilityservice.IAccessibilityServiceClient; import android.content.Context; import android.content.pm.IPackageManager; import android.graphics.Bitmap; +import android.graphics.Rect; import android.hardware.input.InputManager; import android.os.Binder; import android.os.IBinder; @@ -153,7 +154,7 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } @Override - public Bitmap takeScreenshot(int width, int height) { + public Bitmap takeScreenshot(Rect crop, int rotation) { synchronized (mLock) { throwIfCalledByNotTrustedUidLocked(); throwIfShutdownLocked(); @@ -161,7 +162,9 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } final long identity = Binder.clearCallingIdentity(); try { - return SurfaceControl.screenshot(width, height); + int width = crop.width(); + int height = crop.height(); + return SurfaceControl.screenshot(crop, width, height, rotation); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/core/java/android/app/VrManager.java b/core/java/android/app/VrManager.java index 5c6ffa39e6f7..61b90e1766e5 100644 --- a/core/java/android/app/VrManager.java +++ b/core/java/android/app/VrManager.java @@ -4,6 +4,7 @@ import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.content.ComponentName; import android.content.Context; import android.os.Handler; @@ -198,4 +199,38 @@ 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(); + } + } + + /** + * Start VR Input method for the packageName in {@link ComponentName}. + * This method notifies InputMethodManagerService to use VR IME instead of + * regular phone IME. + * @param componentName ComponentName of a VR InputMethod that should be set as selected + * input by InputMethodManagerService. + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + public void setVrInputMethod(ComponentName componentName) { + try { + mService.setVrInputMethod(componentName); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 942cc99585ed..081bd814b30c 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -388,11 +388,12 @@ public class WallpaperManager { public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, @SetWallpaperFlags int which) { - return peekWallpaperBitmap(context, returnDefault, which, context.getUserId()); + return peekWallpaperBitmap(context, returnDefault, which, context.getUserId(), + false /* hardware */); } public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, - @SetWallpaperFlags int which, int userId) { + @SetWallpaperFlags int which, int userId, boolean hardware) { if (mService != null) { try { if (!mService.isWallpaperSupported(context.getOpPackageName())) { @@ -409,7 +410,7 @@ public class WallpaperManager { mCachedWallpaper = null; mCachedWallpaperUserId = 0; try { - mCachedWallpaper = getCurrentWallpaperLocked(context, userId); + mCachedWallpaper = getCurrentWallpaperLocked(context, userId, hardware); mCachedWallpaperUserId = userId; } catch (OutOfMemoryError e) { Log.w(TAG, "Out of memory loading the current wallpaper: " + e); @@ -447,7 +448,7 @@ public class WallpaperManager { } } - private Bitmap getCurrentWallpaperLocked(Context context, int userId) { + private Bitmap getCurrentWallpaperLocked(Context context, int userId, boolean hardware) { if (mService == null) { Log.w(TAG, "WallpaperService not running"); return null; @@ -460,6 +461,9 @@ public class WallpaperManager { if (fd != null) { try { BitmapFactory.Options options = new BitmapFactory.Options(); + if (hardware) { + options.inPreferredConfig = Bitmap.Config.HARDWARE; + } return BitmapFactory.decodeFileDescriptor( fd.getFileDescriptor(), null, options); } catch (OutOfMemoryError e) { @@ -814,12 +818,23 @@ public class WallpaperManager { } /** - * Like {@link #getDrawable()} but returns a Bitmap. + * Like {@link #getDrawable()} but returns a Bitmap with default {@link Bitmap.Config}. * * @hide */ public Bitmap getBitmap() { - return getBitmapAsUser(mContext.getUserId()); + return getBitmap(false); + } + + /** + * Like {@link #getDrawable()} but returns a Bitmap. + * + * @param hardware Asks for a hardware backed bitmap. + * @see Bitmap.Config#HARDWARE + * @hide + */ + public Bitmap getBitmap(boolean hardware) { + return getBitmapAsUser(mContext.getUserId(), hardware); } /** @@ -827,8 +842,8 @@ public class WallpaperManager { * * @hide */ - public Bitmap getBitmapAsUser(int userId) { - return sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, userId); + public Bitmap getBitmapAsUser(int userId, boolean hardware) { + return sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, userId, hardware); } /** diff --git a/core/java/android/app/WindowConfiguration.aidl b/core/java/android/app/WindowConfiguration.aidl new file mode 100644 index 000000000000..1a70f524b278 --- /dev/null +++ b/core/java/android/app/WindowConfiguration.aidl @@ -0,0 +1,19 @@ +/* + * 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.app; + +parcelable WindowConfiguration; diff --git a/core/java/android/app/WindowConfiguration.java b/core/java/android/app/WindowConfiguration.java new file mode 100644 index 000000000000..80399ae65c58 --- /dev/null +++ b/core/java/android/app/WindowConfiguration.java @@ -0,0 +1,590 @@ +/* + * 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.app; + +import static android.app.ActivityThread.isSystem; +import static android.app.WindowConfigurationProto.ACTIVITY_TYPE; +import static android.app.WindowConfigurationProto.APP_BOUNDS; +import static android.app.WindowConfigurationProto.WINDOWING_MODE; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; +import android.view.DisplayInfo; + +/** + * Class that contains windowing configuration/state for other objects that contain windows directly + * or indirectly. E.g. Activities, Task, Displays, ... + * The test class is {@link com.android.server.wm.WindowConfigurationTests} which must be kept + * up-to-date and ran anytime changes are made to this class. + * @hide + */ +@TestApi +public class WindowConfiguration implements Parcelable, Comparable<WindowConfiguration> { + /** + * bounds that can differ from app bounds, which may include things such as insets. + * + * TODO: Investigate combining with {@link mAppBounds}. Can the latter be a product of the + * former? + */ + private Rect mBounds = new Rect(); + + /** + * {@link android.graphics.Rect} defining app bounds. The dimensions override usages of + * {@link DisplayInfo#appHeight} and {@link DisplayInfo#appWidth} and mirrors these values at + * the display level. Lower levels can override these values to provide custom bounds to enforce + * features such as a max aspect ratio. + */ + private Rect mAppBounds; + + /** The current windowing mode of the configuration. */ + private @WindowingMode int mWindowingMode; + + /** Windowing mode is currently not defined. */ + public static final int WINDOWING_MODE_UNDEFINED = 0; + /** Occupies the full area of the screen or the parent container. */ + public static final int WINDOWING_MODE_FULLSCREEN = 1; + /** Always on-top (always visible). of other siblings in its parent container. */ + public static final int WINDOWING_MODE_PINNED = 2; + /** The primary container driving the screen to be in split-screen mode. */ + public static final int WINDOWING_MODE_SPLIT_SCREEN_PRIMARY = 3; + /** + * The containers adjacent to the {@link #WINDOWING_MODE_SPLIT_SCREEN_PRIMARY} container in + * split-screen mode. + * NOTE: Containers launched with the windowing mode with APIs like + * {@link ActivityOptions#setLaunchWindowingMode(int)} will be launched in + * {@link #WINDOWING_MODE_FULLSCREEN} if the display isn't currently in split-screen windowing + * mode + * @see #WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY + */ + public static final int WINDOWING_MODE_SPLIT_SCREEN_SECONDARY = 4; + /** + * Alias for {@link #WINDOWING_MODE_SPLIT_SCREEN_SECONDARY} that makes it clear that the usage + * points for APIs like {@link ActivityOptions#setLaunchWindowingMode(int)} that the container + * will launch into fullscreen or split-screen secondary depending on if the device is currently + * in fullscreen mode or split-screen mode. + */ + public static final int WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY = + WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; + /** Can be freely resized within its parent container. */ + public static final int WINDOWING_MODE_FREEFORM = 5; + + /** @hide */ + @IntDef({ + WINDOWING_MODE_UNDEFINED, + WINDOWING_MODE_FULLSCREEN, + WINDOWING_MODE_PINNED, + WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, + WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, + WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY, + WINDOWING_MODE_FREEFORM, + }) + public @interface WindowingMode {} + + /** The current activity type of the configuration. */ + private @ActivityType int mActivityType; + + /** Activity type is currently not defined. */ + public static final int ACTIVITY_TYPE_UNDEFINED = 0; + /** Standard activity type. Nothing special about the activity... */ + public static final int ACTIVITY_TYPE_STANDARD = 1; + /** Home/Launcher activity type. */ + public static final int ACTIVITY_TYPE_HOME = 2; + /** Recents/Overview activity type. There is only one activity with this type in the system. */ + public static final int ACTIVITY_TYPE_RECENTS = 3; + /** Assistant activity type. */ + public static final int ACTIVITY_TYPE_ASSISTANT = 4; + + /** @hide */ + @IntDef({ + ACTIVITY_TYPE_UNDEFINED, + ACTIVITY_TYPE_STANDARD, + ACTIVITY_TYPE_HOME, + ACTIVITY_TYPE_RECENTS, + ACTIVITY_TYPE_ASSISTANT, + }) + public @interface ActivityType {} + + /** Bit that indicates that the {@link #mBounds} changed. + * @hide */ + public static final int WINDOW_CONFIG_BOUNDS = 1 << 0; + /** Bit that indicates that the {@link #mAppBounds} changed. + * @hide */ + public static final int WINDOW_CONFIG_APP_BOUNDS = 1 << 1; + /** Bit that indicates that the {@link #mWindowingMode} changed. + * @hide */ + public static final int WINDOW_CONFIG_WINDOWING_MODE = 1 << 2; + /** Bit that indicates that the {@link #mActivityType} changed. + * @hide */ + public static final int WINDOW_CONFIG_ACTIVITY_TYPE = 1 << 3; + + /** @hide */ + @IntDef(flag = true, + value = { + WINDOW_CONFIG_BOUNDS, + WINDOW_CONFIG_APP_BOUNDS, + WINDOW_CONFIG_WINDOWING_MODE, + WINDOW_CONFIG_ACTIVITY_TYPE + }) + public @interface WindowConfig {} + + public WindowConfiguration() { + unset(); + } + + /** @hide */ + public WindowConfiguration(WindowConfiguration configuration) { + setTo(configuration); + } + + private WindowConfiguration(Parcel in) { + readFromParcel(in); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mBounds, flags); + dest.writeParcelable(mAppBounds, flags); + dest.writeInt(mWindowingMode); + dest.writeInt(mActivityType); + } + + private void readFromParcel(Parcel source) { + mBounds = source.readParcelable(Rect.class.getClassLoader()); + mAppBounds = source.readParcelable(Rect.class.getClassLoader()); + mWindowingMode = source.readInt(); + mActivityType = source.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + public static final Creator<WindowConfiguration> CREATOR = new Creator<WindowConfiguration>() { + @Override + public WindowConfiguration createFromParcel(Parcel in) { + return new WindowConfiguration(in); + } + + @Override + public WindowConfiguration[] newArray(int size) { + return new WindowConfiguration[size]; + } + }; + + /** + * Sets the bounds to the provided {@link Rect}. + * @param rect the new bounds value. + */ + public void setBounds(Rect rect) { + if (rect == null) { + mBounds.setEmpty(); + return; + } + + mBounds.set(rect); + } + + /** + * Set {@link #mAppBounds} to the input Rect. + * @param rect The rect value to set {@link #mAppBounds} to. + * @see #getAppBounds() + */ + public void setAppBounds(Rect rect) { + if (rect == null) { + mAppBounds = null; + return; + } + + setAppBounds(rect.left, rect.top, rect.right, rect.bottom); + } + + /** + * @see #setAppBounds(Rect) + * @see #getAppBounds() + * @hide + */ + public void setAppBounds(int left, int top, int right, int bottom) { + if (mAppBounds == null) { + mAppBounds = new Rect(); + } + + mAppBounds.set(left, top, right, bottom); + } + + /** @see #setAppBounds(Rect) */ + public Rect getAppBounds() { + return mAppBounds; + } + + /** @see #setBounds(Rect) */ + public Rect getBounds() { + return mBounds; + } + + public void setWindowingMode(@WindowingMode int windowingMode) { + mWindowingMode = windowingMode; + } + + @WindowingMode + public int getWindowingMode() { + return mWindowingMode; + } + + public void setActivityType(@ActivityType int activityType) { + if (mActivityType == activityType) { + return; + } + + // Error check within system server that we are not changing activity type which can be + // dangerous. It is okay for things to change in the application process as it doesn't + // affect how other things is the system is managed. + if (isSystem() + && mActivityType != ACTIVITY_TYPE_UNDEFINED + && activityType != ACTIVITY_TYPE_UNDEFINED) { + throw new IllegalStateException("Can't change activity type once set: " + this + + " activityType=" + activityTypeToString(activityType)); + } + mActivityType = activityType; + } + + @ActivityType + public int getActivityType() { + return mActivityType; + } + + public void setTo(WindowConfiguration other) { + setBounds(other.mBounds); + setAppBounds(other.mAppBounds); + setWindowingMode(other.mWindowingMode); + setActivityType(other.mActivityType); + } + + /** Set this object to completely undefined. + * @hide */ + public void unset() { + setToDefaults(); + } + + /** @hide */ + public void setToDefaults() { + setAppBounds(null); + setBounds(null); + setWindowingMode(WINDOWING_MODE_UNDEFINED); + setActivityType(ACTIVITY_TYPE_UNDEFINED); + } + + /** + * Copies the fields from delta into this Configuration object, keeping + * track of which ones have changed. Any undefined fields in {@code delta} + * are ignored and not copied in to the current Configuration. + * + * @return a bit mask of the changed fields, as per {@link #diff} + * @hide + */ + public @WindowConfig int updateFrom(@NonNull WindowConfiguration delta) { + int changed = 0; + // Only allow override if bounds is not empty + if (!delta.mBounds.isEmpty() && !delta.mBounds.equals(mBounds)) { + changed |= WINDOW_CONFIG_BOUNDS; + setBounds(delta.mBounds); + } + if (delta.mAppBounds != null && !delta.mAppBounds.equals(mAppBounds)) { + changed |= WINDOW_CONFIG_APP_BOUNDS; + setAppBounds(delta.mAppBounds); + } + if (delta.mWindowingMode != WINDOWING_MODE_UNDEFINED + && mWindowingMode != delta.mWindowingMode) { + changed |= WINDOW_CONFIG_WINDOWING_MODE; + setWindowingMode(delta.mWindowingMode); + } + if (delta.mActivityType != ACTIVITY_TYPE_UNDEFINED + && mActivityType != delta.mActivityType) { + changed |= WINDOW_CONFIG_ACTIVITY_TYPE; + setActivityType(delta.mActivityType); + } + return changed; + } + + /** + * Return a bit mask of the differences between this Configuration object and the given one. + * Does not change the values of either. Any undefined fields in <var>other</var> are ignored. + * @param other The configuration to diff against. + * @param compareUndefined If undefined values should be compared. + * @return Returns a bit mask indicating which configuration + * values has changed, containing any combination of {@link WindowConfig} flags. + * + * @see Configuration#diff(Configuration) + * @hide + */ + public @WindowConfig long diff(WindowConfiguration other, boolean compareUndefined) { + long changes = 0; + + if (!mBounds.equals(other.mBounds)) { + changes |= WINDOW_CONFIG_BOUNDS; + } + + // Make sure that one of the values is not null and that they are not equal. + if ((compareUndefined || other.mAppBounds != null) + && mAppBounds != other.mAppBounds + && (mAppBounds == null || !mAppBounds.equals(other.mAppBounds))) { + changes |= WINDOW_CONFIG_APP_BOUNDS; + } + + if ((compareUndefined || other.mWindowingMode != WINDOWING_MODE_UNDEFINED) + && mWindowingMode != other.mWindowingMode) { + changes |= WINDOW_CONFIG_WINDOWING_MODE; + } + + if ((compareUndefined || other.mActivityType != ACTIVITY_TYPE_UNDEFINED) + && mActivityType != other.mActivityType) { + changes |= WINDOW_CONFIG_ACTIVITY_TYPE; + } + + return changes; + } + + @Override + public int compareTo(WindowConfiguration that) { + int n = 0; + if (mAppBounds == null && that.mAppBounds != null) { + return 1; + } else if (mAppBounds != null && that.mAppBounds == null) { + return -1; + } else if (mAppBounds != null && that.mAppBounds != null) { + n = mAppBounds.left - that.mAppBounds.left; + if (n != 0) return n; + n = mAppBounds.top - that.mAppBounds.top; + if (n != 0) return n; + n = mAppBounds.right - that.mAppBounds.right; + if (n != 0) return n; + n = mAppBounds.bottom - that.mAppBounds.bottom; + if (n != 0) return n; + } + + n = mBounds.left - that.mBounds.left; + if (n != 0) return n; + n = mBounds.top - that.mBounds.top; + if (n != 0) return n; + n = mBounds.right - that.mBounds.right; + if (n != 0) return n; + n = mBounds.bottom - that.mBounds.bottom; + if (n != 0) return n; + + n = mWindowingMode - that.mWindowingMode; + if (n != 0) return n; + n = mActivityType - that.mActivityType; + if (n != 0) return n; + + // if (n != 0) return n; + return n; + } + + /** @hide */ + @Override + public boolean equals(Object that) { + if (that == null) return false; + if (that == this) return true; + if (!(that instanceof WindowConfiguration)) { + return false; + } + return this.compareTo((WindowConfiguration) that) == 0; + } + + /** @hide */ + @Override + public int hashCode() { + int result = 0; + if (mAppBounds != null) { + result = 31 * result + mAppBounds.hashCode(); + } + result = 31 * result + mBounds.hashCode(); + + result = 31 * result + mWindowingMode; + result = 31 * result + mActivityType; + return result; + } + + /** @hide */ + @Override + public String toString() { + return "{ mBounds=" + mBounds + + " mAppBounds=" + mAppBounds + + " mWindowingMode=" + windowingModeToString(mWindowingMode) + + " mActivityType=" + activityTypeToString(mActivityType) + "}"; + } + + /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.app.WindowConfigurationProto} + * + * @param protoOutputStream Stream to write the WindowConfiguration object to. + * @param fieldId Field Id of the WindowConfiguration as defined in the parent message + * @hide + */ + public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + final long token = protoOutputStream.start(fieldId); + if (mAppBounds != null) { + mAppBounds.writeToProto(protoOutputStream, APP_BOUNDS); + } + protoOutputStream.write(WINDOWING_MODE, mWindowingMode); + protoOutputStream.write(ACTIVITY_TYPE, mActivityType); + protoOutputStream.end(token); + } + + /** + * Returns true if the activities associated with this window configuration display a shadow + * around their border. + * @hide + */ + public boolean hasWindowShadow() { + return tasksAreFloating(); + } + + /** + * Returns true if the activities associated with this window configuration display a decor + * view. + * @hide + */ + public boolean hasWindowDecorCaption() { + return mWindowingMode == WINDOWING_MODE_FREEFORM; + } + + /** + * Returns true if the tasks associated with this window configuration can be resized + * independently of their parent container. + * @hide + */ + public boolean canResizeTask() { + return mWindowingMode == WINDOWING_MODE_FREEFORM; + } + + /** Returns true if the task bounds should persist across power cycles. + * @hide */ + public boolean persistTaskBounds() { + return mWindowingMode == WINDOWING_MODE_FREEFORM; + } + + /** + * Returns true if the tasks associated with this window configuration are floating. + * Floating tasks are laid out differently as they are allowed to extend past the display bounds + * without overscan insets. + * @hide + */ + public boolean tasksAreFloating() { + return mWindowingMode == WINDOWING_MODE_FREEFORM || mWindowingMode == WINDOWING_MODE_PINNED; + } + + /** + * Returns true if the windows associated with this window configuration can receive input keys. + * @hide + */ + public boolean canReceiveKeys() { + return mWindowingMode != WINDOWING_MODE_PINNED; + } + + /** + * Returns true if the container associated with this window configuration is always-on-top of + * its siblings. + * @hide + */ + public boolean isAlwaysOnTop() { + return mWindowingMode == WINDOWING_MODE_PINNED; + } + + /** + * Returns true if any visible windows belonging to apps with this window configuration should + * be kept on screen when the app is killed due to something like the low memory killer. + * @hide + */ + public boolean keepVisibleDeadAppWindowOnScreen() { + return mWindowingMode != WINDOWING_MODE_PINNED; + } + + /** + * Returns true if the backdrop on the client side should match the frame of the window. + * Returns false, if the backdrop should be fullscreen. + * @hide + */ + public boolean useWindowFrameForBackdrop() { + return mWindowingMode == WINDOWING_MODE_FREEFORM || mWindowingMode == WINDOWING_MODE_PINNED; + } + + /** + * Returns true if this container may be scaled without resizing, and windows within may need + * to be configured as such. + * @hide + */ + public boolean windowsAreScaleable() { + return mWindowingMode == WINDOWING_MODE_PINNED; + } + + /** + * Returns true if windows in this container should be given move animations by default. + * @hide + */ + public boolean hasMovementAnimations() { + return mWindowingMode != WINDOWING_MODE_PINNED; + } + + /** + * Returns true if this container can be put in either + * {@link #WINDOWING_MODE_SPLIT_SCREEN_PRIMARY} or + * {@link #WINDOWING_MODE_SPLIT_SCREEN_SECONDARY} windowing modes based on its current state. + * @hide + */ + public boolean supportSplitScreenWindowingMode() { + return supportSplitScreenWindowingMode(mActivityType); + } + + /** @hide */ + public static boolean supportSplitScreenWindowingMode(int activityType) { + return activityType != ACTIVITY_TYPE_ASSISTANT; + } + + /** @hide */ + public static String windowingModeToString(@WindowingMode int windowingMode) { + switch (windowingMode) { + case WINDOWING_MODE_UNDEFINED: return "undefined"; + case WINDOWING_MODE_FULLSCREEN: return "fullscreen"; + case WINDOWING_MODE_PINNED: return "pinned"; + case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY: return "split-screen-primary"; + case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY: return "split-screen-secondary"; + case WINDOWING_MODE_FREEFORM: return "freeform"; + } + return String.valueOf(windowingMode); + } + + /** @hide */ + public static String activityTypeToString(@ActivityType int applicationType) { + switch (applicationType) { + case ACTIVITY_TYPE_UNDEFINED: return "undefined"; + case ACTIVITY_TYPE_STANDARD: return "standard"; + case ACTIVITY_TYPE_HOME: return "home"; + case ACTIVITY_TYPE_RECENTS: return "recents"; + case ACTIVITY_TYPE_ASSISTANT: return "assistant"; + } + return String.valueOf(applicationType); + } +} diff --git a/core/java/android/app/admin/ConnectEvent.java b/core/java/android/app/admin/ConnectEvent.java index ffd38e2b8760..f06a9257b7f8 100644 --- a/core/java/android/app/admin/ConnectEvent.java +++ b/core/java/android/app/admin/ConnectEvent.java @@ -32,29 +32,30 @@ import java.net.UnknownHostException; public final class ConnectEvent extends NetworkEvent implements Parcelable { /** The destination IP address. */ - private final String ipAddress; + private final String mIpAddress; /** The destination port number. */ - private final int port; + private final int mPort; /** @hide */ public ConnectEvent(String ipAddress, int port, String packageName, long timestamp) { super(packageName, timestamp); - this.ipAddress = ipAddress; - this.port = port; + this.mIpAddress = ipAddress; + this.mPort = port; } private ConnectEvent(Parcel in) { - this.ipAddress = in.readString(); - this.port = in.readInt(); - this.packageName = in.readString(); - this.timestamp = in.readLong(); + this.mIpAddress = in.readString(); + this.mPort = in.readInt(); + this.mPackageName = in.readString(); + this.mTimestamp = in.readLong(); + this.mId = in.readLong(); } public InetAddress getInetAddress() { try { // ipAddress is already an address, not a host name, no DNS resolution will happen. - return InetAddress.getByName(ipAddress); + return InetAddress.getByName(mIpAddress); } catch (UnknownHostException e) { // Should never happen as we aren't passing a host name. return InetAddress.getLoopbackAddress(); @@ -62,13 +63,13 @@ public final class ConnectEvent extends NetworkEvent implements Parcelable { } public int getPort() { - return port; + return mPort; } @Override public String toString() { - return String.format("ConnectEvent(%s, %d, %d, %s)", ipAddress, port, timestamp, - packageName); + return String.format("ConnectEvent(%s, %d, %d, %s)", mIpAddress, mPort, mTimestamp, + mPackageName); } public static final Parcelable.Creator<ConnectEvent> CREATOR @@ -96,10 +97,10 @@ public final class ConnectEvent extends NetworkEvent implements Parcelable { public void writeToParcel(Parcel out, int flags) { // write parcel token first out.writeInt(PARCEL_TOKEN_CONNECT_EVENT); - out.writeString(ipAddress); - out.writeInt(port); - out.writeString(packageName); - out.writeLong(timestamp); + out.writeString(mIpAddress); + out.writeInt(mPort); + out.writeString(mPackageName); + out.writeLong(mTimestamp); + out.writeLong(mId); } } - diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 121b58a2b104..2038898d77cd 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -39,6 +39,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageDataObserver; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ParceledListSlice; @@ -47,6 +48,7 @@ import android.graphics.Bitmap; import android.net.ProxyInfo; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.os.Parcelable; import android.os.PersistableBundle; import android.os.Process; @@ -55,13 +57,20 @@ import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.ContactsContract.Directory; +import android.security.AttestedKeyPair; import android.security.Credentials; +import android.security.KeyChain; +import android.security.KeyChainException; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.ParcelableKeyGenParameterSpec; import android.service.restrictions.RestrictionsReceiver; import android.telephony.TelephonyManager; import android.util.ArraySet; import android.util.Log; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; import com.android.org.conscrypt.TrustedCertificateStore; import java.io.ByteArrayInputStream; @@ -71,6 +80,7 @@ import java.lang.annotation.RetentionPolicy; import java.net.InetSocketAddress; import java.net.Proxy; import java.security.KeyFactory; +import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.Certificate; @@ -1311,9 +1321,15 @@ public class DevicePolicyManager { public static final String DELEGATION_ENABLE_SYSTEM_APP = "delegation-enable-system-app"; /** + * Delegation for installing existing packages. This scope grants access to the + * {@link #installExistingPackage} API. + */ + public static final String DELEGATION_INSTALL_EXISTING_PACKAGE = + "delegation-install-existing-package"; + + /** * Delegation of management of uninstalled packages. This scope grants access to the * {@code #setKeepUninstalledPackages} and {@code #getKeepUninstalledPackages} APIs. - * @hide */ public static final String DELEGATION_KEEP_UNINSTALLED_PACKAGES = "delegation-keep-uninstalled-packages"; @@ -1538,6 +1554,92 @@ public class DevicePolicyManager { public @interface ProvisioningPreCondition {} /** + * Disable all configurable SystemUI features during LockTask mode. This includes, + * <ul> + * <li>system info area in the status bar (connectivity icons, clock, etc.) + * <li>notifications (including alerts, icons, and the notification shade) + * <li>Home button + * <li>Recents button and UI + * <li>global actions menu (i.e. power button menu) + * <li>keyguard + * </ul> + * + * This is the default configuration for LockTask. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_NONE = 0; + + /** + * Enable the system info area in the status bar during LockTask mode. The system info area + * usually occupies the right side of the status bar (although this can differ across OEMs). It + * includes all system information indicators, such as date and time, connectivity, battery, + * vibration mode, etc. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_SYSTEM_INFO = 1; + + /** + * Enable notifications during LockTask mode. This includes notification icons on the status + * bar, heads-up notifications, and the expandable notification shade. Note that the Quick + * Settings panel will still be disabled. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_NOTIFICATIONS = 1 << 1; + + /** + * Enable the Home button during LockTask mode. Note that if a custom launcher is used, it has + * to be registered as the default launcher with + * {@link #addPersistentPreferredActivity(ComponentName, IntentFilter, ComponentName)}, and its + * package needs to be whitelisted for LockTask with + * {@link #setLockTaskPackages(ComponentName, String[])}. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_HOME = 1 << 2; + + /** + * Enable the Recents button and the Recents screen during LockTask mode. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_RECENTS = 1 << 3; + + /** + * Enable the global actions dialog during LockTask mode. This is the dialog that shows up when + * the user long-presses the power button, for example. Note that the user may not be able to + * power off the device if this flag is not set. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_GLOBAL_ACTIONS = 1 << 4; + + /** + * Enable the keyguard during LockTask mode. Note that if the keyguard is already disabled with + * {@link #setKeyguardDisabled(ComponentName, boolean)}, setting this flag will have no effect. + * If this flag is not set, the keyguard will not be shown even if the user has a lock screen + * credential. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_KEYGUARD = 1 << 5; + + /** + * Flags supplied to {@link #setLockTaskFeatures(ComponentName, int)}. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, + value = {LOCK_TASK_FEATURE_NONE, LOCK_TASK_FEATURE_SYSTEM_INFO, + LOCK_TASK_FEATURE_NOTIFICATIONS, LOCK_TASK_FEATURE_HOME, + LOCK_TASK_FEATURE_RECENTS, LOCK_TASK_FEATURE_GLOBAL_ACTIONS, + LOCK_TASK_FEATURE_KEYGUARD}) + public @interface LockTaskFeature {} + + /** * Service action: Action for a service that device owner and profile owner can optionally * own. If a device owner or a profile owner has such a service, the system tries to keep * a bound connection to it, in order to keep their process always running. @@ -3140,6 +3242,7 @@ public class DevicePolicyManager { */ public static final int WIPE_EUICC = 0x0004; + /** * Ask that all user data be wiped. If called as a secondary user, the user will be removed and * other users will remain unaffected. Calling from the primary user will cause the device to @@ -3156,9 +3259,47 @@ public class DevicePolicyManager { */ public void wipeData(int flags) { throwIfParentInstance("wipeData"); + final String wipeReasonForUser = mContext.getString( + R.string.work_profile_deleted_description_dpm_wipe); + wipeDataInternal(flags, wipeReasonForUser); + } + + /** + * Ask that all user data be wiped. If called as a secondary user, the user will be removed and + * other users will remain unaffected, the provided reason for wiping data can be shown to + * user. Calling from the primary user will cause the device to reboot, erasing all device data + * - including all the secondary users and their data - while booting up. In this case, we don't + * show the reason to the user since the device would be factory reset. + * <p> + * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to + * be able to call this method; if it has not, a security exception will be thrown. + * + * @param flags Bit mask of additional options: currently supported flags are + * {@link #WIPE_EXTERNAL_STORAGE} and {@link #WIPE_RESET_PROTECTION_DATA}. + * @param reason a string that contains the reason for wiping data, which can be + * presented to the user. + * @throws SecurityException if the calling application does not own an active administrator + * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} + * @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()); + } + + /** + * Internal function for both {@link #wipeData(int)} and + * {@link #wipeDataWithReason(int, CharSequence)} to call. + * + * @see #wipeData(int) + * @see #wipeDataWithReason(int, CharSequence) + * @hide + */ + private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser) { if (mService != null) { try { - mService.wipeData(flags); + mService.wipeDataWithReason(flags, wipeReasonForUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -3729,6 +3870,47 @@ public class DevicePolicyManager { */ public boolean installKeyPair(@Nullable ComponentName admin, @NonNull PrivateKey privKey, @NonNull Certificate[] certs, @NonNull String alias, boolean requestAccess) { + return installKeyPair(admin, privKey, certs, alias, requestAccess, true); + } + + /** + * Called by a device or profile owner, or delegated certificate installer, to install a + * certificate chain and corresponding private key for the leaf certificate. All apps within the + * profile will be able to access the certificate chain and use the private key, given direct + * user approval (if the user is allowed to select the private key). + * + * <p>The caller of this API may grant itself access to the certificate and private key + * immediately, without user approval. It is a best practice not to request this unless strictly + * necessary since it opens up additional security vulnerabilities. + * + * <p>Whether this key is offered to the user for approval at all or not depends on the + * {@code isUserSelectable} parameter. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or + * {@code null} if calling from a delegated certificate installer. + * @param privKey The private key to install. + * @param certs The certificate chain to install. The chain should start with the leaf + * certificate and include the chain of trust in order. This will be returned by + * {@link android.security.KeyChain#getCertificateChain}. + * @param alias The private key alias under which to install the certificate. If a certificate + * with that alias already exists, it will be overwritten. + * @param requestAccess {@code true} to request that the calling app be granted access to the + * credentials immediately. Otherwise, access to the credentials will be gated by user + * approval. + * @param isUserSelectable {@code true} to indicate that a user can select this key via the + * Certificate Selection prompt, false to indicate that this key can only be granted + * access by implementing + * {@link android.app.admin.DeviceAdminReceiver#onChoosePrivateKeyAlias}. + * @return {@code true} if the keys were installed, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile + * owner. + * @see android.security.KeyChain#getCertificateChain + * @see #setDelegatedScopes + * @see #DELEGATION_CERT_INSTALL + */ + public boolean installKeyPair(@Nullable ComponentName admin, @NonNull PrivateKey privKey, + @NonNull Certificate[] certs, @NonNull String alias, boolean requestAccess, + boolean isUserSelectable) { throwIfParentInstance("installKeyPair"); try { final byte[] pemCert = Credentials.convertToPem(certs[0]); @@ -3739,7 +3921,7 @@ public class DevicePolicyManager { final byte[] pkcs8Key = KeyFactory.getInstance(privKey.getAlgorithm()) .getKeySpec(privKey, PKCS8EncodedKeySpec.class).getEncoded(); return mService.installKeyPair(admin, mContext.getPackageName(), pkcs8Key, pemCert, - pemChain, alias, requestAccess); + pemChain, alias, requestAccess, isUserSelectable); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { @@ -3773,6 +3955,50 @@ public class DevicePolicyManager { } /** + * Called by a device or profile owner, or delegated certificate installer, to generate a + * new private/public key pair. If the device supports key generation via secure hardware, + * this method is useful for creating a key in KeyChain that never left the secure hardware. + * + * Access to the key is controlled the same way as in {@link #installKeyPair}. + * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or + * {@code null} if calling from a delegated certificate installer. + * @param algorithm The key generation algorithm, see {@link java.security.KeyPairGenerator}. + * @param keySpec Specification of the key to generate, see + * {@link java.security.KeyPairGenerator}. + * @return A non-null {@code AttestedKeyPair} if the key generation succeeded, null otherwise. + * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile + * owner. + * @throws IllegalArgumentException if the alias in {@code keySpec} is empty, or if the + * algorithm specification in {@code keySpec} is not {@code RSAKeyGenParameterSpec} + * or {@code ECGenParameterSpec}. + */ + public AttestedKeyPair generateKeyPair(@Nullable ComponentName admin, + @NonNull String algorithm, @NonNull KeyGenParameterSpec keySpec) { + throwIfParentInstance("generateKeyPair"); + try { + final ParcelableKeyGenParameterSpec parcelableSpec = + new ParcelableKeyGenParameterSpec(keySpec); + final boolean success = mService.generateKeyPair( + admin, mContext.getPackageName(), algorithm, parcelableSpec); + if (!success) { + Log.e(TAG, "Error generating key via DevicePolicyManagerService."); + return null; + } + + final KeyPair keyPair = KeyChain.getKeyPair(mContext, keySpec.getKeystoreAlias()); + return new AttestedKeyPair(keyPair, null); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (KeyChainException e) { + Log.w(TAG, "Failed to generate key", e); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while generating key", e); + Thread.currentThread().interrupt(); + } + return null; + } + + /** * @return the alias of a given CA certificate in the certificate store, or {@code null} if it * doesn't exist. */ @@ -5865,7 +6091,6 @@ public class DevicePolicyManager { * @return List of package names to keep cached. * @see #setDelegatedScopes * @see #DELEGATION_KEEP_UNINSTALLED_PACKAGES - * @hide */ public @Nullable List<String> getKeepUninstalledPackages(@Nullable ComponentName admin) { throwIfParentInstance("getKeepUninstalledPackages"); @@ -5893,7 +6118,6 @@ public class DevicePolicyManager { * @throws SecurityException if {@code admin} is not a device owner. * @see #setDelegatedScopes * @see #DELEGATION_KEEP_UNINSTALLED_PACKAGES - * @hide */ public void setKeepUninstalledPackages(@Nullable ComponentName admin, @NonNull List<String> packageNames) { @@ -5968,8 +6192,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; @@ -5981,6 +6205,26 @@ public class DevicePolicyManager { public static final int MAKE_USER_DEMO = 0x0004; /** + * Flag used by {@link #createAndManageUser} to specificy that the newly created user should be + * started in the background as part of the user creation. + */ + // TODO: Investigate solutions for the case where reboot happens before setup is completed. + public static final int START_USER_IN_BACKGROUND = 0x0008; + + /** + * @hide + */ + @IntDef( + flag = true, + prefix = {"SKIP_", "MAKE_USER_", "START_"}, + value = {SKIP_SETUP_WIZARD, MAKE_USER_EPHEMERAL, MAKE_USER_DEMO, + START_USER_IN_BACKGROUND} + ) + @Retention(RetentionPolicy.SOURCE) + public @interface CreateAndManageUserFlags {} + + + /** * Called by a device owner to create a user with the specified name and a given component of * the calling package as profile owner. The UserHandle returned by this method should not be * persisted as user handles are recycled as users are removed and created. If you need to @@ -6011,7 +6255,7 @@ public class DevicePolicyManager { public @Nullable UserHandle createAndManageUser(@NonNull ComponentName admin, @NonNull String name, @NonNull ComponentName profileOwner, @Nullable PersistableBundle adminExtras, - int flags) { + @CreateAndManageUserFlags int flags) { throwIfParentInstance("createAndManageUser"); try { return mService.createAndManageUser(admin, name, profileOwner, adminExtras, flags); @@ -6029,7 +6273,7 @@ public class DevicePolicyManager { * @return {@code true} if the user was removed, {@code false} otherwise. * @throws SecurityException if {@code admin} is not a device owner. */ - public boolean removeUser(@NonNull ComponentName admin, UserHandle userHandle) { + public boolean removeUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) { throwIfParentInstance("removeUser"); try { return mService.removeUser(admin, userHandle); @@ -6040,6 +6284,7 @@ public class DevicePolicyManager { /** * Called by a device owner to switch the specified user to the foreground. + * <p> This cannot be used to switch to a managed profile. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param userHandle the user to switch to; null will switch to primary. @@ -6057,6 +6302,80 @@ public class DevicePolicyManager { } /** + * Called by a device owner to stop the specified secondary user. + * <p> This cannot be used to stop the primary user or a managed profile. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param userHandle the user to be stopped. + * @return {@code true} if the user can be stopped, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public boolean stopUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) { + throwIfParentInstance("stopUser"); + try { + return mService.stopUser(admin, userHandle); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Called by a profile owner that is affiliated with the device owner to stop the calling user + * and switch back to primary. + * <p> This has no effect when called on a managed profile. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return {@code true} if the exit was successful, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not a profile owner affiliated with the device + * owner. + */ + public boolean logoutUser(@NonNull ComponentName admin) { + throwIfParentInstance("logoutUser"); + try { + return mService.logoutUser(admin); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Called by a device owner to list all secondary users on the device, excluding managed + * profiles. + * <p> Used for various user management APIs, including {@link #switchUser}, {@link #removeUser} + * and {@link #stopUser}. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return list of other {@link UserHandle}s on the device. + * @throws SecurityException if {@code admin} is not a device owner. + * @see #switchUser + * @see #removeUser + * @see #stopUser + */ + public List<UserHandle> getSecondaryUsers(@NonNull ComponentName admin) { + throwIfParentInstance("getSecondaryUsers"); + try { + return mService.getSecondaryUsers(admin); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Checks if the profile owner is running in an ephemeral user. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return whether the profile owner is running in an ephemeral user. + */ + public boolean isEphemeralUser(@NonNull ComponentName admin) { + throwIfParentInstance("isEphemeralUser"); + try { + return mService.isEphemeralUser(admin); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Retrieves the application restrictions for a given target application running in the calling * user. * <p> @@ -6291,6 +6610,37 @@ public class DevicePolicyManager { } /** + * Install an existing package that has been installed in another user, or has been kept after + * removal via {@link #setKeepUninstalledPackages}. + * This function can be called by a device owner, profile owner or a delegate given + * the {@link #DELEGATION_INSTALL_EXISTING_PACKAGE} scope via {@link #setDelegatedScopes}. + * When called in a secondary user or managed profile, the user/profile must be affiliated with + * the device owner. See {@link #setAffiliationIds}. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param packageName The package to be installed in the calling profile. + * @return {@code true} if the app is installed; {@code false} otherwise. + * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of + * an affiliated user or profile. + * @see #setKeepUninstalledPackages + * @see #setDelegatedScopes + * @see #setAffiliationIds + * @see #DELEGATION_PACKAGE_ACCESS + */ + public boolean installExistingPackage(@NonNull ComponentName admin, String packageName) { + throwIfParentInstance("installExistingPackage"); + if (mService != null) { + try { + return mService.installExistingPackage(admin, mContext.getPackageName(), + packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + + /** * Called by a device owner or profile owner to disable account management for a specific type * of account. * <p> @@ -6422,7 +6772,62 @@ public class DevicePolicyManager { } /** - * Called by device owners to update {@link android.provider.Settings.Global} settings. + * Sets which system features to enable for LockTask mode. + * <p> + * Feature flags set through this method will only take effect for the duration when the device + * is in LockTask mode. If this method is not called, none of the features listed here will be + * enabled. + * <p> + * This function can only be called by the device owner or by a profile owner of a user/profile + * that is affiliated with the device owner user. See {@link #setAffiliationIds}. Any features + * set via this method will be cleared if the user becomes unaffiliated. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param flags Bitfield of feature flags: + * {@link #LOCK_TASK_FEATURE_NONE} (default), + * {@link #LOCK_TASK_FEATURE_SYSTEM_INFO}, + * {@link #LOCK_TASK_FEATURE_NOTIFICATIONS}, + * {@link #LOCK_TASK_FEATURE_HOME}, + * {@link #LOCK_TASK_FEATURE_RECENTS}, + * {@link #LOCK_TASK_FEATURE_GLOBAL_ACTIONS}, + * {@link #LOCK_TASK_FEATURE_KEYGUARD} + * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of + * an affiliated user or profile. + */ + public void setLockTaskFeatures(@NonNull ComponentName admin, @LockTaskFeature int flags) { + throwIfParentInstance("setLockTaskFeatures"); + if (mService != null) { + try { + mService.setLockTaskFeatures(admin, flags); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Gets which system features are enabled for LockTask mode. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return bitfield of flags. See {@link #setLockTaskFeatures(ComponentName, int)} for a list. + * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of + * an affiliated user or profile. + * @see #setLockTaskFeatures(ComponentName, int) + */ + public @LockTaskFeature int getLockTaskFeatures(@NonNull ComponentName admin) { + throwIfParentInstance("getLockTaskFeatures"); + if (mService != null) { + try { + return mService.getLockTaskFeatures(admin); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return 0; + } + + /** + * Called by device owner to update {@link android.provider.Settings.Global} settings. * Validation that the value of the setting is in the correct form for the setting type should * be performed by the caller. * <p> @@ -6471,6 +6876,83 @@ public class DevicePolicyManager { } /** + * Called by device owner to update {@link android.provider.Settings.System} settings. + * Validation that the value of the setting is in the correct form for the setting type should + * be performed by the caller. + * <p> + * The settings that can be updated with this method are: + * <ul> + * <li>{@link android.provider.Settings.System#SCREEN_BRIGHTNESS}</li> + * <li>{@link android.provider.Settings.System#SCREEN_BRIGHTNESS_MODE}</li> + * <li>{@link android.provider.Settings.System#SCREEN_OFF_TIMEOUT}</li> + * </ul> + * <p> + * + * @see android.provider.Settings.System#SCREEN_OFF_TIMEOUT + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param setting The name of the setting to update. + * @param value The value to update the setting to. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public void setSystemSetting(@NonNull ComponentName admin, @NonNull String setting, + String value) { + throwIfParentInstance("setSystemSetting"); + if (mService != null) { + try { + mService.setSystemSetting(admin, setting, value); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Called by device owner to set the system wall clock time. This only takes effect if called + * when {@link android.provider.Settings.Global#AUTO_TIME} is 0, otherwise {@code false} will be + * returned. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with + * @param millis time in milliseconds since the Epoch + * @return {@code true} if set time succeeded, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public boolean setTime(@NonNull ComponentName admin, long millis) { + throwIfParentInstance("setTime"); + if (mService != null) { + try { + return mService.setTime(admin, millis); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + + /** + * Called by device owner to set the system's persistent default time zone. This only takes + * effect if called when {@link android.provider.Settings.Global#AUTO_TIME_ZONE} is 0, otherwise + * {@code false} will be returned. + * + * @see android.app.AlarmManager#setTimeZone(String) + * @param admin Which {@link DeviceAdminReceiver} this request is associated with + * @param timeZone one of the Olson ids from the list returned by + * {@link java.util.TimeZone#getAvailableIDs} + * @return {@code true} if set timezone succeeded, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public boolean setTimeZone(@NonNull ComponentName admin, String timeZone) { + throwIfParentInstance("setTimeZone"); + if (mService != null) { + try { + return mService.setTimeZone(admin, timeZone); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + + /** * Called by profile or device owners to update {@link android.provider.Settings.Secure} * settings. Validation that the value of the setting is in the correct form for the setting * type should be performed by the caller. @@ -6770,6 +7252,10 @@ public class DevicePolicyManager { * password, pin or pattern is set after the keyguard was disabled, the keyguard stops being * disabled. * + * <p> + * As of {@link android.os.Build.VERSION_CODES#P}, this call also dismisses the + * keyguard if it is currently shown. + * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param disabled {@code true} disables the keyguard, {@code false} reenables it. * @return {@code false} if attempting to disable the keyguard while a lock password was in @@ -6789,6 +7275,12 @@ public class DevicePolicyManager { * Called by device owner to disable the status bar. Disabling the status bar blocks * notifications, quick settings and other screen overlays that allow escaping from a single use * device. + * <p> + * <strong>Note:</strong> This method has no effect for LockTask mode. The behavior of the + * status bar in LockTask mode can be configured with + * {@link #setLockTaskFeatures(ComponentName, int)}. Calls to this method when the device is in + * LockTask mode will be registered, but will only take effect when the device leaves LockTask + * mode. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param disabled {@code true} disables the status bar, {@code false} reenables it. @@ -7508,12 +8000,12 @@ public class DevicePolicyManager { } /** - * Called by the device owner or profile owner to set the name of the organization under - * management. - * <p> - * If the organization name needs to be localized, it is the responsibility of the - * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast - * and set a new version of this string accordingly. + * Called by the device owner (since API 26) or profile owner (since API 24) to set the name of + * the organization under management. + * + * <p>If the organization name needs to be localized, it is the responsibility of the {@link + * DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast and set + * a new version of this string accordingly. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param title The organization name or {@code null} to clear a previously set name. @@ -8109,4 +8601,82 @@ public class DevicePolicyManager { throw re.rethrowFromSystemServer(); } } + + /** + * Called by the device owner or profile owner to clear application user data of a given + * package. The behaviour of this is equivalent to the target application calling + * {@link android.app.ActivityManager#clearApplicationUserData()}. + * + * <p><strong>Note:</strong> an application can store data outside of its application data, e.g. + * external storage or user dictionary. This data will not be wiped by calling this API. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param packageName The name of the package which will have its user data wiped. + * @param listener A callback object that will inform the caller when the clearing is done. + * @param handler The handler indicating the thread on which the listener should be invoked. + * @throws SecurityException if the caller is not the device owner/profile owner. + * @return whether the clearing succeeded. + */ + public boolean clearApplicationUserData(@NonNull ComponentName admin, + @NonNull String packageName, @NonNull OnClearApplicationUserDataListener listener, + @NonNull Handler handler) { + throwIfParentInstance("clearAppData"); + try { + return mService.clearApplicationUserData(admin, packageName, + new IPackageDataObserver.Stub() { + public void onRemoveCompleted(String pkg, boolean succeeded) { + handler.post(() -> + listener.onApplicationUserDataCleared(pkg, succeeded)); + } + }); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Called by a device owner to specify whether logout is enabled for all secondary users. The + * system may show a logout button that stops the user and switches back to the primary user. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param enabled whether logout should be enabled or not. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public void setLogoutEnabled(@NonNull ComponentName admin, boolean enabled) { + throwIfParentInstance("setLogoutEnabled"); + try { + mService.setLogoutEnabled(admin, enabled); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Returns whether logout is enabled by a device owner. + * + * @return {@code true} if logout is enabled by device owner, {@code false} otherwise. + */ + public boolean isLogoutEnabled() { + throwIfParentInstance("isLogoutEnabled"); + try { + return mService.isLogoutEnabled(); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Callback used in {@link #clearApplicationUserData} + * to indicate that the clearing of an application's user data is done. + */ + public interface OnClearApplicationUserDataListener { + /** + * Method invoked when clearing the application user data has completed. + * + * @param packageName The name of the package which had its user data cleared. + * @param succeeded Whether the clearing succeeded. Clearing fails for device administrator + * apps and protected system packages. + */ + void onApplicationUserDataCleared(String packageName, boolean succeeded); + } } diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java index eef2f983cc55..05f6c2a7da6d 100644 --- a/core/java/android/app/admin/DevicePolicyManagerInternal.java +++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java @@ -101,4 +101,18 @@ public abstract class DevicePolicyManagerInternal { * not enforced by the profile/device owner. */ public abstract Intent createUserRestrictionSupportIntent(int userId, String userRestriction); + + /** + * Returns whether this user/profile is affiliated with the device. + * + * <p> + * By definition, the user that the device owner runs on is always affiliated with the device. + * Any other user/profile is considered affiliated with the device if the set specified by its + * profile owner via {@link DevicePolicyManager#setAffiliationIds} intersects with the device + * owner's. + * <p> + * Profile owner on the primary user will never be considered as affiliated as there is no + * device owner to be affiliated with. + */ + public abstract boolean isUserAffiliatedWithDevice(int userId); } diff --git a/core/java/android/app/admin/DnsEvent.java b/core/java/android/app/admin/DnsEvent.java index f84c5b00a135..4ddf13e07344 100644 --- a/core/java/android/app/admin/DnsEvent.java +++ b/core/java/android/app/admin/DnsEvent.java @@ -34,46 +34,47 @@ import java.util.List; public final class DnsEvent extends NetworkEvent implements Parcelable { /** The hostname that was looked up. */ - private final String hostname; + private final String mHostname; /** Contains (possibly a subset of) the IP addresses returned. */ - private final String[] ipAddresses; + private final String[] mIpAddresses; /** * The number of IP addresses returned from the DNS lookup event. May be different from the * length of ipAddresses if there were too many addresses to log. */ - private final int ipAddressesCount; + private final int mIpAddressesCount; /** @hide */ public DnsEvent(String hostname, String[] ipAddresses, int ipAddressesCount, String packageName, long timestamp) { super(packageName, timestamp); - this.hostname = hostname; - this.ipAddresses = ipAddresses; - this.ipAddressesCount = ipAddressesCount; + this.mHostname = hostname; + this.mIpAddresses = ipAddresses; + this.mIpAddressesCount = ipAddressesCount; } private DnsEvent(Parcel in) { - this.hostname = in.readString(); - this.ipAddresses = in.createStringArray(); - this.ipAddressesCount = in.readInt(); - this.packageName = in.readString(); - this.timestamp = in.readLong(); + this.mHostname = in.readString(); + this.mIpAddresses = in.createStringArray(); + this.mIpAddressesCount = in.readInt(); + this.mPackageName = in.readString(); + this.mTimestamp = in.readLong(); + this.mId = in.readLong(); } /** Returns the hostname that was looked up. */ public String getHostname() { - return hostname; + return mHostname; } /** Returns (possibly a subset of) the IP addresses returned. */ public List<InetAddress> getInetAddresses() { - if (ipAddresses == null || ipAddresses.length == 0) { + if (mIpAddresses == null || mIpAddresses.length == 0) { return Collections.emptyList(); } - final List<InetAddress> inetAddresses = new ArrayList<>(ipAddresses.length); - for (final String ipAddress : ipAddresses) { + final List<InetAddress> inetAddresses = new ArrayList<>(mIpAddresses.length); + for (final String ipAddress : mIpAddresses) { try { // ipAddress is already an address, not a host name, no DNS resolution will happen. inetAddresses.add(InetAddress.getByName(ipAddress)); @@ -90,14 +91,14 @@ public final class DnsEvent extends NetworkEvent implements Parcelable { * addresses to log. */ public int getTotalResolvedAddressCount() { - return ipAddressesCount; + return mIpAddressesCount; } @Override public String toString() { - return String.format("DnsEvent(%s, %s, %d, %d, %s)", hostname, - (ipAddresses == null) ? "NONE" : String.join(" ", ipAddresses), - ipAddressesCount, timestamp, packageName); + return String.format("DnsEvent(%s, %s, %d, %d, %s)", mHostname, + (mIpAddresses == null) ? "NONE" : String.join(" ", mIpAddresses), + mIpAddressesCount, mTimestamp, mPackageName); } public static final Parcelable.Creator<DnsEvent> CREATOR @@ -125,11 +126,11 @@ public final class DnsEvent extends NetworkEvent implements Parcelable { public void writeToParcel(Parcel out, int flags) { // write parcel token first out.writeInt(PARCEL_TOKEN_DNS_EVENT); - out.writeString(hostname); - out.writeStringArray(ipAddresses); - out.writeInt(ipAddressesCount); - out.writeString(packageName); - out.writeLong(timestamp); + out.writeString(mHostname); + out.writeStringArray(mIpAddresses); + out.writeInt(mIpAddressesCount); + out.writeString(mPackageName); + out.writeLong(mTimestamp); + out.writeLong(mId); } } - diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index e361d819ac2d..94b0868ae567 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -26,6 +26,7 @@ import android.app.admin.PasswordMetrics; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.IPackageDataObserver; import android.content.pm.ParceledListSlice; import android.content.pm.StringParceledListSlice; import android.graphics.Bitmap; @@ -35,6 +36,7 @@ import android.os.Bundle; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.UserHandle; +import android.security.keystore.ParcelableKeyGenParameterSpec; import java.util.List; @@ -94,7 +96,7 @@ interface IDevicePolicyManager { void lockNow(int flags, boolean parent); - void wipeData(int flags); + void wipeDataWithReason(int flags, String wipeReasonForUser); ComponentName setGlobalProxy(in ComponentName admin, String proxySpec, String exclusionList); ComponentName getGlobalProxyAdmin(int userHandle); @@ -161,8 +163,10 @@ interface IDevicePolicyManager { boolean isCaCertApproved(in String alias, int userHandle); boolean installKeyPair(in ComponentName who, in String callerPackage, in byte[] privKeyBuffer, - in byte[] certBuffer, in byte[] certChainBuffer, String alias, boolean requestAccess); + in byte[] certBuffer, in byte[] certChainBuffer, String alias, boolean requestAccess, + boolean isUserSelectable); boolean removeKeyPair(in ComponentName who, in String callerPackage, String alias); + boolean generateKeyPair(in ComponentName who, in String callerPackage, in String algorithm, in ParcelableKeyGenParameterSpec keySpec); void choosePrivateKeyAlias(int uid, in Uri uri, in String alias, IBinder aliasCallback); void setDelegatedScopes(in ComponentName who, in String delegatePackage, in List<String> scopes); @@ -213,9 +217,13 @@ interface IDevicePolicyManager { UserHandle createAndManageUser(in ComponentName who, in String name, in ComponentName profileOwner, in PersistableBundle adminExtras, in int flags); boolean removeUser(in ComponentName who, in UserHandle userHandle); boolean switchUser(in ComponentName who, in UserHandle userHandle); + boolean stopUser(in ComponentName who, in UserHandle userHandle); + boolean logoutUser(in ComponentName who); + List<UserHandle> getSecondaryUsers(in ComponentName who); void enableSystemApp(in ComponentName admin, in String callerPackage, in String packageName); int enableSystemAppWithIntent(in ComponentName admin, in String callerPackage, in Intent intent); + boolean installExistingPackage(in ComponentName admin, in String callerPackage, in String packageName); void setAccountManagementDisabled(in ComponentName who, in String accountType, in boolean disabled); String[] getAccountTypesWithManagementDisabled(); @@ -225,9 +233,16 @@ interface IDevicePolicyManager { String[] getLockTaskPackages(in ComponentName who); boolean isLockTaskPermitted(in String pkg); + void setLockTaskFeatures(in ComponentName who, int flags); + int getLockTaskFeatures(in ComponentName who); + void setGlobalSetting(in ComponentName who, in String setting, in String value); + void setSystemSetting(in ComponentName who, in String setting, in String value); void setSecureSetting(in ComponentName who, in String setting, in String value); + boolean setTime(in ComponentName who, long millis); + boolean setTimeZone(in ComponentName who, String timeZone); + void setMasterVolumeMuted(in ComponentName admin, boolean on); boolean isMasterVolumeMuted(in ComponentName admin); @@ -343,6 +358,7 @@ interface IDevicePolicyManager { IApplicationThread caller, IBinder token, in Intent service, IServiceConnection connection, int flags, int targetUserId); List<UserHandle> getBindDeviceAdminTargetUsers(in ComponentName admin); + boolean isEphemeralUser(in ComponentName admin); long getLastSecurityLogRetrievalTime(); long getLastBugReportRequestTime(); @@ -355,4 +371,9 @@ interface IDevicePolicyManager { boolean isCurrentInputMethodSetByOwner(); StringParceledListSlice getOwnerInstalledCaCerts(in UserHandle user); + + boolean clearApplicationUserData(in ComponentName admin, in String packageName, in IPackageDataObserver callback); + + void setLogoutEnabled(in ComponentName admin, boolean enabled); + boolean isLogoutEnabled(); } diff --git a/core/java/android/app/admin/NetworkEvent.java b/core/java/android/app/admin/NetworkEvent.java index 2646c3fdba27..947e4fedbb79 100644 --- a/core/java/android/app/admin/NetworkEvent.java +++ b/core/java/android/app/admin/NetworkEvent.java @@ -18,8 +18,8 @@ package android.app.admin; import android.content.pm.PackageManager; import android.os.Parcel; -import android.os.Parcelable; import android.os.ParcelFormatException; +import android.os.Parcelable; /** * An abstract class that represents a network event. @@ -32,10 +32,13 @@ public abstract class NetworkEvent implements Parcelable { static final int PARCEL_TOKEN_CONNECT_EVENT = 2; /** The package name of the UID that performed the query. */ - String packageName; + String mPackageName; /** The timestamp of the event being reported in milliseconds. */ - long timestamp; + long mTimestamp; + + /** The id of the event. */ + long mId; /** @hide */ NetworkEvent() { @@ -44,8 +47,8 @@ public abstract class NetworkEvent implements Parcelable { /** @hide */ NetworkEvent(String packageName, long timestamp) { - this.packageName = packageName; - this.timestamp = timestamp; + this.mPackageName = packageName; + this.mTimestamp = timestamp; } /** @@ -53,7 +56,7 @@ public abstract class NetworkEvent implements Parcelable { * {@link PackageManager#getNameForUid}. */ public String getPackageName() { - return packageName; + return mPackageName; } /** @@ -61,7 +64,20 @@ public abstract class NetworkEvent implements Parcelable { * the time the event was reported and midnight, January 1, 1970 UTC. */ public long getTimestamp() { - return timestamp; + return mTimestamp; + } + + /** @hide */ + public void setId(long id) { + this.mId = id; + } + + /** + * Returns the id of the event, where the id monotonically increases for each event. The id + * is reset when the device reboots, and when network logging is enabled. + */ + public long getId() { + return this.mId; } @Override @@ -95,4 +111,3 @@ public abstract class NetworkEvent implements Parcelable { @Override public abstract void writeToParcel(Parcel out, int flags); } - diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java index bf715c35d9b7..7b549cd59666 100644 --- a/core/java/android/app/assist/AssistStructure.java +++ b/core/java/android/app/assist/AssistStructure.java @@ -359,6 +359,8 @@ public class AssistStructure implements Parcelable { if (DEBUG_PARCEL) Log.d(TAG, "Finished reading: at " + mCurParcel.dataPosition() + ", avail=" + mCurParcel.dataAvail() + ", windows=" + mNumReadWindows + ", views=" + mNumReadViews); + mCurParcel.recycle(); + mCurParcel = null; // Parcel cannot be used after recycled. } Parcel readParcel(int validateToken, int level) { @@ -396,20 +398,23 @@ public class AssistStructure implements Parcelable { private void fetchData() { Parcel data = Parcel.obtain(); - data.writeInterfaceToken(DESCRIPTOR); - data.writeStrongBinder(mTransferToken); - if (DEBUG_PARCEL) Log.d(TAG, "Requesting data with token " + mTransferToken); - if (mCurParcel != null) { - mCurParcel.recycle(); - } - mCurParcel = Parcel.obtain(); try { - mChannel.transact(TRANSACTION_XFER, data, mCurParcel, 0); - } catch (RemoteException e) { - Log.w(TAG, "Failure reading AssistStructure data", e); - throw new IllegalStateException("Failure reading AssistStructure data: " + e); + data.writeInterfaceToken(DESCRIPTOR); + data.writeStrongBinder(mTransferToken); + if (DEBUG_PARCEL) Log.d(TAG, "Requesting data with token " + mTransferToken); + if (mCurParcel != null) { + mCurParcel.recycle(); + } + mCurParcel = Parcel.obtain(); + try { + mChannel.transact(TRANSACTION_XFER, data, mCurParcel, 0); + } catch (RemoteException e) { + Log.w(TAG, "Failure reading AssistStructure data", e); + throw new IllegalStateException("Failure reading AssistStructure data: " + e); + } + } finally { + data.recycle(); } - data.recycle(); mNumReadWindows = mNumReadViews = 0; } } @@ -616,6 +621,9 @@ public class AssistStructure implements Parcelable { CharSequence[] mAutofillOptions; boolean mSanitized; HtmlInfo mHtmlInfo; + int mMinEms = -1; + int mMaxEms = -1; + int mMaxLength = -1; // POJO used to override some autofill-related values when the node is parcelized. // Not written to parcel. @@ -674,6 +682,7 @@ public class AssistStructure implements Parcelable { ViewNodeText mText; int mInputType; + String mWebScheme; String mWebDomain; Bundle mExtras; LocaleList mLocaleList; @@ -712,6 +721,9 @@ public class AssistStructure implements Parcelable { if (p instanceof HtmlInfo) { mHtmlInfo = (HtmlInfo) p; } + mMinEms = in.readInt(); + mMaxEms = in.readInt(); + mMaxLength = in.readInt(); } if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) { mX = in.readInt(); @@ -751,6 +763,7 @@ public class AssistStructure implements Parcelable { mInputType = in.readInt(); } if ((flags&FLAGS_HAS_URL) != 0) { + mWebScheme = in.readString(); mWebDomain = in.readString(); } if ((flags&FLAGS_HAS_LOCALE_LIST) != 0) { @@ -813,7 +826,7 @@ public class AssistStructure implements Parcelable { if (mInputType != 0) { flags |= FLAGS_HAS_INPUT_TYPE; } - if (mWebDomain != null) { + if (mWebScheme != null || mWebDomain != null) { flags |= FLAGS_HAS_URL; } if (mLocaleList != null) { @@ -874,6 +887,9 @@ public class AssistStructure implements Parcelable { } else { out.writeParcelable(null, 0); } + out.writeInt(mMinEms); + out.writeInt(mMaxEms); + out.writeInt(mMaxLength); } if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) { out.writeInt(mX); @@ -908,6 +924,7 @@ public class AssistStructure implements Parcelable { out.writeInt(mInputType); } if ((flags&FLAGS_HAS_URL) != 0) { + out.writeString(mWebScheme); out.writeString(mWebDomain); } if ((flags&FLAGS_HAS_LOCALE_LIST) != 0) { @@ -1272,6 +1289,19 @@ public class AssistStructure implements Parcelable { } /** + * Returns the scheme of the HTML document represented by this view. + * + * <p>Typically used when the view associated with the view is a container for an HTML + * document. + * + * @return scheme-only part of the document. For example, if the full URL is + * {@code https://example.com/login?user=my_user}, it returns {@code https}. + */ + @Nullable public String getWebScheme() { + return mWebScheme; + } + + /** * Returns the HTML properties associated with this view. * * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes, @@ -1428,6 +1458,39 @@ public class AssistStructure implements Parcelable { public ViewNode getChildAt(int index) { return mChildren[index]; } + + /** + * Returns the minimum width in ems of the text associated with this node, or {@code -1} + * if not supported by the node. + * + * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes, + * not for assist purposes. + */ + public int getMinTextEms() { + return mMinEms; + } + + /** + * Returns the maximum width in ems of the text associated with this node, or {@code -1} + * if not supported by the node. + * + * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes, + * not for assist purposes. + */ + public int getMaxTextEms() { + return mMaxEms; + } + + /** + * Returns the maximum length of the text associated with this node node, or {@code -1} + * if not supported by the node or not set. + * + * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes, + * not for assist purposes. + */ + public int getMaxTextLength() { + return mMaxLength; + } } /** @@ -1760,6 +1823,21 @@ public class AssistStructure implements Parcelable { } @Override + public void setMinTextEms(int minEms) { + mNode.mMinEms = minEms; + } + + @Override + public void setMaxTextEms(int maxEms) { + mNode.mMaxEms = maxEms; + } + + @Override + public void setMaxTextLength(int maxLength) { + mNode.mMaxLength = maxLength; + } + + @Override public void setDataIsSensitive(boolean sensitive) { mNode.mSanitized = !sensitive; } @@ -1767,10 +1845,13 @@ public class AssistStructure implements Parcelable { @Override public void setWebDomain(@Nullable String domain) { if (domain == null) { + mNode.mWebScheme = null; mNode.mWebDomain = null; return; } - mNode.mWebDomain = Uri.parse(domain).getHost(); + Uri uri = Uri.parse(domain); + mNode.mWebScheme = uri.getScheme(); + mNode.mWebDomain = uri.getHost(); } @Override diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 7aa80d263976..861cb9a8d035 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -263,6 +263,17 @@ public abstract class BackupAgent extends ContextWrapper { ParcelFileDescriptor newState) throws IOException; /** + * New version of {@link #onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor)} + * that handles a long app version code. Default implementation casts the version code to + * an int and calls {@link #onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor)}. + */ + public void onRestore(BackupDataInput data, long appVersionCode, + ParcelFileDescriptor newState) + throws IOException { + onRestore(data, (int) appVersionCode, newState); + } + + /** * The application is having its entire file system contents backed up. {@code data} * points to the backup destination, and the app has the opportunity to choose which * files are to be stored. To commit a file as part of the backup, call the @@ -947,7 +958,7 @@ public abstract class BackupAgent extends ContextWrapper { } @Override - public void doRestore(ParcelFileDescriptor data, int appVersionCode, + public void doRestore(ParcelFileDescriptor data, long appVersionCode, ParcelFileDescriptor newState, int token, IBackupManager callbackBinder) throws RemoteException { // Ensure that we're running with the app's normal permission level diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java index 9f9b217069d8..6512b98ca085 100644 --- a/core/java/android/app/backup/BackupManager.java +++ b/core/java/android/app/backup/BackupManager.java @@ -16,10 +16,12 @@ package android.app.backup; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -446,6 +448,57 @@ public class BackupManager { } /** + * Update the attributes of the transport identified by {@code transportComponent}. If the + * specified transport has not been bound at least once (for registration), this call will be + * ignored. Only the host process of the transport can change its description, otherwise a + * {@link SecurityException} will be thrown. + * + * @param transportComponent The identity of the transport being described. + * @param name A {@link String} with the new name for the transport. This is NOT for + * identification. MUST NOT be {@code null}. + * @param configurationIntent An {@link Intent} that can be passed to + * {@link Context#startActivity} in order to launch the transport's configuration UI. It may + * be {@code null} if the transport does not offer any user-facing configuration UI. + * @param currentDestinationString A {@link String} describing the destination to which the + * transport is currently sending data. MUST NOT be {@code null}. + * @param dataManagementIntent An {@link Intent} that can be passed to + * {@link Context#startActivity} in order to launch the transport's data-management UI. It + * may be {@code null} if the transport does not offer any user-facing data + * management UI. + * @param dataManagementLabel A {@link String} to be used as the label for the transport's data + * management affordance. This MUST be {@code null} when dataManagementIntent is + * {@code null} and MUST NOT be {@code null} when dataManagementIntent is not {@code null}. + * @throws SecurityException If the UID of the calling process differs from the package UID of + * {@code transportComponent} or if the caller does NOT have BACKUP permission. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BACKUP) + public void updateTransportAttributes( + ComponentName transportComponent, + String name, + @Nullable Intent configurationIntent, + String currentDestinationString, + @Nullable Intent dataManagementIntent, + @Nullable String dataManagementLabel) { + checkServiceBinder(); + if (sService != null) { + try { + sService.updateTransportAttributes( + transportComponent, + name, + configurationIntent, + currentDestinationString, + dataManagementIntent, + dataManagementLabel); + } catch (RemoteException e) { + Log.e(TAG, "describeTransport() couldn't connect"); + } + } + } + + /** * Specify the current backup transport. * * @param transport The name of the transport to select. This should be one diff --git a/core/java/android/app/backup/BackupManagerMonitor.java b/core/java/android/app/backup/BackupManagerMonitor.java index ebad16e0bc3d..ae4a98a4e06e 100644 --- a/core/java/android/app/backup/BackupManagerMonitor.java +++ b/core/java/android/app/backup/BackupManagerMonitor.java @@ -40,9 +40,14 @@ public class BackupManagerMonitor { /** string : the package name */ public static final String EXTRA_LOG_EVENT_PACKAGE_NAME = "android.app.backup.extra.LOG_EVENT_PACKAGE_NAME"; - /** int : the versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME */ + /** int : the versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME + * @deprecated Use {@link #EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION} */ + @Deprecated public static final String EXTRA_LOG_EVENT_PACKAGE_VERSION = "android.app.backup.extra.LOG_EVENT_PACKAGE_VERSION"; + /** long : the full versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME */ + public static final String EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION = + "android.app.backup.extra.LOG_EVENT_PACKAGE_FULL_VERSION"; /** int : the id of the log message, will be a unique identifier */ public static final String EXTRA_LOG_EVENT_ID = "android.app.backup.extra.LOG_EVENT_ID"; /** diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index f1dc6d293914..c42a8989cd74 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -222,6 +222,36 @@ interface IBackupManager { IFullBackupRestoreObserver observer); /** + * Update the attributes of the transport identified by {@code transportComponent}. If the + * specified transport has not been bound at least once (for registration), this call will be + * ignored. Only the host process of the transport can change its description, otherwise a + * {@link SecurityException} will be thrown. + * + * @param transportComponent The identity of the transport being described. + * @param name A {@link String} with the new name for the transport. This is NOT for + * identification. MUST NOT be {@code null}. + * @param configurationIntent An {@link Intent} that can be passed to + * {@link Context#startActivity} in order to launch the transport's configuration UI. It may + * be {@code null} if the transport does not offer any user-facing configuration UI. + * @param currentDestinationString A {@link String} describing the destination to which the + * transport is currently sending data. MUST NOT be {@code null}. + * @param dataManagementIntent An {@link Intent} that can be passed to + * {@link Context#startActivity} in order to launch the transport's data-management UI. It + * may be {@code null} if the transport does not offer any user-facing data + * management UI. + * @param dataManagementLabel A {@link String} to be used as the label for the transport's data + * management affordance. This MUST be {@code null} when dataManagementIntent is + * {@code null} and MUST NOT be {@code null} when dataManagementIntent is not {@code null}. + * @throws SecurityException If the UID of the calling process differs from the package UID of + * {@code transportComponent} or if the caller does NOT have BACKUP permission. + * + * @hide + */ + void updateTransportAttributes(in ComponentName transportComponent, in String name, + in Intent configurationIntent, in String currentDestinationString, + in Intent dataManagementIntent, in String dataManagementLabel); + + /** * Identify the currently selected transport. Callers must hold the * android.permission.BACKUP permission to use this method. */ diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java index 87e516cabba5..7c40b4eaf375 100644 --- a/core/java/android/app/job/JobInfo.java +++ b/core/java/android/app/job/JobInfo.java @@ -16,14 +16,23 @@ 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; import android.annotation.IntDef; import android.annotation.NonNull; 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; @@ -55,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) @@ -68,8 +78,24 @@ 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; /** * Amount of backoff a job has initially by default, in milliseconds. @@ -218,6 +244,13 @@ public class JobInfo implements Parcelable { public static final int FLAG_WILL_BE_FOREGROUND = 1 << 0; /** + * Allows this job to run despite doze restrictions as long as the app is in the foreground + * or on the temporary whitelist + * @hide + */ + public static final int FLAG_IMPORTANT_WHILE_FOREGROUND = 1 << 1; + + /** * @hide */ public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0; @@ -249,7 +282,8 @@ 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; private final boolean isPeriodic; @@ -317,7 +351,8 @@ public class JobInfo implements Parcelable { } /** - * Whether this job needs the device to be plugged in. + * Whether this job requires that the device be charging (or be a non-battery-powered + * device connected to permanent power, such as Android TV devices). */ public boolean isRequireCharging() { return (constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0; @@ -331,7 +366,10 @@ public class JobInfo implements Parcelable { } /** - * Whether this job needs the device to be in an Idle maintenance window. + * Whether this job requires that the user <em>not</em> be interacting with the device. + * + * <p class="note">This is <em>not</em> the same as "doze" or "device idle"; + * it is purely about the user's direct interactions.</p> */ public boolean isRequireDeviceIdle() { return (constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0; @@ -376,10 +414,49 @@ 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; + } + + /** + * Return the estimated size of network traffic that will be performed by + * this job, in bytes. + * + * @return Estimated size of network traffic, or + * {@link #NETWORK_BYTES_UNKNOWN} when unknown. + * @see Builder#setEstimatedNetworkBytes(long) + */ + public @BytesLong long getEstimatedNetworkBytes() { + return networkBytes; } /** @@ -417,8 +494,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; } /** @@ -426,10 +502,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; } /** @@ -438,8 +511,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; } /** @@ -517,7 +589,10 @@ 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) { return false; } if (minLatencyMillis != j.minLatencyMillis) { @@ -577,7 +652,10 @@ 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); hashCode = 31 * hashCode + Boolean.hashCode(isPeriodic); @@ -607,7 +685,12 @@ 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(); isPeriodic = in.readInt() == 1; @@ -635,7 +718,8 @@ 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; isPeriodic = b.mIsPeriodic; @@ -672,7 +756,13 @@ 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); out.writeInt(isPeriodic ? 1 : 0); @@ -805,7 +895,8 @@ 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; private long mTriggerContentMaxDelay = -1; @@ -905,22 +996,138 @@ 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"> + * 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; } /** - * Specify that to run this job, the device needs to be plugged in. This defaults to - * false. - * @param requiresCharging Whether or not the device is plugged in. + * Set the estimated size of network traffic that will be performed by + * this job, in bytes. + * <p> + * Apps are encouraged to provide values that are as accurate as + * possible, but when the exact size isn't available, an + * order-of-magnitude estimate can be provided instead. Here are some + * specific examples: + * <ul> + * <li>A job that is backing up a photo knows the exact size of that + * photo, so it should provide that size as the estimate. + * <li>A job that refreshes top news stories wouldn't know an exact + * size, but if the size is expected to be consistently around 100KB, it + * can provide that order-of-magnitude value as the estimate. + * <li>A job that synchronizes email could end up using an extreme range + * of data, from under 1KB when nothing has changed, to dozens of MB + * when there are new emails with attachments. Jobs that cannot provide + * reasonable estimates should leave this estimated value undefined. + * </ul> + * Note that the system may choose to delay jobs with large network + * usage estimates when the device has a poor network connection, in + * order to save battery. + * + * @param networkBytes The estimated size of network traffic that will + * be performed by this job, in bytes. This value only + * reflects the traffic that will be performed by the base + * job; if you're using {@link JobWorkItem} then you also + * need to define the network traffic used by each work item + * when constructing them. + * @see JobInfo#getEstimatedNetworkBytes() + * @see JobWorkItem#JobWorkItem(android.content.Intent, long) + */ + public Builder setEstimatedNetworkBytes(@BytesLong long networkBytes) { + mNetworkBytes = networkBytes; + return this; + } + + /** + * Specify that to run this job, the device must be charging (or be a + * non-battery-powered device connected to permanent power, such as Android TV + * devices). This defaults to {@code false}. + * + * <p class="note">For purposes of running jobs, a battery-powered device + * "charging" is not quite the same as simply being connected to power. If the + * device is so busy that the battery is draining despite a power connection, jobs + * with this constraint will <em>not</em> run. This can happen during some + * common use cases such as video chat, particularly if the device is plugged in + * to USB rather than to wall power. + * + * @param requiresCharging Pass {@code true} to require that the device be + * charging in order to run the job. */ public Builder setRequiresCharging(boolean requiresCharging) { mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_CHARGING) @@ -942,14 +1149,22 @@ public class JobInfo implements Parcelable { } /** - * Specify that to run, the job needs the device to be in idle mode. This defaults to - * false. - * <p>Idle mode is a loose definition provided by the system, which means that the device - * is not in use, and has not been in use for some time. As such, it is a good time to - * perform resource heavy jobs. Bear in mind that battery usage will still be attributed - * to your application, and surfaced to the user in battery stats.</p> - * @param requiresDeviceIdle Whether or not the device need be within an idle maintenance - * window. + * When set {@code true}, ensure that this job will not run if the device is in active use. + * The default state is {@code false}: that is, the for the job to be runnable even when + * someone is interacting with the device. + * + * <p>This state is a loose definition provided by the system. In general, it means that + * the device is not currently being used interactively, and has not been in use for some + * time. As such, it is a good time to perform resource heavy jobs. Bear in mind that + * battery usage will still be attributed to your application, and surfaced to the user in + * battery stats.</p> + * + * <p class="note">Despite the similar naming, this job constraint is <em>not</em> + * related to the system's "device idle" or "doze" states. This constraint only + * determines whether a job is allowed to run while the device is directly in use. + * + * @param requiresDeviceIdle Pass {@code true} to prevent the job from running + * while the device is being used interactively. */ public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) { mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_DEVICE_IDLE) @@ -1047,6 +1262,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; @@ -1096,6 +1326,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; @@ -1103,6 +1340,30 @@ public class JobInfo implements Parcelable { } /** + * Setting this to true indicates that this job is important while the scheduling app + * is in the foreground or on the temporary whitelist for background restrictions. + * This means that the system will relax doze restrictions on this job during this time. + * + * Apps should use this flag only for short jobs that are essential for the app to function + * properly in the foreground. + * + * Note that once the scheduling app is no longer whitelisted from background restrictions + * and in the background, or the job failed due to unsatisfied constraints, + * this job should be expected to behave like other jobs without this flag. + * + * @param importantWhileForeground whether to relax doze restrictions for this job when the + * app is in the foreground. False by default. + */ + public Builder setImportantWhileForeground(boolean importantWhileForeground) { + if (importantWhileForeground) { + mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND; + } else { + mFlags &= (~FLAG_IMPORTANT_WHILE_FOREGROUND); + } + return this; + } + + /** * Set whether or not to persist this job across device reboots. * * @param isPersisted True to indicate that the job will be written to @@ -1120,11 +1381,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 && 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) { @@ -1154,36 +1426,16 @@ public class JobInfo implements Parcelable { "persisted job"); } } + if ((mFlags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0 && mHasEarlyConstraint) { + throw new IllegalArgumentException("An important while foreground job cannot " + + "have a time delay"); + } if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { throw new IllegalArgumentException("An idle mode job will not respect any" + " 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/core/java/android/app/job/JobParameters.java b/core/java/android/app/job/JobParameters.java index a6f6be22809c..5053dc6fdf05 100644 --- a/core/java/android/app/job/JobParameters.java +++ b/core/java/android/app/job/JobParameters.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.IJobCallback; import android.content.ClipData; +import android.net.Network; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; @@ -66,6 +67,7 @@ public class JobParameters implements Parcelable { private final boolean overrideDeadlineExpired; private final Uri[] mTriggeredContentUris; private final String[] mTriggeredContentAuthorities; + private final Network network; private int stopReason; // Default value of stopReason is REASON_CANCELED @@ -73,7 +75,7 @@ public class JobParameters implements Parcelable { public JobParameters(IBinder callback, int jobId, PersistableBundle extras, Bundle transientExtras, ClipData clipData, int clipGrantFlags, boolean overrideDeadlineExpired, Uri[] triggeredContentUris, - String[] triggeredContentAuthorities) { + String[] triggeredContentAuthorities, Network network) { this.jobId = jobId; this.extras = extras; this.transientExtras = transientExtras; @@ -83,6 +85,7 @@ public class JobParameters implements Parcelable { this.overrideDeadlineExpired = overrideDeadlineExpired; this.mTriggeredContentUris = triggeredContentUris; this.mTriggeredContentAuthorities = triggeredContentAuthorities; + this.network = network; } /** @@ -171,6 +174,28 @@ public class JobParameters implements Parcelable { } /** + * Return the network that should be used to perform any network requests + * for this job. + * <p> + * Devices may have multiple active network connections simultaneously, or + * they may not have a default network route at all. To correctly handle all + * situations like this, your job should always use the network returned by + * this method instead of implicitly using the default network route. + * <p> + * Note that the system may relax the constraints you originally requested, + * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over + * a metered network when there is a surplus of metered data available. + * + * @return the network that should be used to perform any network requests + * for this job, or {@code null} if this job didn't set any required + * network type. + * @see JobInfo.Builder#setRequiredNetworkType(int) + */ + public @Nullable Network getNetwork() { + return network; + } + + /** * Dequeue the next pending {@link JobWorkItem} from these JobParameters associated with their * currently running job. Calling this method when there is no more work available and all * previously dequeued work has been completed will result in the system taking care of @@ -257,6 +282,11 @@ public class JobParameters implements Parcelable { overrideDeadlineExpired = in.readInt() == 1; mTriggeredContentUris = in.createTypedArray(Uri.CREATOR); mTriggeredContentAuthorities = in.createStringArray(); + if (in.readInt() != 0) { + network = Network.CREATOR.createFromParcel(in); + } else { + network = null; + } stopReason = in.readInt(); } @@ -286,6 +316,12 @@ public class JobParameters implements Parcelable { dest.writeInt(overrideDeadlineExpired ? 1 : 0); dest.writeTypedArray(mTriggeredContentUris, flags); dest.writeStringArray(mTriggeredContentAuthorities); + if (network != null) { + dest.writeInt(1); + network.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } dest.writeInt(stopReason); } diff --git a/core/java/android/app/job/JobScheduler.java b/core/java/android/app/job/JobScheduler.java index 3868439f092f..0deb2e13dce0 100644 --- a/core/java/android/app/job/JobScheduler.java +++ b/core/java/android/app/job/JobScheduler.java @@ -24,7 +24,6 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.ClipData; import android.content.Context; -import android.content.Intent; import android.os.Bundle; import android.os.PersistableBundle; @@ -40,16 +39,18 @@ import java.util.List; * and how to construct them. You will construct these JobInfo objects and pass them to the * JobScheduler with {@link #schedule(JobInfo)}. When the criteria declared are met, the * system will execute this job on your application's {@link android.app.job.JobService}. - * You identify which JobService is meant to execute the logic for your job when you create the - * JobInfo with + * You identify the service component that implements the logic for your job when you + * construct the JobInfo using * {@link android.app.job.JobInfo.Builder#JobInfo.Builder(int,android.content.ComponentName)}. * </p> * <p> - * The framework will be intelligent about when you receive your callbacks, and attempt to batch - * and defer them as much as possible. Typically if you don't specify a deadline on your job, it - * can be run at any moment depending on the current state of the JobScheduler's internal queue, - * however it might be deferred as long as until the next time the device is connected to a power - * source. + * The framework will be intelligent about when it executes jobs, and attempt to batch + * and defer them as much as possible. Typically if you don't specify a deadline on a job, it + * can be run at any moment depending on the current state of the JobScheduler's internal queue. + * <p> + * While a job is running, the system holds a wakelock on behalf of your app. For this reason, + * you do not need to take any action to guarantee that the device stays awake for the + * duration of the job. * </p> * <p>You do not * instantiate this class directly; instead, retrieve it through @@ -141,30 +142,34 @@ public abstract class JobScheduler { int userId, String tag); /** - * Cancel a job that is pending in the JobScheduler. - * @param jobId unique identifier for this job. Obtain this value from the jobs returned by - * {@link #getAllPendingJobs()}. + * Cancel the specified job. If the job is currently executing, it is stopped + * immediately and the return value from its {@link JobService#onStopJob(JobParameters)} + * method is ignored. + * + * @param jobId unique identifier for the job to be canceled, as supplied to + * {@link JobInfo.Builder#JobInfo.Builder(int, android.content.ComponentName) + * JobInfo.Builder(int, android.content.ComponentName)}. */ public abstract void cancel(int jobId); /** - * Cancel all jobs that have been registered with the JobScheduler by this package. + * Cancel <em>all</em> jobs that have been scheduled by the calling application. */ public abstract void cancelAll(); /** - * Retrieve all jobs for this package that are pending in the JobScheduler. + * Retrieve all jobs that have been scheduled by the calling application. * - * @return a list of all the jobs registered by this package that have not - * yet been executed. + * @return a list of all of the app's scheduled jobs. This includes jobs that are + * currently started as well as those that are still waiting to run. */ public abstract @NonNull List<JobInfo> getAllPendingJobs(); /** - * Retrieve a specific job for this package that is pending in the - * JobScheduler. + * Look up the description of a scheduled job. * - * @return job registered by this package that has not yet been executed. + * @return The {@link JobInfo} description of the given scheduled job, or {@code null} + * if the supplied job ID does not correspond to any job. */ public abstract @Nullable JobInfo getPendingJob(int jobId); } diff --git a/core/java/android/app/job/JobService.java b/core/java/android/app/job/JobService.java index 9096b47b8d4d..61afadab9b0c 100644 --- a/core/java/android/app/job/JobService.java +++ b/core/java/android/app/job/JobService.java @@ -18,16 +18,7 @@ package android.app.job; import android.app.Service; import android.content.Intent; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.util.Log; - -import com.android.internal.annotations.GuardedBy; - -import java.lang.ref.WeakReference; /** * <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p> @@ -55,7 +46,7 @@ public abstract class JobService extends Service { * </pre> * * <p>If a job service is declared in the manifest but not protected with this - * permission, that service will be ignored by the OS. + * permission, that service will be ignored by the system. */ public static final String PERMISSION_BIND = "android.permission.BIND_JOB_SERVICE"; @@ -81,14 +72,63 @@ public abstract class JobService extends Service { } /** - * Override this method with the callback logic for your job. Any such logic needs to be - * performed on a separate thread, as this function is executed on your application's main - * thread. + * Call this to inform the JobScheduler that the job has finished its work. When the + * system receives this message, it releases the wakelock being held for the job. + * <p> + * You can request that the job be scheduled again by passing {@code true} as + * the <code>wantsReschedule</code> parameter. This will apply back-off policy + * for the job; this policy can be adjusted through the + * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} method + * when the job is originally scheduled. The job's initial + * requirements are preserved when jobs are rescheduled, regardless of backed-off + * policy. + * <p class="note"> + * A job running while the device is dozing will not be rescheduled with the normal back-off + * policy. Instead, the job will be re-added to the queue and executed again during + * a future idle maintenance window. + * </p> + * + * @param params The parameters identifying this job, as supplied to + * the job in the {@link #onStartJob(JobParameters)} callback. + * @param wantsReschedule {@code true} if this job should be rescheduled according + * to the back-off criteria specified when it was first scheduled; {@code false} + * otherwise. + */ + public final void jobFinished(JobParameters params, boolean wantsReschedule) { + mEngine.jobFinished(params, wantsReschedule); + } + + /** + * Called to indicate that the job has begun executing. Override this method with the + * logic for your job. Like all other component lifecycle callbacks, this method executes + * on your application's main thread. + * <p> + * Return {@code true} from this method if your job needs to continue running. If you + * do this, the job remains active until you call + * {@link #jobFinished(JobParameters, boolean)} to tell the system that it has completed + * its work, or until the job's required constraints are no longer satisfied. For + * example, if the job was scheduled using + * {@link JobInfo.Builder#setRequiresCharging(boolean) setRequiresCharging(true)}, + * it will be immediately halted by the system if the user unplugs the device from power, + * the job's {@link #onStopJob(JobParameters)} callback will be invoked, and the app + * will be expected to shut down all ongoing work connected with that job. + * <p> + * The system holds a wakelock on behalf of your app as long as your job is executing. + * This wakelock is acquired before this method is invoked, and is not released until either + * you call {@link #jobFinished(JobParameters, boolean)}, or after the system invokes + * {@link #onStopJob(JobParameters)} to notify your job that it is being shut down + * prematurely. + * <p> + * Returning {@code false} from this method means your job is already finished. The + * system's wakelock for the job will be released, and {@link #onStopJob(JobParameters)} + * will not be invoked. * - * @param params Parameters specifying info about this job, including the extras bundle you - * optionally provided at job-creation time. - * @return True if your service needs to process the work (on a separate thread). False if - * there's no more work to be done for this job. + * @param params Parameters specifying info about this job, including the optional + * extras configured with {@link JobInfo.Builder#setExtras(android.os.PersistableBundle). + * This object serves to identify this specific running job instance when calling + * {@link #jobFinished(JobParameters, boolean)}. + * @return {@code true} if your service will continue running, using a separate thread + * when appropriate. {@code false} means that this job has completed its work. */ public abstract boolean onStartJob(JobParameters params); @@ -101,37 +141,17 @@ public abstract class JobService extends Service { * {@link android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}, yet while your * job was executing the user toggled WiFi. Another example is if you had specified * {@link android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its - * idle maintenance window. You are solely responsible for the behaviour of your application - * upon receipt of this message; your app will likely start to misbehave if you ignore it. One - * immediate repercussion is that the system will cease holding a wakelock for you.</p> - * - * @param params Parameters specifying info about this job. - * @return True to indicate to the JobManager whether you'd like to reschedule this job based - * on the retry criteria provided at job creation-time. False to drop the job. Regardless of - * the value returned, your job must stop executing. - */ - public abstract boolean onStopJob(JobParameters params); - - /** - * Call this to inform the JobManager you've finished executing. This can be called from any - * thread, as it will ultimately be run on your application's main thread. When the system - * receives this message it will release the wakelock being held. + * idle maintenance window. You are solely responsible for the behavior of your application + * upon receipt of this message; your app will likely start to misbehave if you ignore it. * <p> - * You can specify post-execution behaviour to the scheduler here with - * <code>needsReschedule </code>. This will apply a back-off timer to your job based on - * the default, or what was set with - * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}. The original - * requirements are always honoured even for a backed-off job. Note that a job running in - * idle mode will not be backed-off. Instead what will happen is the job will be re-added - * to the queue and re-executed within a future idle maintenance window. - * </p> + * Once this method returns, the system releases the wakelock that it is holding on + * behalf of the job.</p> * - * @param params Parameters specifying system-provided info about this job, this was given to - * your application in {@link #onStartJob(JobParameters)}. - * @param needsReschedule True if this job should be rescheduled according to the back-off - * criteria specified at schedule-time. False otherwise. + * @param params The parameters identifying this job, as supplied to + * the job in the {@link #onStartJob(JobParameters)} callback. + * @return {@code true} to indicate to the JobManager whether you'd like to reschedule + * this job based on the retry criteria provided at job creation-time; or {@code false} + * to end the job entirely. Regardless of the value returned, your job must stop executing. */ - public final void jobFinished(JobParameters params, boolean needsReschedule) { - mEngine.jobFinished(params, needsReschedule); - } -}
\ No newline at end of file + public abstract boolean onStopJob(JobParameters params); +} diff --git a/core/java/android/app/job/JobWorkItem.java b/core/java/android/app/job/JobWorkItem.java index 0eb0450e8f2a..1c46e8ecbe52 100644 --- a/core/java/android/app/job/JobWorkItem.java +++ b/core/java/android/app/job/JobWorkItem.java @@ -16,6 +16,7 @@ package android.app.job; +import android.annotation.BytesLong; import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; @@ -27,6 +28,7 @@ import android.os.Parcelable; */ final public class JobWorkItem implements Parcelable { final Intent mIntent; + final long mNetworkBytes; int mDeliveryCount; int mWorkId; Object mGrants; @@ -39,6 +41,22 @@ final public class JobWorkItem implements Parcelable { */ public JobWorkItem(Intent intent) { mIntent = intent; + mNetworkBytes = JobInfo.NETWORK_BYTES_UNKNOWN; + } + + /** + * Create a new piece of work, which can be submitted to + * {@link JobScheduler#enqueue JobScheduler.enqueue}. + * + * @param intent The general Intent describing this work. + * @param networkBytes The estimated size of network traffic that will be + * performed by this job work item, in bytes. See + * {@link JobInfo.Builder#setEstimatedNetworkBytes(long)} for + * details about how to estimate. + */ + public JobWorkItem(Intent intent, @BytesLong long networkBytes) { + mIntent = intent; + mNetworkBytes = networkBytes; } /** @@ -49,6 +67,17 @@ final public class JobWorkItem implements Parcelable { } /** + * Return the estimated size of network traffic that will be performed by + * this job work item, in bytes. + * + * @return estimated size, or {@link JobInfo#NETWORK_BYTES_UNKNOWN} when + * unknown. + */ + public @BytesLong long getEstimatedNetworkBytes() { + return mNetworkBytes; + } + + /** * Return the count of the number of times this work item has been delivered * to the job. The value will be > 1 if it has been redelivered because the job * was stopped or crashed while it had previously been delivered but before the @@ -99,6 +128,10 @@ final public class JobWorkItem implements Parcelable { sb.append(mWorkId); sb.append(" intent="); sb.append(mIntent); + if (mNetworkBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + sb.append(" networkBytes="); + sb.append(mNetworkBytes); + } if (mDeliveryCount != 0) { sb.append(" dcount="); sb.append(mDeliveryCount); @@ -118,6 +151,7 @@ final public class JobWorkItem implements Parcelable { } else { out.writeInt(0); } + out.writeLong(mNetworkBytes); out.writeInt(mDeliveryCount); out.writeInt(mWorkId); } @@ -139,6 +173,7 @@ final public class JobWorkItem implements Parcelable { } else { mIntent = null; } + mNetworkBytes = in.readLong(); mDeliveryCount = in.readInt(); mWorkId = in.readInt(); } diff --git a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java new file mode 100644 index 000000000000..a2b7d5809c4d --- /dev/null +++ b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java @@ -0,0 +1,116 @@ +/* + * 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.app.servertransaction; + +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; +import static android.view.Display.INVALID_DISPLAY; + +import android.app.ClientTransactionHandler; +import android.content.res.Configuration; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Trace; + +import java.util.Objects; + +/** + * Activity configuration changed callback. + * @hide + */ +public class ActivityConfigurationChangeItem extends ClientTransactionItem { + + private Configuration mConfiguration; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + // TODO(lifecycler): detect if PIP or multi-window mode changed and report it here. + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged"); + client.handleActivityConfigurationChanged(token, mConfiguration, INVALID_DISPLAY); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + + // ObjectPoolItem implementation + + private ActivityConfigurationChangeItem() {} + + /** Obtain an instance initialized with provided params. */ + public static ActivityConfigurationChangeItem obtain(Configuration config) { + ActivityConfigurationChangeItem instance = + ObjectPool.obtain(ActivityConfigurationChangeItem.class); + if (instance == null) { + instance = new ActivityConfigurationChangeItem(); + } + instance.mConfiguration = config; + + return instance; + } + + @Override + public void recycle() { + mConfiguration = null; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedObject(mConfiguration, flags); + } + + /** Read from Parcel. */ + private ActivityConfigurationChangeItem(Parcel in) { + mConfiguration = in.readTypedObject(Configuration.CREATOR); + } + + public static final Creator<ActivityConfigurationChangeItem> CREATOR = + new Creator<ActivityConfigurationChangeItem>() { + public ActivityConfigurationChangeItem createFromParcel(Parcel in) { + return new ActivityConfigurationChangeItem(in); + } + + public ActivityConfigurationChangeItem[] newArray(int size) { + return new ActivityConfigurationChangeItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ActivityConfigurationChangeItem other = (ActivityConfigurationChangeItem) o; + return Objects.equals(mConfiguration, other.mConfiguration); + } + + @Override + public int hashCode() { + return mConfiguration.hashCode(); + } + + @Override + public String toString() { + return "ActivityConfigurationChange{config=" + mConfiguration + "}"; + } +} diff --git a/core/java/android/app/servertransaction/ActivityLifecycleItem.java b/core/java/android/app/servertransaction/ActivityLifecycleItem.java new file mode 100644 index 000000000000..24141e5152b9 --- /dev/null +++ b/core/java/android/app/servertransaction/ActivityLifecycleItem.java @@ -0,0 +1,47 @@ +/* + * 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.app.servertransaction; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Request for lifecycle state that an activity should reach. + * @hide + */ +public abstract class ActivityLifecycleItem extends ClientTransactionItem { + + @IntDef({UNDEFINED, PRE_ON_CREATE, ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, + ON_DESTROY, ON_RESTART}) + @Retention(RetentionPolicy.SOURCE) + public @interface LifecycleState{} + public static final int UNDEFINED = -1; + public static final int PRE_ON_CREATE = 0; + public static final int ON_CREATE = 1; + public static final int ON_START = 2; + public static final int ON_RESUME = 3; + public static final int ON_PAUSE = 4; + public static final int ON_STOP = 5; + public static final int ON_DESTROY = 6; + public static final int ON_RESTART = 7; + + /** A final lifecycle state that an activity should reach. */ + @LifecycleState + public abstract int getTargetState(); +} diff --git a/core/java/android/app/servertransaction/ActivityResultItem.java b/core/java/android/app/servertransaction/ActivityResultItem.java new file mode 100644 index 000000000000..73b5ec440441 --- /dev/null +++ b/core/java/android/app/servertransaction/ActivityResultItem.java @@ -0,0 +1,121 @@ +/* + * 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.app.servertransaction; + +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; + +import android.app.ClientTransactionHandler; +import android.app.ResultInfo; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Trace; + +import java.util.List; +import java.util.Objects; + +/** + * Activity result delivery callback. + * @hide + */ +public class ActivityResultItem extends ClientTransactionItem { + + private List<ResultInfo> mResultInfoList; + + @Override + public int getPreExecutionState() { + return ON_PAUSE; + } + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityDeliverResult"); + client.handleSendResult(token, mResultInfoList); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + + // ObjectPoolItem implementation + + private ActivityResultItem() {} + + /** Obtain an instance initialized with provided params. */ + public static ActivityResultItem obtain(List<ResultInfo> resultInfoList) { + ActivityResultItem instance = ObjectPool.obtain(ActivityResultItem.class); + if (instance == null) { + instance = new ActivityResultItem(); + } + instance.mResultInfoList = resultInfoList; + + return instance; + } + + @Override + public void recycle() { + mResultInfoList = null; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(mResultInfoList, flags); + } + + /** Read from Parcel. */ + private ActivityResultItem(Parcel in) { + mResultInfoList = in.createTypedArrayList(ResultInfo.CREATOR); + } + + public static final Parcelable.Creator<ActivityResultItem> CREATOR = + new Parcelable.Creator<ActivityResultItem>() { + public ActivityResultItem createFromParcel(Parcel in) { + return new ActivityResultItem(in); + } + + public ActivityResultItem[] newArray(int size) { + return new ActivityResultItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ActivityResultItem other = (ActivityResultItem) o; + return Objects.equals(mResultInfoList, other.mResultInfoList); + } + + @Override + public int hashCode() { + return mResultInfoList.hashCode(); + } + + @Override + public String toString() { + return "ActivityResultItem{resultInfoList=" + mResultInfoList + "}"; + } +} diff --git a/core/java/android/app/servertransaction/BaseClientRequest.java b/core/java/android/app/servertransaction/BaseClientRequest.java new file mode 100644 index 000000000000..c91e0ca5ffc1 --- /dev/null +++ b/core/java/android/app/servertransaction/BaseClientRequest.java @@ -0,0 +1,57 @@ +/* + * 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.app.servertransaction; + +import android.app.ClientTransactionHandler; +import android.os.IBinder; + +/** + * Base interface for individual requests from server to client. + * Each of them can be prepared before scheduling and, eventually, executed. + * @hide + */ +public interface BaseClientRequest extends ObjectPoolItem { + + /** + * Prepare the client request before scheduling. + * An example of this might be informing about pending updates for some values. + * + * @param client Target client handler. + * @param token Target activity token. + */ + default void preExecute(ClientTransactionHandler client, IBinder token) { + } + + /** + * Execute the request. + * @param client Target client handler. + * @param token Target activity token. + * @param pendingActions Container that may have data pending to be used. + */ + void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions); + + /** + * Perform all actions that need to happen after execution, e.g. report the result to server. + * @param client Target client handler. + * @param token Target activity token. + * @param pendingActions Container that may have data pending to be used. + */ + default void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + } +} diff --git a/core/java/com/android/internal/app/IAssistScreenshotReceiver.aidl b/core/java/android/app/servertransaction/ClientTransaction.aidl index a987a166b496..ad8bcbf6eb04 100644 --- a/core/java/com/android/internal/app/IAssistScreenshotReceiver.aidl +++ b/core/java/android/app/servertransaction/ClientTransaction.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 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. @@ -14,11 +14,7 @@ * limitations under the License. */ -package com.android.internal.app; - -import android.graphics.Bitmap; +package android.app.servertransaction; /** @hide */ -oneway interface IAssistScreenshotReceiver { - void send(in Bitmap screenshot); -} +parcelable ClientTransaction;
\ No newline at end of file diff --git a/core/java/android/app/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java new file mode 100644 index 000000000000..764ceede5d20 --- /dev/null +++ b/core/java/android/app/servertransaction/ClientTransaction.java @@ -0,0 +1,232 @@ +/* + * 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.app.servertransaction; + +import android.annotation.Nullable; +import android.app.ClientTransactionHandler; +import android.app.IApplicationThread; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A container that holds a sequence of messages, which may be sent to a client. + * This includes a list of callbacks and a final lifecycle state. + * + * @see com.android.server.am.ClientLifecycleManager + * @see ClientTransactionItem + * @see ActivityLifecycleItem + * @hide + */ +public class ClientTransaction implements Parcelable, ObjectPoolItem { + + /** A list of individual callbacks to a client. */ + private List<ClientTransactionItem> mActivityCallbacks; + + /** + * Final lifecycle state in which the client activity should be after the transaction is + * executed. + */ + private ActivityLifecycleItem mLifecycleStateRequest; + + /** Target client. */ + private IApplicationThread mClient; + + /** Target client activity. Might be null if the entire transaction is targeting an app. */ + private IBinder mActivityToken; + + /** + * Add a message to the end of the sequence of callbacks. + * @param activityCallback A single message that can contain a lifecycle request/callback. + */ + public void addCallback(ClientTransactionItem activityCallback) { + if (mActivityCallbacks == null) { + mActivityCallbacks = new ArrayList<>(); + } + mActivityCallbacks.add(activityCallback); + } + + /** Get the list of callbacks. */ + @Nullable + List<ClientTransactionItem> getCallbacks() { + return mActivityCallbacks; + } + + /** Get the target activity. */ + @Nullable + public IBinder getActivityToken() { + return mActivityToken; + } + + /** Get the target state lifecycle request. */ + ActivityLifecycleItem getLifecycleStateRequest() { + return mLifecycleStateRequest; + } + + /** + * Set the lifecycle state in which the client should be after executing the transaction. + * @param stateRequest A lifecycle request initialized with right parameters. + */ + public void setLifecycleStateRequest(ActivityLifecycleItem stateRequest) { + mLifecycleStateRequest = stateRequest; + } + + /** + * Do what needs to be done while the transaction is being scheduled on the client side. + * @param clientTransactionHandler Handler on the client side that will executed all operations + * requested by transaction items. + */ + public void preExecute(android.app.ClientTransactionHandler clientTransactionHandler) { + if (mActivityCallbacks != null) { + final int size = mActivityCallbacks.size(); + for (int i = 0; i < size; ++i) { + mActivityCallbacks.get(i).preExecute(clientTransactionHandler, mActivityToken); + } + } + if (mLifecycleStateRequest != null) { + mLifecycleStateRequest.preExecute(clientTransactionHandler, mActivityToken); + } + } + + /** + * Schedule the transaction after it was initialized. It will be send to client and all its + * individual parts will be applied in the following sequence: + * 1. The client calls {@link #preExecute(ClientTransactionHandler)}, which triggers all work + * that needs to be done before actually scheduling the transaction for callbacks and + * lifecycle state request. + * 2. The transaction message is scheduled. + * 3. The client calls {@link TransactionExecutor#execute(ClientTransaction)}, which executes + * all callbacks and necessary lifecycle transitions. + */ + public void schedule() throws RemoteException { + mClient.scheduleTransaction(this); + } + + + // ObjectPoolItem implementation + + private ClientTransaction() {} + + /** Obtain an instance initialized with provided params. */ + public static ClientTransaction obtain(IApplicationThread client, IBinder activityToken) { + ClientTransaction instance = ObjectPool.obtain(ClientTransaction.class); + if (instance == null) { + instance = new ClientTransaction(); + } + instance.mClient = client; + instance.mActivityToken = activityToken; + + return instance; + } + + @Override + public void recycle() { + if (mActivityCallbacks != null) { + int size = mActivityCallbacks.size(); + for (int i = 0; i < size; i++) { + mActivityCallbacks.get(i).recycle(); + } + mActivityCallbacks.clear(); + } + if (mLifecycleStateRequest != null) { + mLifecycleStateRequest.recycle(); + mLifecycleStateRequest = null; + } + mClient = null; + mActivityToken = null; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mClient.asBinder()); + final boolean writeActivityToken = mActivityToken != null; + dest.writeBoolean(writeActivityToken); + if (writeActivityToken) { + dest.writeStrongBinder(mActivityToken); + } + dest.writeParcelable(mLifecycleStateRequest, flags); + final boolean writeActivityCallbacks = mActivityCallbacks != null; + dest.writeBoolean(writeActivityCallbacks); + if (writeActivityCallbacks) { + dest.writeParcelableList(mActivityCallbacks, flags); + } + } + + /** Read from Parcel. */ + private ClientTransaction(Parcel in) { + mClient = (IApplicationThread) in.readStrongBinder(); + final boolean readActivityToken = in.readBoolean(); + if (readActivityToken) { + mActivityToken = in.readStrongBinder(); + } + mLifecycleStateRequest = in.readParcelable(getClass().getClassLoader()); + final boolean readActivityCallbacks = in.readBoolean(); + if (readActivityCallbacks) { + mActivityCallbacks = new ArrayList<>(); + in.readParcelableList(mActivityCallbacks, getClass().getClassLoader()); + } + } + + public static final Creator<ClientTransaction> CREATOR = + new Creator<ClientTransaction>() { + public ClientTransaction createFromParcel(Parcel in) { + return new ClientTransaction(in); + } + + public ClientTransaction[] newArray(int size) { + return new ClientTransaction[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ClientTransaction other = (ClientTransaction) o; + return Objects.equals(mActivityCallbacks, other.mActivityCallbacks) + && Objects.equals(mLifecycleStateRequest, other.mLifecycleStateRequest) + && mClient == other.mClient + && mActivityToken == other.mActivityToken; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + Objects.hashCode(mActivityCallbacks); + result = 31 * result + Objects.hashCode(mLifecycleStateRequest); + return result; + } +} diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java new file mode 100644 index 000000000000..6f2cc007ac27 --- /dev/null +++ b/core/java/android/app/servertransaction/ClientTransactionItem.java @@ -0,0 +1,54 @@ +/* + * 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.app.servertransaction; + +import static android.app.servertransaction.ActivityLifecycleItem.LifecycleState; +import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED; + +import android.os.Parcelable; + +/** + * A callback message to a client that can be scheduled and executed. + * Examples of these might be activity configuration change, multi-window mode change, activity + * result delivery etc. + * + * @see ClientTransaction + * @see com.android.server.am.ClientLifecycleManager + * @hide + */ +public abstract class ClientTransactionItem implements BaseClientRequest, Parcelable { + + /** Get the state in which this callback can be executed. */ + @LifecycleState + public int getPreExecutionState() { + return UNDEFINED; + } + + /** Get the state that must follow this callback. */ + @LifecycleState + public int getPostExecutionState() { + return UNDEFINED; + } + + + // Parcelable + + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/app/servertransaction/ConfigurationChangeItem.java b/core/java/android/app/servertransaction/ConfigurationChangeItem.java new file mode 100644 index 000000000000..4ab7251e4d8a --- /dev/null +++ b/core/java/android/app/servertransaction/ConfigurationChangeItem.java @@ -0,0 +1,113 @@ +/* + * 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.app.servertransaction; + +import android.app.ClientTransactionHandler; +import android.content.res.Configuration; +import android.os.IBinder; +import android.os.Parcel; + +import java.util.Objects; + +/** + * App configuration change message. + * @hide + */ +public class ConfigurationChangeItem extends ClientTransactionItem { + + private Configuration mConfiguration; + + @Override + public void preExecute(android.app.ClientTransactionHandler client, IBinder token) { + client.updatePendingConfiguration(mConfiguration); + } + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + client.handleConfigurationChanged(mConfiguration); + } + + + // ObjectPoolItem implementation + + private ConfigurationChangeItem() {} + + /** Obtain an instance initialized with provided params. */ + public static ConfigurationChangeItem obtain(Configuration config) { + ConfigurationChangeItem instance = ObjectPool.obtain(ConfigurationChangeItem.class); + if (instance == null) { + instance = new ConfigurationChangeItem(); + } + instance.mConfiguration = config; + + return instance; + } + + @Override + public void recycle() { + mConfiguration = null; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedObject(mConfiguration, flags); + } + + /** Read from Parcel. */ + private ConfigurationChangeItem(Parcel in) { + mConfiguration = in.readTypedObject(Configuration.CREATOR); + } + + public static final Creator<ConfigurationChangeItem> CREATOR = + new Creator<ConfigurationChangeItem>() { + public ConfigurationChangeItem createFromParcel(Parcel in) { + return new ConfigurationChangeItem(in); + } + + public ConfigurationChangeItem[] newArray(int size) { + return new ConfigurationChangeItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ConfigurationChangeItem other = (ConfigurationChangeItem) o; + return Objects.equals(mConfiguration, other.mConfiguration); + } + + @Override + public int hashCode() { + return mConfiguration.hashCode(); + } + + @Override + public String toString() { + return "ConfigurationChangeItem{config=" + mConfiguration + "}"; + } +} diff --git a/core/java/android/app/servertransaction/DestroyActivityItem.java b/core/java/android/app/servertransaction/DestroyActivityItem.java new file mode 100644 index 000000000000..83da5f33c62a --- /dev/null +++ b/core/java/android/app/servertransaction/DestroyActivityItem.java @@ -0,0 +1,125 @@ +/* + * 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.app.servertransaction; + +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; + +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Trace; + +/** + * Request to destroy an activity. + * @hide + */ +public class DestroyActivityItem extends ActivityLifecycleItem { + + private boolean mFinished; + private int mConfigChanges; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityDestroy"); + client.handleDestroyActivity(token, mFinished, mConfigChanges, + false /* getNonConfigInstance */); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + @Override + public int getTargetState() { + return ON_DESTROY; + } + + + // ObjectPoolItem implementation + + private DestroyActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static DestroyActivityItem obtain(boolean finished, int configChanges) { + DestroyActivityItem instance = ObjectPool.obtain(DestroyActivityItem.class); + if (instance == null) { + instance = new DestroyActivityItem(); + } + instance.mFinished = finished; + instance.mConfigChanges = configChanges; + + return instance; + } + + @Override + public void recycle() { + mFinished = false; + mConfigChanges = 0; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(mFinished); + dest.writeInt(mConfigChanges); + } + + /** Read from Parcel. */ + private DestroyActivityItem(Parcel in) { + mFinished = in.readBoolean(); + mConfigChanges = in.readInt(); + } + + public static final Creator<DestroyActivityItem> CREATOR = + new Creator<DestroyActivityItem>() { + public DestroyActivityItem createFromParcel(Parcel in) { + return new DestroyActivityItem(in); + } + + public DestroyActivityItem[] newArray(int size) { + return new DestroyActivityItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final DestroyActivityItem other = (DestroyActivityItem) o; + return mFinished == other.mFinished && mConfigChanges == other.mConfigChanges; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (mFinished ? 1 : 0); + result = 31 * result + mConfigChanges; + return result; + } + + @Override + public String toString() { + return "DestroyActivityItem{finished=" + mFinished + ",mConfigChanges=" + + mConfigChanges + "}"; + } +} diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java new file mode 100644 index 000000000000..7be82bf9f505 --- /dev/null +++ b/core/java/android/app/servertransaction/LaunchActivityItem.java @@ -0,0 +1,266 @@ +/* + * 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.app.servertransaction; + +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; + +import android.app.ActivityThread.ActivityClientRecord; +import android.app.ClientTransactionHandler; +import android.app.ProfilerInfo; +import android.app.ResultInfo; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; +import android.os.BaseBundle; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.PersistableBundle; +import android.os.Trace; + +import com.android.internal.app.IVoiceInteractor; +import com.android.internal.content.ReferrerIntent; + +import java.util.List; +import java.util.Objects; + +/** + * Request to launch an activity. + * @hide + */ +public class LaunchActivityItem extends ClientTransactionItem { + + private Intent mIntent; + private int mIdent; + private ActivityInfo mInfo; + private Configuration mCurConfig; + private Configuration mOverrideConfig; + private CompatibilityInfo mCompatInfo; + private String mReferrer; + private IVoiceInteractor mVoiceInteractor; + private int mProcState; + private Bundle mState; + private PersistableBundle mPersistentState; + private List<ResultInfo> mPendingResults; + private List<ReferrerIntent> mPendingNewIntents; + private boolean mIsForward; + private ProfilerInfo mProfilerInfo; + + @Override + public void preExecute(ClientTransactionHandler client, IBinder token) { + client.updateProcessState(mProcState, false); + client.updatePendingConfiguration(mCurConfig); + } + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); + ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo, + mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState, + mPendingResults, mPendingNewIntents, mIsForward, + mProfilerInfo, client); + client.handleLaunchActivity(r, pendingActions); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + + // ObjectPoolItem implementation + + private LaunchActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static LaunchActivityItem obtain(Intent intent, int ident, ActivityInfo info, + Configuration curConfig, Configuration overrideConfig, CompatibilityInfo compatInfo, + String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, + PersistableBundle persistentState, List<ResultInfo> pendingResults, + List<ReferrerIntent> pendingNewIntents, boolean isForward, ProfilerInfo profilerInfo) { + LaunchActivityItem instance = ObjectPool.obtain(LaunchActivityItem.class); + if (instance == null) { + instance = new LaunchActivityItem(); + } + setValues(instance, intent, ident, info, curConfig, overrideConfig, compatInfo, referrer, + voiceInteractor, procState, state, persistentState, pendingResults, + pendingNewIntents, isForward, profilerInfo); + + return instance; + } + + @Override + public void recycle() { + setValues(this, null, 0, null, null, null, null, null, null, 0, null, null, null, null, + false, null); + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write from Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedObject(mIntent, flags); + dest.writeInt(mIdent); + dest.writeTypedObject(mInfo, flags); + dest.writeTypedObject(mCurConfig, flags); + dest.writeTypedObject(mOverrideConfig, flags); + dest.writeTypedObject(mCompatInfo, flags); + dest.writeString(mReferrer); + dest.writeStrongInterface(mVoiceInteractor); + dest.writeInt(mProcState); + dest.writeBundle(mState); + dest.writePersistableBundle(mPersistentState); + dest.writeTypedList(mPendingResults, flags); + dest.writeTypedList(mPendingNewIntents, flags); + dest.writeBoolean(mIsForward); + dest.writeTypedObject(mProfilerInfo, flags); + } + + /** Read from Parcel. */ + private LaunchActivityItem(Parcel in) { + setValues(this, in.readTypedObject(Intent.CREATOR), in.readInt(), + in.readTypedObject(ActivityInfo.CREATOR), in.readTypedObject(Configuration.CREATOR), + in.readTypedObject(Configuration.CREATOR), + in.readTypedObject(CompatibilityInfo.CREATOR), in.readString(), + IVoiceInteractor.Stub.asInterface(in.readStrongBinder()), in.readInt(), + in.readBundle(getClass().getClassLoader()), + in.readPersistableBundle(getClass().getClassLoader()), + in.createTypedArrayList(ResultInfo.CREATOR), + in.createTypedArrayList(ReferrerIntent.CREATOR), in.readBoolean(), + in.readTypedObject(ProfilerInfo.CREATOR)); + } + + public static final Creator<LaunchActivityItem> CREATOR = + new Creator<LaunchActivityItem>() { + public LaunchActivityItem createFromParcel(Parcel in) { + return new LaunchActivityItem(in); + } + + public LaunchActivityItem[] newArray(int size) { + return new LaunchActivityItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final LaunchActivityItem other = (LaunchActivityItem) o; + final boolean intentsEqual = (mIntent == null && other.mIntent == null) + || (mIntent != null && mIntent.filterEquals(other.mIntent)); + return intentsEqual && mIdent == other.mIdent + && activityInfoEqual(other.mInfo) && Objects.equals(mCurConfig, other.mCurConfig) + && Objects.equals(mOverrideConfig, other.mOverrideConfig) + && Objects.equals(mCompatInfo, other.mCompatInfo) + && Objects.equals(mReferrer, other.mReferrer) + && mProcState == other.mProcState && areBundlesEqual(mState, other.mState) + && areBundlesEqual(mPersistentState, other.mPersistentState) + && Objects.equals(mPendingResults, other.mPendingResults) + && Objects.equals(mPendingNewIntents, other.mPendingNewIntents) + && mIsForward == other.mIsForward + && Objects.equals(mProfilerInfo, other.mProfilerInfo); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + mIntent.filterHashCode(); + result = 31 * result + mIdent; + result = 31 * result + Objects.hashCode(mCurConfig); + result = 31 * result + Objects.hashCode(mOverrideConfig); + result = 31 * result + Objects.hashCode(mCompatInfo); + result = 31 * result + Objects.hashCode(mReferrer); + result = 31 * result + Objects.hashCode(mProcState); + result = 31 * result + (mState != null ? mState.size() : 0); + result = 31 * result + (mPersistentState != null ? mPersistentState.size() : 0); + result = 31 * result + Objects.hashCode(mPendingResults); + result = 31 * result + Objects.hashCode(mPendingNewIntents); + result = 31 * result + (mIsForward ? 1 : 0); + result = 31 * result + Objects.hashCode(mProfilerInfo); + return result; + } + + private boolean activityInfoEqual(ActivityInfo other) { + if (mInfo == null) { + return other == null; + } + return other != null && mInfo.flags == other.flags + && mInfo.maxAspectRatio == other.maxAspectRatio + && Objects.equals(mInfo.launchToken, other.launchToken) + && Objects.equals(mInfo.getComponentName(), other.getComponentName()); + } + + private static boolean areBundlesEqual(BaseBundle extras, BaseBundle newExtras) { + if (extras == null || newExtras == null) { + return extras == newExtras; + } + + if (extras.size() != newExtras.size()) { + return false; + } + + for (String key : extras.keySet()) { + if (key != null) { + final Object value = extras.get(key); + final Object newValue = newExtras.get(key); + if (!Objects.equals(value, newValue)) { + return false; + } + } + } + return true; + } + + @Override + public String toString() { + return "LaunchActivityItem{intent=" + mIntent + ",ident=" + mIdent + ",info=" + mInfo + + ",curConfig=" + mCurConfig + ",overrideConfig=" + mOverrideConfig + + ",referrer=" + mReferrer + ",procState=" + mProcState + ",state=" + mState + + ",persistentState=" + mPersistentState + ",pendingResults=" + mPendingResults + + ",pendingNewIntents=" + mPendingNewIntents + ",profilerInfo=" + mProfilerInfo + + "}"; + } + + // Using the same method to set and clear values to make sure we don't forget anything + private static void setValues(LaunchActivityItem instance, Intent intent, int ident, + ActivityInfo info, Configuration curConfig, Configuration overrideConfig, + CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor, + int procState, Bundle state, PersistableBundle persistentState, + List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, + boolean isForward, ProfilerInfo profilerInfo) { + instance.mIntent = intent; + instance.mIdent = ident; + instance.mInfo = info; + instance.mCurConfig = curConfig; + instance.mOverrideConfig = overrideConfig; + instance.mCompatInfo = compatInfo; + instance.mReferrer = referrer; + instance.mVoiceInteractor = voiceInteractor; + instance.mProcState = procState; + instance.mState = state; + instance.mPersistentState = persistentState; + instance.mPendingResults = pendingResults; + instance.mPendingNewIntents = pendingNewIntents; + instance.mIsForward = isForward; + instance.mProfilerInfo = profilerInfo; + } +} diff --git a/core/java/android/app/servertransaction/MoveToDisplayItem.java b/core/java/android/app/servertransaction/MoveToDisplayItem.java new file mode 100644 index 000000000000..b3dddfb37eaa --- /dev/null +++ b/core/java/android/app/servertransaction/MoveToDisplayItem.java @@ -0,0 +1,122 @@ +/* + * 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.app.servertransaction; + +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; + +import android.app.ClientTransactionHandler; +import android.content.res.Configuration; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Trace; + +import java.util.Objects; + +/** + * Activity move to a different display message. + * @hide + */ +public class MoveToDisplayItem extends ClientTransactionItem { + + private int mTargetDisplayId; + private Configuration mConfiguration; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityMovedToDisplay"); + client.handleActivityConfigurationChanged(token, mConfiguration, mTargetDisplayId); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + + // ObjectPoolItem implementation + + private MoveToDisplayItem() {} + + /** Obtain an instance initialized with provided params. */ + public static MoveToDisplayItem obtain(int targetDisplayId, Configuration configuration) { + MoveToDisplayItem instance = ObjectPool.obtain(MoveToDisplayItem.class); + if (instance == null) { + instance = new MoveToDisplayItem(); + } + instance.mTargetDisplayId = targetDisplayId; + instance.mConfiguration = configuration; + + return instance; + } + + @Override + public void recycle() { + mTargetDisplayId = 0; + mConfiguration = null; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mTargetDisplayId); + dest.writeTypedObject(mConfiguration, flags); + } + + /** Read from Parcel. */ + private MoveToDisplayItem(Parcel in) { + mTargetDisplayId = in.readInt(); + mConfiguration = in.readTypedObject(Configuration.CREATOR); + } + + public static final Creator<MoveToDisplayItem> CREATOR = new Creator<MoveToDisplayItem>() { + public MoveToDisplayItem createFromParcel(Parcel in) { + return new MoveToDisplayItem(in); + } + + public MoveToDisplayItem[] newArray(int size) { + return new MoveToDisplayItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final MoveToDisplayItem other = (MoveToDisplayItem) o; + return mTargetDisplayId == other.mTargetDisplayId + && Objects.equals(mConfiguration, other.mConfiguration); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + mTargetDisplayId; + result = 31 * result + mConfiguration.hashCode(); + return result; + } + + @Override + public String toString() { + return "MoveToDisplayItem{targetDisplayId=" + mTargetDisplayId + + ",configuration=" + mConfiguration + "}"; + } +} diff --git a/core/java/android/app/servertransaction/MultiWindowModeChangeItem.java b/core/java/android/app/servertransaction/MultiWindowModeChangeItem.java new file mode 100644 index 000000000000..c3022d6facc7 --- /dev/null +++ b/core/java/android/app/servertransaction/MultiWindowModeChangeItem.java @@ -0,0 +1,121 @@ +/* + * 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.app.servertransaction; + +import android.app.ClientTransactionHandler; +import android.content.res.Configuration; +import android.os.IBinder; +import android.os.Parcel; + +import java.util.Objects; + +/** + * Multi-window mode change message. + * @hide + */ +// TODO(lifecycler): Remove the use of this and just use the configuration change message to +// communicate multi-window mode change with WindowConfiguration. +public class MultiWindowModeChangeItem extends ClientTransactionItem { + + private boolean mIsInMultiWindowMode; + private Configuration mOverrideConfig; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + client.handleMultiWindowModeChanged(token, mIsInMultiWindowMode, mOverrideConfig); + } + + + // ObjectPoolItem implementation + + private MultiWindowModeChangeItem() {} + + /** Obtain an instance initialized with provided params. */ + public static MultiWindowModeChangeItem obtain(boolean isInMultiWindowMode, + Configuration overrideConfig) { + MultiWindowModeChangeItem instance = ObjectPool.obtain(MultiWindowModeChangeItem.class); + if (instance == null) { + instance = new MultiWindowModeChangeItem(); + } + instance.mIsInMultiWindowMode = isInMultiWindowMode; + instance.mOverrideConfig = overrideConfig; + + return instance; + } + + @Override + public void recycle() { + mIsInMultiWindowMode = false; + mOverrideConfig = null; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(mIsInMultiWindowMode); + dest.writeTypedObject(mOverrideConfig, flags); + } + + /** Read from Parcel. */ + private MultiWindowModeChangeItem(Parcel in) { + mIsInMultiWindowMode = in.readBoolean(); + mOverrideConfig = in.readTypedObject(Configuration.CREATOR); + } + + public static final Creator<MultiWindowModeChangeItem> CREATOR = + new Creator<MultiWindowModeChangeItem>() { + public MultiWindowModeChangeItem createFromParcel(Parcel in) { + return new MultiWindowModeChangeItem(in); + } + + public MultiWindowModeChangeItem[] newArray(int size) { + return new MultiWindowModeChangeItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final MultiWindowModeChangeItem other = (MultiWindowModeChangeItem) o; + return mIsInMultiWindowMode == other.mIsInMultiWindowMode + && Objects.equals(mOverrideConfig, other.mOverrideConfig); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (mIsInMultiWindowMode ? 1 : 0); + result = 31 * result + mOverrideConfig.hashCode(); + return result; + } + + @Override + public String toString() { + return "MultiWindowModeChangeItem{isInMultiWindowMode=" + mIsInMultiWindowMode + + ",overrideConfig=" + mOverrideConfig + "}"; + } +} diff --git a/core/java/android/app/servertransaction/NewIntentItem.java b/core/java/android/app/servertransaction/NewIntentItem.java new file mode 100644 index 000000000000..7dfde73c0534 --- /dev/null +++ b/core/java/android/app/servertransaction/NewIntentItem.java @@ -0,0 +1,133 @@ +/* + * 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.app.servertransaction; + +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Trace; + +import com.android.internal.content.ReferrerIntent; + +import java.util.List; +import java.util.Objects; + +/** + * New intent message. + * @hide + */ +public class NewIntentItem extends ClientTransactionItem { + + private List<ReferrerIntent> mIntents; + private boolean mPause; + + // TODO(lifecycler): Switch new intent handling to this scheme. + /*@Override + public int getPreExecutionState() { + return ON_PAUSE; + } + + @Override + public int getPostExecutionState() { + return ON_RESUME; + }*/ + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityNewIntent"); + client.handleNewIntent(token, mIntents, mPause); + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + } + + + // ObjectPoolItem implementation + + private NewIntentItem() {} + + /** Obtain an instance initialized with provided params. */ + public static NewIntentItem obtain(List<ReferrerIntent> intents, boolean pause) { + NewIntentItem instance = ObjectPool.obtain(NewIntentItem.class); + if (instance == null) { + instance = new NewIntentItem(); + } + instance.mIntents = intents; + instance.mPause = pause; + + return instance; + } + + @Override + public void recycle() { + mIntents = null; + mPause = false; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(mPause); + dest.writeTypedList(mIntents, flags); + } + + /** Read from Parcel. */ + private NewIntentItem(Parcel in) { + mPause = in.readBoolean(); + mIntents = in.createTypedArrayList(ReferrerIntent.CREATOR); + } + + public static final Parcelable.Creator<NewIntentItem> CREATOR = + new Parcelable.Creator<NewIntentItem>() { + public NewIntentItem createFromParcel(Parcel in) { + return new NewIntentItem(in); + } + + public NewIntentItem[] newArray(int size) { + return new NewIntentItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final NewIntentItem other = (NewIntentItem) o; + return mPause == other.mPause && Objects.equals(mIntents, other.mIntents); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (mPause ? 1 : 0); + result = 31 * result + mIntents.hashCode(); + return result; + } + + @Override + public String toString() { + return "NewIntentItem{pause=" + mPause + ",intents=" + mIntents + "}"; + } +} diff --git a/core/java/android/app/servertransaction/ObjectPool.java b/core/java/android/app/servertransaction/ObjectPool.java new file mode 100644 index 000000000000..98121253f486 --- /dev/null +++ b/core/java/android/app/servertransaction/ObjectPool.java @@ -0,0 +1,73 @@ +/* + * 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.app.servertransaction; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +/** + * An object pool that can provide reused objects if available. + * @hide + */ +class ObjectPool { + + private static final Object sPoolSync = new Object(); + private static final Map<Class, LinkedList<? extends ObjectPoolItem>> sPoolMap = + new HashMap<>(); + + private static final int MAX_POOL_SIZE = 50; + + /** + * Obtain an instance of a specific class from the pool + * @param itemClass The class of the object we're looking for. + * @return An instance or null if there is none. + */ + public static <T extends ObjectPoolItem> T obtain(Class<T> itemClass) { + synchronized (sPoolSync) { + @SuppressWarnings("unchecked") + LinkedList<T> itemPool = (LinkedList<T>) sPoolMap.get(itemClass); + if (itemPool != null && !itemPool.isEmpty()) { + return itemPool.poll(); + } + return null; + } + } + + /** + * Recycle the object to the pool. The object should be properly cleared before this. + * @param item The object to recycle. + * @see ObjectPoolItem#recycle() + */ + public static <T extends ObjectPoolItem> void recycle(T item) { + synchronized (sPoolSync) { + @SuppressWarnings("unchecked") + LinkedList<T> itemPool = (LinkedList<T>) sPoolMap.get(item.getClass()); + if (itemPool == null) { + itemPool = new LinkedList<>(); + sPoolMap.put(item.getClass(), itemPool); + } + if (itemPool.contains(item)) { + throw new IllegalStateException("Trying to recycle already recycled item"); + } + + if (itemPool.size() < MAX_POOL_SIZE) { + itemPool.add(item); + } + } + } +} diff --git a/core/java/android/app/servertransaction/ObjectPoolItem.java b/core/java/android/app/servertransaction/ObjectPoolItem.java new file mode 100644 index 000000000000..17bd4f30640f --- /dev/null +++ b/core/java/android/app/servertransaction/ObjectPoolItem.java @@ -0,0 +1,29 @@ +/* + * 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.app.servertransaction; + +/** + * Base interface for all lifecycle items that can be put in object pool. + * @hide + */ +public interface ObjectPoolItem { + /** + * Clear the contents of the item and putting it to a pool. The implementation should call + * {@link ObjectPool#recycle(ObjectPoolItem)} passing itself. + */ + void recycle(); +} diff --git a/core/java/android/app/servertransaction/PauseActivityItem.java b/core/java/android/app/servertransaction/PauseActivityItem.java new file mode 100644 index 000000000000..880fef73c6f2 --- /dev/null +++ b/core/java/android/app/servertransaction/PauseActivityItem.java @@ -0,0 +1,170 @@ +/* + * 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.app.servertransaction; + +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; + +import android.app.ActivityManager; +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.Trace; + +/** + * Request to move an activity to paused state. + * @hide + */ +public class PauseActivityItem extends ActivityLifecycleItem { + + private static final String TAG = "PauseActivityItem"; + + private boolean mFinished; + private boolean mUserLeaving; + private int mConfigChanges; + private boolean mDontReport; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); + client.handlePauseActivity(token, mFinished, mUserLeaving, mConfigChanges, mDontReport, + pendingActions); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + @Override + public int getTargetState() { + return ON_PAUSE; + } + + @Override + public void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + if (mDontReport) { + return; + } + try { + // TODO(lifecycler): Use interface callback instead of AMS. + ActivityManager.getService().activityPaused(token); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + + // ObjectPoolItem implementation + + private PauseActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static PauseActivityItem obtain(boolean finished, boolean userLeaving, int configChanges, + boolean dontReport) { + PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class); + if (instance == null) { + instance = new PauseActivityItem(); + } + instance.mFinished = finished; + instance.mUserLeaving = userLeaving; + instance.mConfigChanges = configChanges; + instance.mDontReport = dontReport; + + return instance; + } + + /** Obtain an instance initialized with default params. */ + public static PauseActivityItem obtain() { + PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class); + if (instance == null) { + instance = new PauseActivityItem(); + } + instance.mFinished = false; + instance.mUserLeaving = false; + instance.mConfigChanges = 0; + instance.mDontReport = true; + + return instance; + } + + @Override + public void recycle() { + mFinished = false; + mUserLeaving = false; + mConfigChanges = 0; + mDontReport = false; + ObjectPool.recycle(this); + } + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(mFinished); + dest.writeBoolean(mUserLeaving); + dest.writeInt(mConfigChanges); + dest.writeBoolean(mDontReport); + } + + /** Read from Parcel. */ + private PauseActivityItem(Parcel in) { + mFinished = in.readBoolean(); + mUserLeaving = in.readBoolean(); + mConfigChanges = in.readInt(); + mDontReport = in.readBoolean(); + } + + public static final Creator<PauseActivityItem> CREATOR = + new Creator<PauseActivityItem>() { + public PauseActivityItem createFromParcel(Parcel in) { + return new PauseActivityItem(in); + } + + public PauseActivityItem[] newArray(int size) { + return new PauseActivityItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PauseActivityItem other = (PauseActivityItem) o; + return mFinished == other.mFinished && mUserLeaving == other.mUserLeaving + && mConfigChanges == other.mConfigChanges && mDontReport == other.mDontReport; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (mFinished ? 1 : 0); + result = 31 * result + (mUserLeaving ? 1 : 0); + result = 31 * result + mConfigChanges; + result = 31 * result + (mDontReport ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "PauseActivityItem{finished=" + mFinished + ",userLeaving=" + mUserLeaving + + ",configChanges=" + mConfigChanges + ",dontReport=" + mDontReport + "}"; + } +} diff --git a/core/java/android/app/servertransaction/PendingTransactionActions.java b/core/java/android/app/servertransaction/PendingTransactionActions.java new file mode 100644 index 000000000000..073d28cfa27f --- /dev/null +++ b/core/java/android/app/servertransaction/PendingTransactionActions.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.app.servertransaction; + +import static android.app.ActivityThread.DEBUG_MEMORY_TRIM; + +import android.app.ActivityManager; +import android.app.ActivityThread.ActivityClientRecord; +import android.os.Build; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.os.TransactionTooLargeException; +import android.util.Log; +import android.util.LogWriter; +import android.util.Slog; + +import com.android.internal.util.IndentingPrintWriter; + +/** + * Container that has data pending to be used at later stages of + * {@link android.app.servertransaction.ClientTransaction}. + * An instance of this class is passed to each individual transaction item, so it can use some + * information from previous steps or add some for the following steps. + * + * @hide + */ +public class PendingTransactionActions { + private boolean mRestoreInstanceState; + private boolean mCallOnPostCreate; + private Bundle mOldState; + private StopInfo mStopInfo; + + public PendingTransactionActions() { + clear(); + } + + /** Reset the state of the instance to default, non-initialized values. */ + public void clear() { + mRestoreInstanceState = false; + mCallOnPostCreate = false; + mOldState = null; + mStopInfo = null; + } + + /** Getter */ + public boolean shouldRestoreInstanceState() { + return mRestoreInstanceState; + } + + public void setRestoreInstanceState(boolean restoreInstanceState) { + mRestoreInstanceState = restoreInstanceState; + } + + /** Getter */ + public boolean shouldCallOnPostCreate() { + return mCallOnPostCreate; + } + + public void setCallOnPostCreate(boolean callOnPostCreate) { + mCallOnPostCreate = callOnPostCreate; + } + + public Bundle getOldState() { + return mOldState; + } + + public void setOldState(Bundle oldState) { + mOldState = oldState; + } + + public StopInfo getStopInfo() { + return mStopInfo; + } + + public void setStopInfo(StopInfo stopInfo) { + mStopInfo = stopInfo; + } + + /** Reports to server about activity stop. */ + public static class StopInfo implements Runnable { + private static final String TAG = "ActivityStopInfo"; + + private ActivityClientRecord mActivity; + private Bundle mState; + private PersistableBundle mPersistentState; + private CharSequence mDescription; + + public void setActivity(ActivityClientRecord activity) { + mActivity = activity; + } + + public void setState(Bundle state) { + mState = state; + } + + public void setPersistentState(PersistableBundle persistentState) { + mPersistentState = persistentState; + } + + public void setDescription(CharSequence description) { + mDescription = description; + } + + @Override + public void run() { + // Tell activity manager we have been stopped. + try { + if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + mActivity); + // TODO(lifecycler): Use interface callback instead of AMS. + ActivityManager.getService().activityStopped( + mActivity.token, mState, mPersistentState, mDescription); + } catch (RemoteException ex) { + // Dump statistics about bundle to help developers debug + final LogWriter writer = new LogWriter(Log.WARN, TAG); + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + pw.println("Bundle stats:"); + Bundle.dumpStats(pw, mState); + pw.println("PersistableBundle stats:"); + Bundle.dumpStats(pw, mPersistentState); + + if (ex instanceof TransactionTooLargeException + && mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) { + Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex); + return; + } + throw ex.rethrowFromSystemServer(); + } + } + } +} diff --git a/core/java/android/app/servertransaction/PipModeChangeItem.java b/core/java/android/app/servertransaction/PipModeChangeItem.java new file mode 100644 index 000000000000..b999cd7e295e --- /dev/null +++ b/core/java/android/app/servertransaction/PipModeChangeItem.java @@ -0,0 +1,119 @@ +/* + * 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.app.servertransaction; + +import android.app.ClientTransactionHandler; +import android.content.res.Configuration; +import android.os.IBinder; +import android.os.Parcel; + +import java.util.Objects; + +/** + * Picture in picture mode change message. + * @hide + */ +// TODO(lifecycler): Remove the use of this and just use the configuration change message to +// communicate multi-window mode change with WindowConfiguration. +public class PipModeChangeItem extends ClientTransactionItem { + + private boolean mIsInPipMode; + private Configuration mOverrideConfig; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + client.handlePictureInPictureModeChanged(token, mIsInPipMode, mOverrideConfig); + } + + + // ObjectPoolItem implementation + + private PipModeChangeItem() {} + + /** Obtain an instance initialized with provided params. */ + public static PipModeChangeItem obtain(boolean isInPipMode, Configuration overrideConfig) { + PipModeChangeItem instance = ObjectPool.obtain(PipModeChangeItem.class); + if (instance == null) { + instance = new PipModeChangeItem(); + } + instance.mIsInPipMode = isInPipMode; + instance.mOverrideConfig = overrideConfig; + + return instance; + } + + @Override + public void recycle() { + mIsInPipMode = false; + mOverrideConfig = null; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(mIsInPipMode); + dest.writeTypedObject(mOverrideConfig, flags); + } + + /** Read from Parcel. */ + private PipModeChangeItem(Parcel in) { + mIsInPipMode = in.readBoolean(); + mOverrideConfig = in.readTypedObject(Configuration.CREATOR); + } + + public static final Creator<PipModeChangeItem> CREATOR = + new Creator<PipModeChangeItem>() { + public PipModeChangeItem createFromParcel(Parcel in) { + return new PipModeChangeItem(in); + } + + public PipModeChangeItem[] newArray(int size) { + return new PipModeChangeItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PipModeChangeItem other = (PipModeChangeItem) o; + return mIsInPipMode == other.mIsInPipMode + && Objects.equals(mOverrideConfig, other.mOverrideConfig); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (mIsInPipMode ? 1 : 0); + result = 31 * result + mOverrideConfig.hashCode(); + return result; + } + + @Override + public String toString() { + return "PipModeChangeItem{isInPipMode=" + mIsInPipMode + + ",overrideConfig=" + mOverrideConfig + "}"; + } +} diff --git a/core/java/android/app/servertransaction/ResumeActivityItem.java b/core/java/android/app/servertransaction/ResumeActivityItem.java new file mode 100644 index 000000000000..9249c6e8ed54 --- /dev/null +++ b/core/java/android/app/servertransaction/ResumeActivityItem.java @@ -0,0 +1,166 @@ +/* + * 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.app.servertransaction; + +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; + +import android.app.ActivityManager; +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.Trace; + +/** + * Request to move an activity to resumed state. + * @hide + */ +public class ResumeActivityItem extends ActivityLifecycleItem { + + private static final String TAG = "ResumeActivityItem"; + + private int mProcState; + private boolean mUpdateProcState; + private boolean mIsForward; + + @Override + public void preExecute(ClientTransactionHandler client, IBinder token) { + if (mUpdateProcState) { + client.updateProcessState(mProcState, false); + } + } + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume"); + client.handleResumeActivity(token, true /* clearHide */, mIsForward, "RESUME_ACTIVITY"); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + @Override + public void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + try { + // TODO(lifecycler): Use interface callback instead of AMS. + ActivityManager.getService().activityResumed(token); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + @Override + public int getTargetState() { + return ON_RESUME; + } + + + // ObjectPoolItem implementation + + private ResumeActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static ResumeActivityItem obtain(int procState, boolean isForward) { + ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class); + if (instance == null) { + instance = new ResumeActivityItem(); + } + instance.mProcState = procState; + instance.mUpdateProcState = true; + instance.mIsForward = isForward; + + return instance; + } + + /** Obtain an instance initialized with provided params. */ + public static ResumeActivityItem obtain(boolean isForward) { + ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class); + if (instance == null) { + instance = new ResumeActivityItem(); + } + instance.mProcState = ActivityManager.PROCESS_STATE_UNKNOWN; + instance.mUpdateProcState = false; + instance.mIsForward = isForward; + + return instance; + } + + @Override + public void recycle() { + mProcState = ActivityManager.PROCESS_STATE_UNKNOWN; + mUpdateProcState = false; + mIsForward = false; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mProcState); + dest.writeBoolean(mUpdateProcState); + dest.writeBoolean(mIsForward); + } + + /** Read from Parcel. */ + private ResumeActivityItem(Parcel in) { + mProcState = in.readInt(); + mUpdateProcState = in.readBoolean(); + mIsForward = in.readBoolean(); + } + + public static final Creator<ResumeActivityItem> CREATOR = + new Creator<ResumeActivityItem>() { + public ResumeActivityItem createFromParcel(Parcel in) { + return new ResumeActivityItem(in); + } + + public ResumeActivityItem[] newArray(int size) { + return new ResumeActivityItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ResumeActivityItem other = (ResumeActivityItem) o; + return mProcState == other.mProcState && mUpdateProcState == other.mUpdateProcState + && mIsForward == other.mIsForward; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + mProcState; + result = 31 * result + (mUpdateProcState ? 1 : 0); + result = 31 * result + (mIsForward ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "ResumeActivityItem{procState=" + mProcState + + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward + "}"; + } +} diff --git a/core/java/android/app/servertransaction/StopActivityItem.java b/core/java/android/app/servertransaction/StopActivityItem.java new file mode 100644 index 000000000000..5c5c3041344f --- /dev/null +++ b/core/java/android/app/servertransaction/StopActivityItem.java @@ -0,0 +1,132 @@ +/* + * 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.app.servertransaction; + +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; + +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Trace; + +/** + * Request to move an activity to stopped state. + * @hide + */ +public class StopActivityItem extends ActivityLifecycleItem { + + private static final String TAG = "StopActivityItem"; + + private boolean mShowWindow; + private int mConfigChanges; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStop"); + client.handleStopActivity(token, mShowWindow, mConfigChanges, pendingActions); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + @Override + public void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + client.reportStop(pendingActions); + } + + @Override + public int getTargetState() { + return ON_STOP; + } + + + // ObjectPoolItem implementation + + private StopActivityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static StopActivityItem obtain(boolean showWindow, int configChanges) { + StopActivityItem instance = ObjectPool.obtain(StopActivityItem.class); + if (instance == null) { + instance = new StopActivityItem(); + } + instance.mShowWindow = showWindow; + instance.mConfigChanges = configChanges; + + return instance; + } + + @Override + public void recycle() { + mShowWindow = false; + mConfigChanges = 0; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(mShowWindow); + dest.writeInt(mConfigChanges); + } + + /** Read from Parcel. */ + private StopActivityItem(Parcel in) { + mShowWindow = in.readBoolean(); + mConfigChanges = in.readInt(); + } + + public static final Creator<StopActivityItem> CREATOR = + new Creator<StopActivityItem>() { + public StopActivityItem createFromParcel(Parcel in) { + return new StopActivityItem(in); + } + + public StopActivityItem[] newArray(int size) { + return new StopActivityItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final StopActivityItem other = (StopActivityItem) o; + return mShowWindow == other.mShowWindow && mConfigChanges == other.mConfigChanges; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (mShowWindow ? 1 : 0); + result = 31 * result + mConfigChanges; + return result; + } + + @Override + public String toString() { + return "StopActivityItem{showWindow=" + mShowWindow + ",configChanges=" + mConfigChanges + + "}"; + } +} diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java new file mode 100644 index 000000000000..5b0ea6b1f9d4 --- /dev/null +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -0,0 +1,248 @@ +/* + * 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.app.servertransaction; + +import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY; +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_RESTART; +import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME; +import static android.app.servertransaction.ActivityLifecycleItem.ON_START; +import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; +import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED; + +import android.app.ActivityThread.ActivityClientRecord; +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.util.IntArray; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; + +/** + * Class that manages transaction execution in the correct order. + * @hide + */ +public class TransactionExecutor { + + private static final boolean DEBUG_RESOLVER = false; + private static final String TAG = "TransactionExecutor"; + + private ClientTransactionHandler mTransactionHandler; + private PendingTransactionActions mPendingActions = new PendingTransactionActions(); + + // Temp holder for lifecycle path. + // No direct transition between two states should take more than one complete cycle of 6 states. + @ActivityLifecycleItem.LifecycleState + private IntArray mLifecycleSequence = new IntArray(6); + + /** Initialize an instance with transaction handler, that will execute all requested actions. */ + public TransactionExecutor(ClientTransactionHandler clientTransactionHandler) { + mTransactionHandler = clientTransactionHandler; + } + + /** + * Resolve transaction. + * First all callbacks will be executed in the order they appear in the list. If a callback + * requires a certain pre- or post-execution state, the client will be transitioned accordingly. + * Then the client will cycle to the final lifecycle state if provided. Otherwise, it will + * either remain in the initial state, or last state needed by a callback. + */ + public void execute(ClientTransaction transaction) { + final IBinder token = transaction.getActivityToken(); + log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token); + + executeCallbacks(transaction); + + executeLifecycleState(transaction); + mPendingActions.clear(); + log("End resolving transaction"); + } + + /** Cycle through all states requested by callbacks and execute them at proper times. */ + @VisibleForTesting + public void executeCallbacks(ClientTransaction transaction) { + final List<ClientTransactionItem> callbacks = transaction.getCallbacks(); + if (callbacks == null) { + // No callbacks to execute, return early. + return; + } + log("Resolving callbacks"); + + final IBinder token = transaction.getActivityToken(); + ActivityClientRecord r = mTransactionHandler.getActivityClient(token); + final int size = callbacks.size(); + for (int i = 0; i < size; ++i) { + final ClientTransactionItem item = callbacks.get(i); + log("Resolving callback: " + item); + final int preExecutionState = item.getPreExecutionState(); + if (preExecutionState != UNDEFINED) { + cycleToPath(r, preExecutionState); + } + + item.execute(mTransactionHandler, token, mPendingActions); + item.postExecute(mTransactionHandler, token, mPendingActions); + if (r == null) { + // Launch activity request will create an activity record. + r = mTransactionHandler.getActivityClient(token); + } + + final int postExecutionState = item.getPostExecutionState(); + if (postExecutionState != UNDEFINED) { + cycleToPath(r, postExecutionState); + } + } + } + + /** Transition to the final state if requested by the transaction. */ + private void executeLifecycleState(ClientTransaction transaction) { + final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest(); + if (lifecycleItem == null) { + // No lifecycle request, return early. + return; + } + log("Resolving lifecycle state: " + lifecycleItem); + + final IBinder token = transaction.getActivityToken(); + final ActivityClientRecord r = mTransactionHandler.getActivityClient(token); + + // Cycle to the state right before the final requested state. + cycleToPath(r, lifecycleItem.getTargetState(), true /* excludeLastState */); + + // Execute the final transition with proper parameters. + lifecycleItem.execute(mTransactionHandler, token, mPendingActions); + lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions); + } + + /** Transition the client between states. */ + @VisibleForTesting + public void cycleToPath(ActivityClientRecord r, int finish) { + cycleToPath(r, finish, false /* excludeLastState */); + } + + /** + * Transition the client between states with an option not to perform the last hop in the + * sequence. This is used when resolving lifecycle state request, when the last transition must + * be performed with some specific parameters. + */ + private void cycleToPath(ActivityClientRecord r, int finish, + boolean excludeLastState) { + final int start = r.getLifecycleState(); + log("Cycle from: " + start + " to: " + finish + " excludeLastState:" + excludeLastState); + initLifecyclePath(start, finish, excludeLastState); + performLifecycleSequence(r); + } + + /** Transition the client through previously initialized state sequence. */ + private void performLifecycleSequence(ActivityClientRecord r) { + final int size = mLifecycleSequence.size(); + for (int i = 0, state; i < size; i++) { + state = mLifecycleSequence.get(i); + log("Transitioning to state: " + state); + switch (state) { + case ON_CREATE: + mTransactionHandler.handleLaunchActivity(r, mPendingActions); + break; + case ON_START: + mTransactionHandler.handleStartActivity(r, mPendingActions); + break; + case ON_RESUME: + mTransactionHandler.handleResumeActivity(r.token, false /* clearHide */, + r.isForward, "LIFECYCLER_RESUME_ACTIVITY"); + break; + case ON_PAUSE: + mTransactionHandler.handlePauseActivity(r.token, false /* finished */, + false /* userLeaving */, 0 /* configChanges */, + true /* dontReport */, mPendingActions); + break; + case ON_STOP: + mTransactionHandler.handleStopActivity(r.token, false /* show */, + 0 /* configChanges */, mPendingActions); + break; + case ON_DESTROY: + mTransactionHandler.handleDestroyActivity(r.token, false /* finishing */, + 0 /* configChanges */, false /* getNonConfigInstance */); + break; + case ON_RESTART: + mTransactionHandler.performRestartActivity(r.token, false /* start */); + break; + default: + throw new IllegalArgumentException("Unexpected lifecycle state: " + state); + } + } + } + + /** + * Calculate the path through main lifecycle states for an activity and fill + * @link #mLifecycleSequence} with values starting with the state that follows the initial + * state. + */ + public void initLifecyclePath(int start, int finish, boolean excludeLastState) { + mLifecycleSequence.clear(); + if (finish >= start) { + // just go there + for (int i = start + 1; i <= finish; i++) { + mLifecycleSequence.add(i); + } + } else { // finish < start, can't just cycle down + if (start == ON_PAUSE && finish == ON_RESUME) { + // Special case when we can just directly go to resumed state. + mLifecycleSequence.add(ON_RESUME); + } else if (start <= ON_STOP && finish >= ON_START) { + // Restart and go to required state. + + // Go to stopped state first. + for (int i = start + 1; i <= ON_STOP; i++) { + mLifecycleSequence.add(i); + } + // Restart + mLifecycleSequence.add(ON_RESTART); + // Go to required state + for (int i = ON_START; i <= finish; i++) { + mLifecycleSequence.add(i); + } + } else { + // Relaunch and go to required state + + // Go to destroyed state first. + for (int i = start + 1; i <= ON_DESTROY; i++) { + mLifecycleSequence.add(i); + } + // Go to required state + for (int i = ON_CREATE; i <= finish; i++) { + mLifecycleSequence.add(i); + } + } + } + + // Remove last transition in case we want to perform it with some specific params. + if (excludeLastState && mLifecycleSequence.size() != 0) { + mLifecycleSequence.remove(mLifecycleSequence.size() - 1); + } + } + + @VisibleForTesting + public int[] getLifecycleSequence() { + return mLifecycleSequence.toArray(); + } + + private static void log(String message) { + if (DEBUG_RESOLVER) Slog.d(TAG, message); + } +} diff --git a/core/java/android/app/servertransaction/WindowVisibilityItem.java b/core/java/android/app/servertransaction/WindowVisibilityItem.java new file mode 100644 index 000000000000..d9956b1348b1 --- /dev/null +++ b/core/java/android/app/servertransaction/WindowVisibilityItem.java @@ -0,0 +1,110 @@ +/* + * 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.app.servertransaction; + +import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; + +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Trace; + +/** + * Window visibility change message. + * @hide + */ +public class WindowVisibilityItem extends ClientTransactionItem { + + private boolean mShowWindow; + + @Override + public void execute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityShowWindow"); + client.handleWindowVisibility(token, mShowWindow); + Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); + } + + + // ObjectPoolItem implementation + + private WindowVisibilityItem() {} + + /** Obtain an instance initialized with provided params. */ + public static WindowVisibilityItem obtain(boolean showWindow) { + WindowVisibilityItem instance = ObjectPool.obtain(WindowVisibilityItem.class); + if (instance == null) { + instance = new WindowVisibilityItem(); + } + instance.mShowWindow = showWindow; + + return instance; + } + + @Override + public void recycle() { + mShowWindow = false; + ObjectPool.recycle(this); + } + + + // Parcelable implementation + + /** Write to Parcel. */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(mShowWindow); + } + + /** Read from Parcel. */ + private WindowVisibilityItem(Parcel in) { + mShowWindow = in.readBoolean(); + } + + public static final Creator<WindowVisibilityItem> CREATOR = + new Creator<WindowVisibilityItem>() { + public WindowVisibilityItem createFromParcel(Parcel in) { + return new WindowVisibilityItem(in); + } + + public WindowVisibilityItem[] newArray(int size) { + return new WindowVisibilityItem[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final WindowVisibilityItem other = (WindowVisibilityItem) o; + return mShowWindow == other.mShowWindow; + } + + @Override + public int hashCode() { + return 17 + 31 * (mShowWindow ? 1 : 0); + } + + @Override + public String toString() { + return "WindowVisibilityItem{showWindow=" + mShowWindow + "}"; + } +} diff --git a/core/java/android/view/autofill/AutoFillType.aidl b/core/java/android/app/slice/ISliceManager.aidl index 4606b48e9e10..6e52f385bcf7 100644 --- a/core/java/android/view/autofill/AutoFillType.aidl +++ b/core/java/android/app/slice/ISliceManager.aidl @@ -1,5 +1,5 @@ /** - * Copyright (c) 2016, The Android Open Source Project + * 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. @@ -14,9 +14,8 @@ * limitations under the License. */ -package android.view.autofill; +package android.app.slice; -/* - * TODO(b/35956626): remove once clients use getAutoFilltype() - */ -parcelable AutoFillType;
\ No newline at end of file +/** @hide */ +interface ISliceManager { +} diff --git a/core/java/android/app/slice/Slice.java b/core/java/android/app/slice/Slice.java new file mode 100644 index 000000000000..b13067ee97e3 --- /dev/null +++ b/core/java/android/app/slice/Slice.java @@ -0,0 +1,632 @@ +/* + * 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.app.slice; + +import android.annotation.NonNull; +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; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * A slice is a piece of app content and actions that can be surfaced outside of the app. + * + * <p>They are constructed using {@link Builder} in a tree structure + * that provides the OS some information about how the content should be displayed. + */ +public final class Slice implements Parcelable { + + /** + * @hide + */ + @StringDef(prefix = { "HINT_" }, value = { + HINT_TITLE, + HINT_LIST, + HINT_LIST_ITEM, + HINT_LARGE, + HINT_ACTIONS, + HINT_SELECTED, + HINT_NO_TINT, + HINT_HIDDEN, + HINT_TOGGLE, + HINT_HORIZONTAL, + HINT_PARTIAL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SliceHint {} + + /** + * The meta-data key that allows an activity to easily be linked directly to a slice. + * <p> + * An activity can be statically linked to a slice uri by including a meta-data item + * for this key that contains a valid slice uri for the same application declaring + * the activity. + * @hide + */ + public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI"; + + /** + * 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 SliceItem#FORMAT_ACTION + */ + public static final String HINT_TITLE = "title"; + /** + * Hint that all sub-items/sub-slices within this content should be considered + * to have {@link #HINT_LIST_ITEM}. + */ + public static final String HINT_LIST = "list"; + /** + * Hint that this item is part of a list and should be formatted as if is part + * of a list. + */ + public static final String HINT_LIST_ITEM = "list_item"; + /** + * Hint that this content is important and should be larger when displayed if + * possible. + */ + public static final String HINT_LARGE = "large"; + /** + * Hint that this slice contains a number of actions that can be grouped together + * in a sort of controls area of the UI. + */ + public static final String HINT_ACTIONS = "actions"; + /** + * Hint indicating that this item (and its sub-items) are the current selection. + */ + public static final String HINT_SELECTED = "selected"; + /** + * Hint to indicate that this content should not be tinted. + */ + public static final String HINT_NO_TINT = "no_tint"; + /** + * Hint to indicate that this content should not be shown in larger renderings + * of Slices. This content may be used to populate the shortcut/icon + * 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 that list items within this slice or subslice would appear better + * if organized horizontally. + */ + public static final String HINT_HORIZONTAL = "horizontal"; + /** + * 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. + */ + public static final String HINT_PARTIAL = "partial"; + /** + * A hint representing that this item is the max value possible for the slice containing this. + * Used to indicate the maximum integer value for a {@link #SUBTYPE_SLIDER}. + */ + public static final String HINT_MAX = "max"; + + /** + * 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"; + /** + * Subtype to indicate that this is a message as part of a communication + * sequence in this slice. + */ + public static final String SUBTYPE_MESSAGE = "message"; + /** + * Subtype to tag the source (i.e. sender) of a {@link #SUBTYPE_MESSAGE}. + */ + public static final String SUBTYPE_SOURCE = "source"; + /** + * Subtype to tag an item as representing a color. + */ + public static final String SUBTYPE_COLOR = "color"; + /** + * Subtype to tag an item represents a slider. + */ + public static final String SUBTYPE_SLIDER = "slider"; + + private final SliceItem[] mItems; + private final @SliceHint String[] mHints; + private SliceSpec mSpec; + private Uri mUri; + + Slice(ArrayList<SliceItem> items, @SliceHint String[] hints, Uri uri, SliceSpec spec) { + mHints = hints; + mItems = items.toArray(new SliceItem[items.size()]); + mUri = uri; + mSpec = spec; + } + + protected Slice(Parcel in) { + mHints = in.readStringArray(); + int n = in.readInt(); + mItems = new SliceItem[n]; + for (int i = 0; i < n; i++) { + mItems[i] = SliceItem.CREATOR.createFromParcel(in); + } + mUri = Uri.CREATOR.createFromParcel(in); + mSpec = in.readTypedObject(SliceSpec.CREATOR); + } + + /** + * @return The spec for this slice + */ + public @Nullable SliceSpec getSpec() { + return mSpec; + } + + /** + * @return The Uri that this Slice represents. + */ + public Uri getUri() { + return mUri; + } + + /** + * @return All child {@link SliceItem}s that this Slice contains. + */ + public List<SliceItem> getItems() { + return Arrays.asList(mItems); + } + + /** + * @return All hints associated with this Slice. + */ + public @SliceHint List<String> getHints() { + return Arrays.asList(mHints); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringArray(mHints); + dest.writeInt(mItems.length); + for (int i = 0; i < mItems.length; i++) { + mItems[i].writeToParcel(dest, flags); + } + mUri.writeToParcel(dest, 0); + dest.writeTypedObject(mSpec, flags); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * @hide + */ + public boolean hasHint(@SliceHint String hint) { + return ArrayUtils.contains(mHints, hint); + } + + /** + * A Builder used to construct {@link Slice}s + */ + public static class Builder { + + private final Uri mUri; + private ArrayList<SliceItem> mItems = new ArrayList<>(); + private @SliceHint ArrayList<String> mHints = new ArrayList<>(); + private SliceSpec mSpec; + + /** + * Create a builder which will construct a {@link Slice} for the Given Uri. + * @param uri Uri to tag for this slice. + */ + public Builder(@NonNull Uri uri) { + mUri = uri; + } + + /** + * Create a builder for a {@link Slice} that is a sub-slice of the slice + * being constructed by the provided builder. + * @param parent The builder constructing the parent slice + */ + public Builder(@NonNull Slice.Builder parent) { + mUri = parent.mUri.buildUpon().appendPath("_gen") + .appendPath(String.valueOf(mItems.size())).build(); + } + + /** + * Add hints to the Slice being constructed + */ + public Builder addHints(@SliceHint String... hints) { + mHints.addAll(Arrays.asList(hints)); + return this; + } + + /** + * Add hints to the Slice being constructed + */ + public Builder addHints(@SliceHint List<String> hints) { + return addHints(hints.toArray(new String[hints.size()])); + } + + /** + * Add the spec for this slice. + */ + public Builder setSpec(SliceSpec spec) { + mSpec = spec; + return this; + } + + /** + * Add a sub-slice to the slice being constructed + */ + public Builder addSubSlice(@NonNull Slice slice) { + return addSubSlice(slice, null); + } + + /** + * Add a sub-slice to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Builder addSubSlice(@NonNull Slice slice, @Nullable String subType) { + mItems.add(new SliceItem(slice, SliceItem.FORMAT_SLICE, subType, + slice.getHints().toArray(new String[slice.getHints().size()]))); + return this; + } + + /** + * Add an action to the slice being constructed + */ + public Slice.Builder addAction(@NonNull PendingIntent action, @NonNull Slice s) { + return addAction(action, s, null); + } + + /** + * Add an action to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addAction(@NonNull PendingIntent action, @NonNull Slice s, + @Nullable String subType) { + List<String> hints = s.getHints(); + s.mSpec = null; + mItems.add(new SliceItem(action, s, SliceItem.FORMAT_ACTION, subType, hints.toArray( + new String[hints.size()]))); + return this; + } + + /** + * Add text to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Builder addText(CharSequence text, @Nullable String subType, + @SliceHint String... hints) { + mItems.add(new SliceItem(text, SliceItem.FORMAT_TEXT, subType, hints)); + return this; + } + + /** + * Add text to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Builder addText(CharSequence text, @Nullable String subType, + @SliceHint List<String> hints) { + return addText(text, subType, hints.toArray(new String[hints.size()])); + } + + /** + * Add an image to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Builder addIcon(Icon icon, @Nullable String subType, @SliceHint String... hints) { + mItems.add(new SliceItem(icon, SliceItem.FORMAT_IMAGE, subType, hints)); + return this; + } + + /** + * Add an image to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Builder addIcon(Icon icon, @Nullable String subType, @SliceHint List<String> hints) { + return addIcon(icon, subType, hints.toArray(new String[hints.size()])); + } + + /** + * Add remote input to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addRemoteInput(RemoteInput remoteInput, @Nullable String subType, + @SliceHint List<String> hints) { + return addRemoteInput(remoteInput, subType, hints.toArray(new String[hints.size()])); + } + + /** + * Add remote input to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addRemoteInput(RemoteInput remoteInput, @Nullable String subType, + @SliceHint String... hints) { + mItems.add(new SliceItem(remoteInput, SliceItem.FORMAT_REMOTE_INPUT, + subType, hints)); + return this; + } + + /** + * Add a color to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + * @deprecated will be removed once supportlib updates + */ + public Builder addColor(int color, @Nullable String subType, @SliceHint String... hints) { + mItems.add(new SliceItem(color, SliceItem.FORMAT_INT, subType, hints)); + return this; + } + + /** + * Add a color to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + * @deprecated will be removed once supportlib updates + */ + public Builder addColor(int color, @Nullable String subType, + @SliceHint List<String> hints) { + return addColor(color, subType, hints.toArray(new String[hints.size()])); + } + + /** + * Add a color to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Builder addInt(int value, @Nullable String subType, @SliceHint String... hints) { + mItems.add(new SliceItem(value, SliceItem.FORMAT_INT, subType, hints)); + return this; + } + + /** + * Add a color to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Builder addInt(int value, @Nullable String subType, + @SliceHint List<String> hints) { + return addInt(value, subType, hints.toArray(new String[hints.size()])); + } + + /** + * Add a timestamp to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addTimestamp(long time, @Nullable String subType, + @SliceHint String... hints) { + mItems.add(new SliceItem(time, SliceItem.FORMAT_TIMESTAMP, subType, + hints)); + return this; + } + + /** + * Add a timestamp to the slice being constructed + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addTimestamp(long time, @Nullable String subType, + @SliceHint List<String> hints) { + return addTimestamp(time, subType, hints.toArray(new String[hints.size()])); + } + + /** + * Add a bundle to the slice being constructed. + * <p>Expected to be used for support library extension, should not be used for general + * development + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addBundle(Bundle bundle, @Nullable String subType, + @SliceHint String... hints) { + mItems.add(new SliceItem(bundle, SliceItem.FORMAT_BUNDLE, subType, + hints)); + return this; + } + + /** + * Add a bundle to the slice being constructed. + * <p>Expected to be used for support library extension, should not be used for general + * development + * @param subType Optional template-specific type information + * @see {@link SliceItem#getSubType()} + */ + public Slice.Builder addBundle(Bundle bundle, @Nullable String subType, + @SliceHint List<String> hints) { + return addBundle(bundle, subType, hints.toArray(new String[hints.size()])); + } + + /** + * Construct the slice. + */ + public Slice build() { + return new Slice(mItems, mHints.toArray(new String[mHints.size()]), mUri, mSpec); + } + } + + public static final Creator<Slice> CREATOR = new Creator<Slice>() { + @Override + public Slice createFromParcel(Parcel in) { + return new Slice(in); + } + + @Override + public Slice[] newArray(int size) { + return new Slice[size]; + } + }; + + /** + * @hide + * @return A string representation of this slice. + */ + public String toString() { + return toString(""); + } + + private String toString(String indent) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mItems.length; i++) { + sb.append(indent); + if (Objects.equals(mItems[i].getFormat(), SliceItem.FORMAT_SLICE)) { + sb.append("slice:\n"); + sb.append(mItems[i].getSlice().toString(indent + " ")); + } else if (Objects.equals(mItems[i].getFormat(), SliceItem.FORMAT_TEXT)) { + sb.append("text: "); + sb.append(mItems[i].getText()); + sb.append("\n"); + } else { + sb.append(mItems[i].getFormat()); + sb.append("\n"); + } + } + return sb.toString(); + } + + /** + * Turns a slice Uri into slice content. + * + * @param resolver ContentResolver to be used. + * @param uri The URI to a slice provider + * @param supportedSpecs List of supported specs. + * @return The Slice provided by the app or null if none is given. + * @see Slice + */ + public static @Nullable Slice bindSlice(ContentResolver resolver, @NonNull Uri uri, + List<SliceSpec> supportedSpecs) { + Preconditions.checkNotNull(uri, "uri"); + IContentProvider provider = resolver.acquireProvider(uri); + if (provider == null) { + throw new IllegalArgumentException("Unknown URI " + uri); + } + try { + Bundle extras = new Bundle(); + extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); + extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, + new ArrayList<>(supportedSpecs)); + final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE, + null, extras); + Bundle.setDefusable(res, true); + 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); + } + } + + /** + * 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. + * @param supportedSpecs List of supported specs. + * @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, + List<SliceSpec> supportedSpecs) { + 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, supportedSpecs); + } + // 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); + extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, + new ArrayList<>(supportedSpecs)); + 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/core/java/android/app/slice/SliceItem.java b/core/java/android/app/slice/SliceItem.java new file mode 100644 index 000000000000..bcfd413fb823 --- /dev/null +++ b/core/java/android/app/slice/SliceItem.java @@ -0,0 +1,381 @@ +/* + * 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.app.slice; + +import android.annotation.NonNull; +import android.annotation.StringDef; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Pair; +import android.widget.RemoteViews; + +import com.android.internal.util.ArrayUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; + + +/** + * A SliceItem is a single unit in the tree structure of a {@link Slice}. + * + * A SliceItem a piece of content and some hints about what that content + * means or how it should be displayed. The types of content can be: + * <li>{@link #FORMAT_SLICE}</li> + * <li>{@link #FORMAT_TEXT}</li> + * <li>{@link #FORMAT_IMAGE}</li> + * <li>{@link #FORMAT_ACTION}</li> + * <li>{@link #FORMAT_INT}</li> + * <li>{@link #FORMAT_TIMESTAMP}</li> + * <li>{@link #FORMAT_REMOTE_INPUT}</li> + * <li>{@link #FORMAT_BUNDLE}</li> + * + * The hints that a {@link SliceItem} are a set of strings which annotate + * the content. The hints that are guaranteed to be understood by the system + * are defined on {@link Slice}. + */ +public final class SliceItem implements Parcelable { + + private static final String TAG = "SliceItem"; + + /** + * @hide + */ + @StringDef(prefix = { "FORMAT_" }, value = { + FORMAT_SLICE, + FORMAT_TEXT, + FORMAT_IMAGE, + FORMAT_ACTION, + FORMAT_INT, + FORMAT_TIMESTAMP, + FORMAT_REMOTE_INPUT, + FORMAT_BUNDLE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SliceType {} + + /** + * A {@link SliceItem} that contains a {@link Slice} + */ + public static final String FORMAT_SLICE = "slice"; + /** + * A {@link SliceItem} that contains a {@link CharSequence} + */ + public static final String FORMAT_TEXT = "text"; + /** + * A {@link SliceItem} that contains an {@link Icon} + */ + public static final String FORMAT_IMAGE = "image"; + /** + * A {@link SliceItem} that contains a {@link PendingIntent} + * + * Note: Actions contain 2 pieces of data, In addition to the pending intent, the + * item contains a {@link Slice} that the action applies to. + */ + public static final String FORMAT_ACTION = "action"; + /** + * A {@link SliceItem} that contains an int. + */ + public static final String FORMAT_INT = "int"; + /** + * A {@link SliceItem} that contains an int. + * @deprecated to be removed + */ + public static final String FORMAT_COLOR = "color"; + /** + * A {@link SliceItem} that contains a timestamp. + */ + public static final String FORMAT_TIMESTAMP = "timestamp"; + /** + * A {@link SliceItem} that contains a {@link RemoteInput}. + */ + public static final String FORMAT_REMOTE_INPUT = "input"; + /** + * A {@link SliceItem} that contains a {@link Bundle}. + */ + public static final String FORMAT_BUNDLE = "bundle"; + + /** + * @hide + */ + protected @Slice.SliceHint + String[] mHints; + private final String mFormat; + private final String mSubType; + private final Object mObj; + + /** + * @hide + */ + public SliceItem(Object obj, @SliceType String format, String subType, + @Slice.SliceHint String[] hints) { + mHints = hints; + mFormat = format; + mSubType = subType; + mObj = obj; + } + + /** + * @hide + */ + public SliceItem(PendingIntent intent, Slice slice, String format, String subType, + @Slice.SliceHint String[] hints) { + this(new Pair<>(intent, slice), format, subType, hints); + } + + /** + * Gets all hints associated with this SliceItem. + * @return Array of hints. + */ + public @NonNull @Slice.SliceHint List<String> getHints() { + return Arrays.asList(mHints); + } + + /** + * Get the format of this SliceItem. + * <p> + * The format will be one of the following types supported by the platform: + * <li>{@link #FORMAT_SLICE}</li> + * <li>{@link #FORMAT_TEXT}</li> + * <li>{@link #FORMAT_IMAGE}</li> + * <li>{@link #FORMAT_ACTION}</li> + * <li>{@link #FORMAT_INT}</li> + * <li>{@link #FORMAT_TIMESTAMP}</li> + * <li>{@link #FORMAT_REMOTE_INPUT}</li> + * <li>{@link #FORMAT_BUNDLE}</li> + * @see #getSubType() () + */ + public String getFormat() { + return mFormat; + } + + /** + * Get the sub-type of this SliceItem. + * <p> + * Subtypes provide additional information about the type of this information beyond basic + * interpretations inferred by {@link #getFormat()}. For example a slice may contain + * many {@link #FORMAT_TEXT} items, but only some of them may be {@link Slice#SUBTYPE_MESSAGE}. + * @see #getFormat() + */ + public String getSubType() { + return mSubType; + } + + /** + * @return The text held by this {@link #FORMAT_TEXT} SliceItem + */ + public CharSequence getText() { + return (CharSequence) mObj; + } + + /** + * @return The parcelable held by this {@link #FORMAT_BUNDLE} SliceItem + */ + public Bundle getBundle() { + return (Bundle) mObj; + } + + /** + * @return The icon held by this {@link #FORMAT_IMAGE} SliceItem + */ + public Icon getIcon() { + return (Icon) mObj; + } + + /** + * @return The pending intent held by this {@link #FORMAT_ACTION} SliceItem + */ + public PendingIntent getAction() { + return ((Pair<PendingIntent, Slice>) mObj).first; + } + + /** + * @hide This isn't final + */ + public RemoteViews getRemoteView() { + return (RemoteViews) mObj; + } + + /** + * @return The remote input held by this {@link #FORMAT_REMOTE_INPUT} SliceItem + */ + public RemoteInput getRemoteInput() { + return (RemoteInput) mObj; + } + + /** + * @return The color held by this {@link #FORMAT_INT} SliceItem + */ + public int getInt() { + return (Integer) mObj; + } + + /** + * @deprecated to be removed. + */ + public int getColor() { + return (Integer) mObj; + } + + /** + * @return The slice held by this {@link #FORMAT_ACTION} or {@link #FORMAT_SLICE} SliceItem + */ + public Slice getSlice() { + if (FORMAT_ACTION.equals(getFormat())) { + return ((Pair<PendingIntent, Slice>) mObj).second; + } + return (Slice) mObj; + } + + /** + * @return The timestamp held by this {@link #FORMAT_TIMESTAMP} SliceItem + */ + public long getTimestamp() { + return (Long) mObj; + } + + /** + * @param hint The hint to check for + * @return true if this item contains the given hint + */ + public boolean hasHint(@Slice.SliceHint String hint) { + return ArrayUtils.contains(mHints, hint); + } + + /** + * @hide + */ + public SliceItem(Parcel in) { + mHints = in.readStringArray(); + mFormat = in.readString(); + mSubType = in.readString(); + mObj = readObj(mFormat, in); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringArray(mHints); + dest.writeString(mFormat); + dest.writeString(mSubType); + writeObj(dest, flags, mObj, mFormat); + } + + /** + * @hide + */ + public boolean hasHints(@Slice.SliceHint String[] hints) { + if (hints == null) return true; + for (String hint : hints) { + if (!TextUtils.isEmpty(hint) && !ArrayUtils.contains(mHints, hint)) { + return false; + } + } + return true; + } + + /** + * @hide + */ + public boolean hasAnyHints(@Slice.SliceHint String[] hints) { + if (hints == null) return false; + for (String hint : hints) { + if (ArrayUtils.contains(mHints, hint)) { + return true; + } + } + return false; + } + + private static String getBaseType(String type) { + int index = type.indexOf('/'); + if (index >= 0) { + return type.substring(0, index); + } + return type; + } + + private static void writeObj(Parcel dest, int flags, Object obj, String type) { + switch (getBaseType(type)) { + case FORMAT_SLICE: + case FORMAT_IMAGE: + case FORMAT_REMOTE_INPUT: + case FORMAT_BUNDLE: + ((Parcelable) obj).writeToParcel(dest, flags); + break; + case FORMAT_ACTION: + ((Pair<PendingIntent, Slice>) obj).first.writeToParcel(dest, flags); + ((Pair<PendingIntent, Slice>) obj).second.writeToParcel(dest, flags); + break; + case FORMAT_TEXT: + TextUtils.writeToParcel((CharSequence) obj, dest, flags); + break; + case FORMAT_INT: + dest.writeInt((Integer) obj); + break; + case FORMAT_TIMESTAMP: + dest.writeLong((Long) obj); + break; + } + } + + private static Object readObj(String type, Parcel in) { + switch (getBaseType(type)) { + case FORMAT_SLICE: + return Slice.CREATOR.createFromParcel(in); + case FORMAT_TEXT: + return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + case FORMAT_IMAGE: + return Icon.CREATOR.createFromParcel(in); + case FORMAT_ACTION: + return new Pair<>( + PendingIntent.CREATOR.createFromParcel(in), + Slice.CREATOR.createFromParcel(in)); + case FORMAT_INT: + return in.readInt(); + case FORMAT_TIMESTAMP: + return in.readLong(); + case FORMAT_REMOTE_INPUT: + return RemoteInput.CREATOR.createFromParcel(in); + case FORMAT_BUNDLE: + return Bundle.CREATOR.createFromParcel(in); + } + throw new RuntimeException("Unsupported type " + type); + } + + public static final Creator<SliceItem> CREATOR = new Creator<SliceItem>() { + @Override + public SliceItem createFromParcel(Parcel in) { + return new SliceItem(in); + } + + @Override + public SliceItem[] newArray(int size) { + return new SliceItem[size]; + } + }; +} diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java new file mode 100644 index 000000000000..e99f67632712 --- /dev/null +++ b/core/java/android/app/slice/SliceManager.java @@ -0,0 +1,39 @@ +/* + * 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.app.slice; + +import android.annotation.SystemService; +import android.content.Context; +import android.os.Handler; +import android.os.ServiceManager; +import android.os.ServiceManager.ServiceNotFoundException; + +/** + * @hide + */ +@SystemService(Context.SLICE_SERVICE) +public class SliceManager { + + private final ISliceManager mService; + private final Context mContext; + + public SliceManager(Context context, Handler handler) throws ServiceNotFoundException { + mContext = context; + mService = ISliceManager.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE)); + } +} diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java new file mode 100644 index 000000000000..ac5365c35f49 --- /dev/null +++ b/core/java/android/app/slice/SliceProvider.java @@ -0,0 +1,269 @@ +/* + * 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.app.slice; + +import android.Manifest.permission; +import android.annotation.NonNull; +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.List; +import java.util.concurrent.CountDownLatch; + +/** + * 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> + * + * <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}. + */ + public static final String SLICE_TYPE = "vnd.android.slice"; + + private static final String TAG = "SliceProvider"; + /** + * @hide + */ + public static final String EXTRA_BIND_URI = "slice_uri"; + /** + * @hide + */ + public static final String EXTRA_SUPPORTED_SPECS = "supported_specs"; + /** + * @hide + */ + public static final String METHOD_SLICE = "bind_slice"; + /** + * @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; + + /** + * Implemented to create a slice. Will be called on the main thread. + * <p> + * onBindSlice should return as quickly as possible so that the UI tied + * to this slice can be responsive. No network or other IO will be allowed + * during onBindSlice. Any loading that needs to be done should happen + * off the main thread with a call to {@link ContentResolver#notifyChange(Uri, ContentObserver)} + * when the app is ready to provide the complete data in onBindSlice. + * <p> + * The slice returned should have a spec that is compatible with one of + * the supported specs. + * + * @param sliceUri Uri to bind. + * @param supportedSpecs List of supported specs. + * @see {@link Slice}. + * @see {@link Slice#HINT_PARTIAL} + */ + public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) { + return onBindSlice(sliceUri); + } + + /** + * @deprecated migrating to {@link #onBindSlice(Uri, List)} + */ + @Deprecated + public Slice onBindSlice(Uri sliceUri) { + return null; + } + + /** + * 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} + */ + 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) { + if (DEBUG) Log.d(TAG, "update " + uri); + return 0; + } + + @Override + public final int delete(Uri uri, String selection, String[] selectionArgs) { + if (DEBUG) Log.d(TAG, "delete " + uri); + return 0; + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, String[] + selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Cursor query(Uri uri, String[] projection, Bundle queryArgs, + CancellationSignal cancellationSignal) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Uri insert(Uri uri, ContentValues values) { + if (DEBUG) Log.d(TAG, "insert " + uri); + return null; + } + + @Override + public final String getType(Uri uri) { + if (DEBUG) Log.d(TAG, "getType " + uri); + return SLICE_TYPE; + } + + @Override + public Bundle call(String method, String arg, Bundle extras) { + if (method.equals(METHOD_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"); + } + List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS); + + Slice s = handleBindSlice(uri, supportedSpecs); + 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); + List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS); + Bundle b = new Bundle(); + if (uri != null) { + Slice s = handleBindSlice(uri, supportedSpecs); + b.putParcelable(EXTRA_SLICE, s); + } else { + b.putParcelable(EXTRA_SLICE, null); + } + return b; + } + return super.call(method, arg, extras); + } + + private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) { + if (Looper.myLooper() == Looper.getMainLooper()) { + return onBindSliceStrict(sliceUri, supportedSpecs); + } else { + CountDownLatch latch = new CountDownLatch(1); + Slice[] output = new Slice[1]; + Handler.getMain().post(() -> { + output[0] = onBindSliceStrict(sliceUri, supportedSpecs); + latch.countDown(); + }); + try { + latch.await(); + return output[0]; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> supportedSpecs) { + ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + try { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyDeath() + .build()); + return onBindSlice(sliceUri, supportedSpecs); + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + } +} diff --git a/core/java/android/app/slice/SliceQuery.java b/core/java/android/app/slice/SliceQuery.java new file mode 100644 index 000000000000..20eca880f63b --- /dev/null +++ b/core/java/android/app/slice/SliceQuery.java @@ -0,0 +1,188 @@ +/* + * 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.app.slice; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * A bunch of utilities for searching the contents of a slice. + * @hide + */ +public class SliceQuery { + private static final String TAG = "SliceQuery"; + + /** + * @hide + */ + public static SliceItem getPrimaryIcon(Slice slice) { + for (SliceItem item : slice.getItems()) { + if (Objects.equals(item.getFormat(), SliceItem.FORMAT_IMAGE)) { + return item; + } + if (!(compareTypes(item, SliceItem.FORMAT_SLICE) + && item.hasHint(Slice.HINT_LIST)) + && !item.hasHint(Slice.HINT_ACTIONS) + && !item.hasHint(Slice.HINT_LIST_ITEM) + && !compareTypes(item, SliceItem.FORMAT_ACTION)) { + SliceItem icon = SliceQuery.find(item, SliceItem.FORMAT_IMAGE); + if (icon != null) { + return icon; + } + } + } + return null; + } + + /** + * @hide + */ + public static SliceItem findNotContaining(SliceItem container, List<SliceItem> list) { + SliceItem ret = null; + while (ret == null && list.size() != 0) { + SliceItem remove = list.remove(0); + if (!contains(container, remove)) { + ret = remove; + } + } + return ret; + } + + /** + * @hide + */ + private static boolean contains(SliceItem container, SliceItem item) { + if (container == null || item == null) return false; + return stream(container).filter(s -> (s == item)).findAny().isPresent(); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, String type) { + return findAll(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, String type, String hints, String nonHints) { + return findAll(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, String type, String[] hints, + String[] nonHints) { + return stream(s).filter(item -> compareTypes(item, type) + && (item.hasHints(hints) && !item.hasAnyHints(nonHints))) + .collect(Collectors.toList()); + } + + /** + * @hide + */ + public static SliceItem find(Slice s, String type, String hints, String nonHints) { + return find(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @hide + */ + public static SliceItem find(Slice s, String type) { + return find(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, String type) { + return find(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, String type, String hints, String nonHints) { + return find(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @hide + */ + public static SliceItem find(Slice s, String type, String[] hints, String[] nonHints) { + List<String> h = s.getHints(); + return find(new SliceItem(s, SliceItem.FORMAT_SLICE, null, h.toArray(new String[h.size()])), + type, hints, nonHints); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, String type, String[] hints, String[] nonHints) { + return stream(s).filter(item -> compareTypes(item, type) + && (item.hasHints(hints) && !item.hasAnyHints(nonHints))).findFirst().orElse(null); + } + + /** + * @hide + */ + public static Stream<SliceItem> stream(SliceItem slice) { + Queue<SliceItem> items = new LinkedList(); + items.add(slice); + Iterator<SliceItem> iterator = new Iterator<SliceItem>() { + @Override + public boolean hasNext() { + return items.size() != 0; + } + + @Override + public SliceItem next() { + SliceItem item = items.poll(); + if (compareTypes(item, SliceItem.FORMAT_SLICE) + || compareTypes(item, SliceItem.FORMAT_ACTION)) { + items.addAll(item.getSlice().getItems()); + } + return item; + } + }; + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); + } + + /** + * @hide + */ + public static boolean compareTypes(SliceItem item, String desiredType) { + final int typeLength = desiredType.length(); + if (typeLength == 3 && desiredType.equals("*/*")) { + return true; + } + if (item.getSubType() == null && desiredType.indexOf('/') < 0) { + return item.getFormat().equals(desiredType); + } + return (item.getFormat() + "/" + item.getSubType()) + .matches(desiredType.replaceAll("\\*", ".*")); + } +} diff --git a/core/java/android/app/slice/SliceSpec.java b/core/java/android/app/slice/SliceSpec.java new file mode 100644 index 000000000000..433b67e9aacb --- /dev/null +++ b/core/java/android/app/slice/SliceSpec.java @@ -0,0 +1,117 @@ +/* + * 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.app.slice; + +import android.annotation.NonNull; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Class describing the structure of the data contained within a slice. + * <p> + * A data version contains a string which describes the type of structure + * and a revision which denotes this specific implementation. Revisions are expected + * to be backwards compatible and monotonically increasing. Meaning if a + * SliceSpec has the same type and an equal or lesser revision, + * it is expected to be compatible. + * <p> + * Apps rendering slices will provide a list of supported versions to the OS which + * will also be given to the app. Apps should only return a {@link Slice} with a + * {@link SliceSpec} that one of the supported {@link SliceSpec}s provided + * {@link #canRender}. + * + * @see Slice + * @see SliceProvider#onBindSlice(Uri) + */ +public final class SliceSpec implements Parcelable { + + private final String mType; + private final int mRevision; + + public SliceSpec(@NonNull String type, int revision) { + mType = type; + mRevision = revision; + } + + /** + * @hide + */ + public SliceSpec(Parcel source) { + mType = source.readString(); + mRevision = source.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mType); + dest.writeInt(mRevision); + } + + /** + * Gets the type of the version. + */ + public String getType() { + return mType; + } + + /** + * Gets the revision of the version. + */ + public int getRevision() { + return mRevision; + } + + /** + * Indicates that this spec can be used to render the specified spec. + * <p> + * Rendering support is not bi-directional (e.g. Spec v3 can render + * Spec v2, but Spec v2 cannot render Spec v3). + * + * @param candidate candidate format of data. + * @return true if versions are compatible. + * @see androidx.app.slice.widget.SliceView + */ + public boolean canRender(@NonNull SliceSpec candidate) { + if (!mType.equals(candidate.mType)) return false; + return mRevision >= candidate.mRevision; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SliceSpec)) return false; + SliceSpec other = (SliceSpec) obj; + return mType.equals(other.mType) && mRevision == other.mRevision; + } + + public static final Creator<SliceSpec> CREATOR = new Creator<SliceSpec>() { + @Override + public SliceSpec createFromParcel(Parcel source) { + return new SliceSpec(source); + } + + @Override + public SliceSpec[] newArray(int size) { + return new SliceSpec[size]; + } + }; +} diff --git a/core/java/android/app/timezone/RulesManager.java b/core/java/android/app/timezone/RulesManager.java index ad9b698a8fd7..417e7d26f4f5 100644 --- a/core/java/android/app/timezone/RulesManager.java +++ b/core/java/android/app/timezone/RulesManager.java @@ -105,9 +105,9 @@ public final class RulesManager { */ public RulesState getRulesState() { try { - logDebug("sIRulesManager.getRulesState()"); + logDebug("mIRulesManager.getRulesState()"); RulesState rulesState = mIRulesManager.getRulesState(); - logDebug("sIRulesManager.getRulesState() returned " + rulesState); + logDebug("mIRulesManager.getRulesState() returned " + rulesState); return rulesState; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -131,7 +131,7 @@ public final class RulesManager { ICallback iCallback = new CallbackWrapper(mContext, callback); try { - logDebug("sIRulesManager.requestInstall()"); + logDebug("mIRulesManager.requestInstall()"); return mIRulesManager.requestInstall(distroFileDescriptor, checkToken, iCallback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -151,7 +151,7 @@ public final class RulesManager { public int requestUninstall(byte[] checkToken, Callback callback) { ICallback iCallback = new CallbackWrapper(mContext, callback); try { - logDebug("sIRulesManager.requestUninstall()"); + logDebug("mIRulesManager.requestUninstall()"); return mIRulesManager.requestUninstall(checkToken, iCallback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -196,7 +196,7 @@ public final class RulesManager { */ public void requestNothing(byte[] checkToken, boolean succeeded) { try { - logDebug("sIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken)); + logDebug("mIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken)); mIRulesManager.requestNothing(checkToken, succeeded); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 31b235977a04..4fbbdf2a9281 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -36,4 +36,6 @@ interface IUsageStatsManager { void onCarrierPrivilegedAppsChanged(); void reportChooserSelection(String packageName, int userId, String contentType, in String[] annotations, String action); + int getAppStandbyBucket(String packageName, String callingPackage, int userId); + void setAppStandbyBucket(String packageName, int bucket, int userId); } diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java index 0d7a94138411..8200414fa6bd 100644 --- a/core/java/android/app/usage/UsageEvents.java +++ b/core/java/android/app/usage/UsageEvents.java @@ -100,6 +100,12 @@ public final class UsageEvents implements Parcelable { */ public static final int CHOOSER_ACTION = 9; + /** + * An event type denoting that a notification was viewed by the user. + * @hide + */ + public static final int NOTIFICATION_SEEN = 10; + /** @hide */ public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0; diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 1f939f996c68..d614b20a0788 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -16,6 +16,7 @@ package android.app.usage; +import android.annotation.IntDef; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; @@ -25,6 +26,8 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; import java.util.Map; @@ -48,10 +51,10 @@ import java.util.Map; * </pre> * A request for data in the middle of a time interval will include that interval. * <p/> - * <b>NOTE:</b> This API requires the permission android.permission.PACKAGE_USAGE_STATS, which - * is a system-level permission and will not be granted to third-party apps. However, declaring - * the permission implies intention to use the API and the user of the device can grant permission - * through the Settings application. + * <b>NOTE:</b> This API requires the permission android.permission.PACKAGE_USAGE_STATS. + * However, declaring the permission implies intention to use the API and the user of the device + * still needs to grant permission through the Settings application. + * See {@link android.provider.Settings#ACTION_USAGE_ACCESS_SETTINGS} */ @SystemService(Context.USAGE_STATS_SERVICE) public final class UsageStatsManager { @@ -89,6 +92,76 @@ public final class UsageStatsManager { */ public static final int INTERVAL_COUNT = 4; + + /** + * The app is whitelisted for some reason and the bucket cannot be changed. + * {@hide} + */ + @SystemApi + public static final int STANDBY_BUCKET_EXEMPTED = 5; + + /** + * The app was used very recently, currently in use or likely to be used very soon. + * @see #getAppStandbyBucket() + */ + public static final int STANDBY_BUCKET_ACTIVE = 10; + + /** + * The app was used recently and/or likely to be used in the next few hours. + * @see #getAppStandbyBucket() + */ + public static final int STANDBY_BUCKET_WORKING_SET = 20; + + /** + * The app was used in the last few days and/or likely to be used in the next few days. + * @see #getAppStandbyBucket() + */ + public static final int STANDBY_BUCKET_FREQUENT = 30; + + /** + * The app has not be used for several days and/or is unlikely to be used for several days. + * @see #getAppStandbyBucket() + */ + public static final int STANDBY_BUCKET_RARE = 40; + + /** + * The app has never been used. + * {@hide} + */ + @SystemApi + public static final int STANDBY_BUCKET_NEVER = 50; + + /** {@hide} Reason for bucketing -- default initial state */ + public static final String REASON_DEFAULT = "default"; + + /** {@hide} Reason for bucketing -- timeout */ + public static final String REASON_TIMEOUT = "timeout"; + + /** {@hide} Reason for bucketing -- usage */ + public static final String REASON_USAGE = "usage"; + + /** {@hide} Reason for bucketing -- forced by user / shell command */ + public static final String REASON_FORCED = "forced"; + + /** + * {@hide} + * Reason for bucketing -- predicted. This is a prefix and the UID of the bucketeer will + * be appended. + */ + public static final String REASON_PREDICTED = "predicted"; + + /** @hide */ + @IntDef(flag = false, value = { + STANDBY_BUCKET_EXEMPTED, + STANDBY_BUCKET_ACTIVE, + STANDBY_BUCKET_WORKING_SET, + STANDBY_BUCKET_FREQUENT, + STANDBY_BUCKET_RARE, + STANDBY_BUCKET_NEVER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface StandbyBuckets {} + private static final UsageEvents sEmptyResults = new UsageEvents(); private final Context mContext; @@ -122,7 +195,7 @@ public final class UsageStatsManager { * @param intervalType The time interval by which the stats are aggregated. * @param beginTime The inclusive beginning of the range of stats to include in the results. * @param endTime The exclusive end of the range of stats to include in the results. - * @return A list of {@link UsageStats} or null if none are available. + * @return A list of {@link UsageStats} * * @see #INTERVAL_DAILY * @see #INTERVAL_WEEKLY @@ -139,7 +212,7 @@ public final class UsageStatsManager { return slice.getList(); } } catch (RemoteException e) { - // fallthrough and return null. + // fallthrough and return the empty list. } return Collections.emptyList(); } @@ -152,7 +225,7 @@ public final class UsageStatsManager { * @param intervalType The time interval by which the stats are aggregated. * @param beginTime The inclusive beginning of the range of stats to include in the results. * @param endTime The exclusive end of the range of stats to include in the results. - * @return A list of {@link ConfigurationStats} or null if none are available. + * @return A list of {@link ConfigurationStats} */ public List<ConfigurationStats> queryConfigurations(int intervalType, long beginTime, long endTime) { @@ -172,9 +245,6 @@ public final class UsageStatsManager { /** * Query for events in the given time range. Events are only kept by the system for a few * days. - * <p /> - * <b>NOTE:</b> The last few minutes of the event log will be truncated to prevent abuse - * by applications. * * @param beginTime The inclusive beginning of the range of events to include in the results. * @param endTime The exclusive end of the range of events to include in the results. @@ -188,7 +258,7 @@ public final class UsageStatsManager { return iter; } } catch (RemoteException e) { - // fallthrough and return null + // fallthrough and return empty result. } return sEmptyResults; } @@ -200,8 +270,7 @@ public final class UsageStatsManager { * * @param beginTime The inclusive beginning of the range of stats to include in the results. * @param endTime The exclusive end of the range of stats to include in the results. - * @return A {@link java.util.Map} keyed by package name, or null if no stats are - * available. + * @return A {@link java.util.Map} keyed by package name */ public Map<String, UsageStats> queryAndAggregateUsageStats(long beginTime, long endTime) { List<UsageStats> stats = queryUsageStats(INTERVAL_BEST, beginTime, endTime); @@ -240,7 +309,7 @@ public final class UsageStatsManager { } /** - * @hide + * {@hide} */ public void setAppInactive(String packageName, boolean inactive) { try { @@ -251,6 +320,58 @@ public final class UsageStatsManager { } /** + * Returns the current standby bucket of the calling app. The system determines the standby + * state of the app based on app usage patterns. Standby buckets determine how much an app will + * be restricted from running background tasks such as jobs, alarms and certain PendingIntent + * callbacks. + * Restrictions increase progressively from {@link #STANDBY_BUCKET_ACTIVE} to + * {@link #STANDBY_BUCKET_RARE}, with {@link #STANDBY_BUCKET_ACTIVE} being the least + * restrictive. The battery level of the device might also affect the restrictions. + * + * @return the current standby bucket of the calling app. + */ + public @StandbyBuckets int getAppStandbyBucket() { + try { + return mService.getAppStandbyBucket(mContext.getOpPackageName(), + mContext.getOpPackageName(), + mContext.getUserId()); + } catch (RemoteException e) { + } + return STANDBY_BUCKET_ACTIVE; + } + + /** + * {@hide} + * Returns the current standby bucket of the specified app. The caller must hold the permission + * android.permission.PACKAGE_USAGE_STATS. + * @param packageName the package for which to fetch the current standby bucket. + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) + public @StandbyBuckets int getAppStandbyBucket(String packageName) { + try { + return mService.getAppStandbyBucket(packageName, mContext.getOpPackageName(), + mContext.getUserId()); + } catch (RemoteException e) { + } + return STANDBY_BUCKET_ACTIVE; + } + + /** + * {@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()); + } catch (RemoteException e) { + // Nothing to do + } + } + + /** * {@hide} * Temporarily whitelist the specified app for a short duration. This is to allow an app * receiving a high priority message to be able to access the network and acquire wakelocks diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java index dbaace2f0ac9..4b4fe72f855f 100644 --- a/core/java/android/app/usage/UsageStatsManagerInternal.java +++ b/core/java/android/app/usage/UsageStatsManagerInternal.java @@ -16,6 +16,7 @@ package android.app.usage; +import android.app.usage.UsageStatsManager.StandbyBuckets; import android.content.ComponentName; import android.content.res.Configuration; @@ -91,6 +92,19 @@ public abstract class UsageStatsManagerInternal { public abstract boolean isAppIdle(String packageName, int uidForAppId, int userId); /** + * Returns the app standby bucket that the app is currently in. This accessor does + * <em>not</em> obfuscate instant apps. + * + * @param packageName + * @param userId + * @param nowElapsed The current time, in the elapsedRealtime time base + * @return the AppStandby bucket code the app currently resides in. If the app is + * unknown in the given user, STANDBY_BUCKET_NEVER is returned. + */ + @StandbyBuckets public abstract int getAppStandbyBucket(String packageName, int userId, + long nowElapsed); + + /** * Returns all of the uids for a given user where all packages associating with that uid * are in the app idle state -- there are no associated apps that are not idle. This means * all of the returned uids can be safely considered app idle. @@ -118,7 +132,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/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index 1242cb0fbdfa..ab0eb92e1726 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -19,28 +19,20 @@ package android.appwidget; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; -import android.content.pm.LauncherApps; -import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; -import android.os.Parcel; import android.os.Parcelable; -import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Adapter; import android.widget.AdapterView; @@ -59,24 +51,21 @@ import java.util.concurrent.Executor; * {@link RemoteViews}. */ public class AppWidgetHostView extends FrameLayout { + static final String TAG = "AppWidgetHostView"; + private static final String KEY_JAILED_ARRAY = "jail"; + static final boolean LOGD = false; - static final boolean CROSSFADE = false; static final int VIEW_MODE_NOINIT = 0; static final int VIEW_MODE_CONTENT = 1; static final int VIEW_MODE_ERROR = 2; static final int VIEW_MODE_DEFAULT = 3; - static final int FADE_DURATION = 1000; - // When we're inflating the initialLayout for a AppWidget, we only allow // views that are allowed in RemoteViews. - static final LayoutInflater.Filter sInflaterFilter = new LayoutInflater.Filter() { - public boolean onLoadClass(Class clazz) { - return clazz.isAnnotationPresent(RemoteViews.RemoteView.class); - } - }; + private static final LayoutInflater.Filter INFLATER_FILTER = + (clazz) -> clazz.isAnnotationPresent(RemoteViews.RemoteView.class); Context mContext; Context mRemoteContext; @@ -86,9 +75,6 @@ public class AppWidgetHostView extends FrameLayout { View mView; int mViewMode = VIEW_MODE_NOINIT; int mLayoutId = -1; - long mFadeStartTime = -1; - Bitmap mOld; - Paint mOldPaint = new Paint(); private OnClickHandler mOnClickHandler; private Executor mAsyncExecutor; @@ -145,13 +131,19 @@ public class AppWidgetHostView extends FrameLayout { mAppWidgetId = appWidgetId; mInfo = info; + // We add padding to the AppWidgetHostView if necessary + Rect padding = getDefaultPadding(); + setPadding(padding.left, padding.top, padding.right, padding.bottom); + // Sometimes the AppWidgetManager returns a null AppWidgetProviderInfo object for // a widget, eg. for some widgets in safe mode. if (info != null) { - // We add padding to the AppWidgetHostView if necessary - Rect padding = getDefaultPaddingForWidget(mContext, info.provider, null); - setPadding(padding.left, padding.top, padding.right, padding.bottom); - updateContentDescription(info); + String description = info.loadLabel(getContext().getPackageManager()); + if ((info.providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0) { + description = Resources.getSystem().getString( + com.android.internal.R.string.suspended_widget_accessibility, description); + } + setContentDescription(description); } } @@ -173,23 +165,23 @@ public class AppWidgetHostView extends FrameLayout { */ public static Rect getDefaultPaddingForWidget(Context context, ComponentName component, Rect padding) { - PackageManager packageManager = context.getPackageManager(); - ApplicationInfo appInfo; + ApplicationInfo appInfo = null; + try { + appInfo = context.getPackageManager().getApplicationInfo(component.getPackageName(), 0); + } catch (NameNotFoundException e) { + // if we can't find the package, ignore + } + return getDefaultPaddingForWidget(context, appInfo, padding); + } + private static Rect getDefaultPaddingForWidget(Context context, ApplicationInfo appInfo, + Rect padding) { if (padding == null) { padding = new Rect(0, 0, 0, 0); } else { padding.set(0, 0, 0, 0); } - - try { - appInfo = packageManager.getApplicationInfo(component.getPackageName(), 0); - } catch (NameNotFoundException e) { - // if we can't find the package, return 0 padding - return padding; - } - - if (appInfo.targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (appInfo != null && appInfo.targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { Resources r = context.getResources(); padding.left = r.getDimensionPixelSize(com.android.internal. R.dimen.default_app_widget_padding_left); @@ -203,6 +195,11 @@ public class AppWidgetHostView extends FrameLayout { return padding; } + private Rect getDefaultPadding() { + return getDefaultPaddingForWidget(mContext, + mInfo == null ? null : mInfo.providerInfo.applicationInfo, null); + } + public int getAppWidgetId() { return mAppWidgetId; } @@ -213,9 +210,12 @@ public class AppWidgetHostView extends FrameLayout { @Override protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { - final ParcelableSparseArray jail = new ParcelableSparseArray(); + final SparseArray<Parcelable> jail = new SparseArray<>(); super.dispatchSaveInstanceState(jail); - container.put(generateId(), jail); + + Bundle bundle = new Bundle(); + bundle.putSparseParcelableArray(KEY_JAILED_ARRAY, jail); + container.put(generateId(), bundle); } private int generateId() { @@ -227,12 +227,12 @@ public class AppWidgetHostView extends FrameLayout { protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { final Parcelable parcelable = container.get(generateId()); - ParcelableSparseArray jail = null; - if (parcelable != null && parcelable instanceof ParcelableSparseArray) { - jail = (ParcelableSparseArray) parcelable; + SparseArray<Parcelable> jail = null; + if (parcelable instanceof Bundle) { + jail = ((Bundle) parcelable).getSparseParcelableArray(KEY_JAILED_ARRAY); } - if (jail == null) jail = new ParcelableSparseArray(); + if (jail == null) jail = new SparseArray<>(); try { super.dispatchRestoreInstanceState(jail); @@ -290,10 +290,7 @@ public class AppWidgetHostView extends FrameLayout { newOptions = new Bundle(); } - Rect padding = new Rect(); - if (mInfo != null) { - padding = getDefaultPaddingForWidget(mContext, mInfo.provider, padding); - } + Rect padding = getDefaultPadding(); float density = getResources().getDisplayMetrics().density; int xPaddingDips = (int) ((padding.left + padding.right) / density); @@ -367,7 +364,7 @@ public class AppWidgetHostView extends FrameLayout { * initial layout. */ void resetAppWidget(AppWidgetProviderInfo info) { - mInfo = info; + setAppWidget(mAppWidgetId, info); mViewMode = VIEW_MODE_NOINIT; updateAppWidget(null); } @@ -384,31 +381,10 @@ public class AppWidgetHostView extends FrameLayout { * @hide */ protected void applyRemoteViews(RemoteViews remoteViews, boolean useAsyncIfPossible) { - if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); - boolean recycled = false; View content = null; Exception exception = null; - // Capture the old view into a bitmap so we can do the crossfade. - if (CROSSFADE) { - if (mFadeStartTime < 0) { - if (mView != null) { - final int width = mView.getWidth(); - final int height = mView.getHeight(); - try { - mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - } catch (OutOfMemoryError e) { - // we just won't do the fade - mOld = null; - } - if (mOld != null) { - //mView.drawIntoBitmap(mOld); - } - } - } - } - if (mLastExecutionSignal != null) { mLastExecutionSignal.cancel(); mLastExecutionSignal = null; @@ -460,7 +436,6 @@ public class AppWidgetHostView extends FrameLayout { } applyContent(content, recycled, exception); - updateContentDescription(mInfo); } private void applyContent(View content, boolean recycled, Exception exception) { @@ -469,7 +444,9 @@ public class AppWidgetHostView extends FrameLayout { // We've already done this -- nothing to do. return ; } - Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception); + if (exception != null) { + Log.w(TAG, "Error inflating RemoteViews : " + exception.toString()); + } content = getErrorView(); mViewMode = VIEW_MODE_ERROR; } @@ -483,37 +460,6 @@ public class AppWidgetHostView extends FrameLayout { removeView(mView); mView = content; } - - if (CROSSFADE) { - if (mFadeStartTime < 0) { - // if there is already an animation in progress, don't do anything -- - // the new view will pop in on top of the old one during the cross fade, - // and that looks okay. - mFadeStartTime = SystemClock.uptimeMillis(); - invalidate(); - } - } - } - - private void updateContentDescription(AppWidgetProviderInfo info) { - if (info != null) { - LauncherApps launcherApps = getContext().getSystemService(LauncherApps.class); - ApplicationInfo appInfo = null; - try { - appInfo = launcherApps.getApplicationInfo( - info.provider.getPackageName(), 0, info.getProfile()); - } catch (NameNotFoundException e) { - // ignore -- use null. - } - if (appInfo != null && - (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0) { - setContentDescription( - Resources.getSystem().getString( - com.android.internal.R.string.suspended_widget_accessibility, info.label)); - } else { - setContentDescription(info.label); - } - } } private void inflateAsync(RemoteViews remoteViews) { @@ -616,45 +562,6 @@ public class AppWidgetHostView extends FrameLayout { } } - @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - if (CROSSFADE) { - int alpha; - int l = child.getLeft(); - int t = child.getTop(); - if (mFadeStartTime > 0) { - alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); - if (alpha > 255) { - alpha = 255; - } - Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t - + " w=" + child.getWidth()); - if (alpha != 255 && mOld != null) { - mOldPaint.setAlpha(255-alpha); - //canvas.drawBitmap(mOld, l, t, mOldPaint); - } - } else { - alpha = 255; - } - int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, - Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); - boolean rv = super.drawChild(canvas, child, drawingTime); - canvas.restoreToCount(restoreTo); - if (alpha < 255) { - invalidate(); - } else { - mFadeStartTime = -1; - if (mOld != null) { - mOld.recycle(); - mOld = null; - } - } - return rv; - } else { - return super.drawChild(canvas, child, drawingTime); - } - } - /** * Prepare the given view to be shown. This might include adjusting * {@link FrameLayout.LayoutParams} before inserting. @@ -688,7 +595,7 @@ public class AppWidgetHostView extends FrameLayout { LayoutInflater inflater = (LayoutInflater) theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater = inflater.cloneInContext(theirContext); - inflater.setFilter(sInflaterFilter); + inflater.setFilter(INFLATER_FILTER); AppWidgetManager manager = AppWidgetManager.getInstance(mContext); Bundle options = manager.getAppWidgetOptions(mAppWidgetId); @@ -739,36 +646,4 @@ public class AppWidgetHostView extends FrameLayout { super.onInitializeAccessibilityNodeInfoInternal(info); info.setClassName(AppWidgetHostView.class.getName()); } - - private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable { - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - final int count = size(); - dest.writeInt(count); - for (int i = 0; i < count; i++) { - dest.writeInt(keyAt(i)); - dest.writeParcelable(valueAt(i), 0); - } - } - - public static final Parcelable.Creator<ParcelableSparseArray> CREATOR = - new Parcelable.Creator<ParcelableSparseArray>() { - public ParcelableSparseArray createFromParcel(Parcel source) { - final ParcelableSparseArray array = new ParcelableSparseArray(); - final ClassLoader loader = array.getClass().getClassLoader(); - final int count = source.readInt(); - for (int i = 0; i < count; i++) { - array.put(source.readInt(), source.readParcelable(loader)); - } - return array; - } - - public ParcelableSparseArray[] newArray(int size) { - return new ParcelableSparseArray[size]; - } - }; - } } diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 969b19ee48ac..37bb6b05c3e7 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -20,17 +20,19 @@ import android.annotation.BroadcastBehavior; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; -import android.annotation.SystemService; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SystemService; +import android.app.IServiceConnection; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; +import android.content.ServiceConnection; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; import android.os.Bundle; -import android.os.IBinder; +import android.os.Handler; import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; @@ -1051,43 +1053,23 @@ public class AppWidgetManager { * The appWidgetId specified must already be bound to the calling AppWidgetHost via * {@link android.appwidget.AppWidgetManager#bindAppWidgetId AppWidgetManager.bindAppWidgetId()}. * - * @param packageName The package from which the binding is requested. * @param appWidgetId The AppWidget instance for which to bind the RemoteViewsService. * @param intent The intent of the service which will be providing the data to the * RemoteViewsAdapter. * @param connection The callback interface to be notified when a connection is made or lost. - * @hide - */ - public void bindRemoteViewsService(String packageName, int appWidgetId, Intent intent, - IBinder connection) { - if (mService == null) { - return; - } - try { - mService.bindRemoteViewsService(packageName, appWidgetId, intent, connection); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** - * Unbinds the RemoteViewsService for a given appWidgetId and intent. - * - * The appWidgetId specified muse already be bound to the calling AppWidgetHost via - * {@link android.appwidget.AppWidgetManager#bindAppWidgetId AppWidgetManager.bindAppWidgetId()}. + * @param flags Flags used for binding to the service * - * @param packageName The package from which the binding is requested. - * @param appWidgetId The AppWidget instance for which to bind the RemoteViewsService. - * @param intent The intent of the service which will be providing the data to the - * RemoteViewsAdapter. + * @see Context#getServiceDispatcher(ServiceConnection, Handler, int) * @hide */ - public void unbindRemoteViewsService(String packageName, int appWidgetId, Intent intent) { + public boolean bindRemoteViewsService(Context context, int appWidgetId, Intent intent, + IServiceConnection connection, @Context.BindServiceFlags int flags) { if (mService == null) { - return; + return false; } try { - mService.unbindRemoteViewsService(packageName, appWidgetId, intent); + return mService.bindRemoteViewsService(context.getOpPackageName(), appWidgetId, intent, + context.getIApplicationThread(), context.getActivityToken(), connection, flags); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/appwidget/AppWidgetManagerInternal.java b/core/java/android/appwidget/AppWidgetManagerInternal.java new file mode 100644 index 000000000000..7ab3d8bdd857 --- /dev/null +++ b/core/java/android/appwidget/AppWidgetManagerInternal.java @@ -0,0 +1,39 @@ +/* + * 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.appwidget; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArraySet; + +import java.util.Set; + +/** + * App widget manager local system service interface. + * + * @hide Only for use within the system server. + */ +public abstract class AppWidgetManagerInternal { + + /** + * Gets the packages from which the uid hosts widgets. + * + * @param uid The potential host UID. + * @return Whether the UID hosts widgets from the package. + */ + public abstract @Nullable ArraySet<String> getHostedWidgetPackages(int uid); +} diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java index fd1b0e0274b1..75ce4fbb60c7 100644 --- a/core/java/android/appwidget/AppWidgetProviderInfo.java +++ b/core/java/android/appwidget/AppWidgetProviderInfo.java @@ -17,15 +17,17 @@ package android.appwidget; import android.annotation.NonNull; +import android.app.PendingIntent; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.ResourceId; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.content.ComponentName; import android.os.UserHandle; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -69,6 +71,23 @@ public class AppWidgetProviderInfo implements Parcelable { public static final int WIDGET_CATEGORY_SEARCHBOX = 4; /** + * The widget can be reconfigured anytime after it is bound by starting the + * {@link #configure} activity. + * + * @see #widgetFeatures + */ + public static final int WIDGET_FEATURE_RECONFIGURABLE = 1; + + /** + * The widget is added directly by the app, and the host may hide this widget when providing + * the user with the list of available widgets to choose from. + * + * @see AppWidgetManager#requestPinAppWidget(ComponentName, Bundle, PendingIntent) + * @see #widgetFeatures + */ + public static final int WIDGET_FEATURE_HIDE_FROM_PICKER = 2; + + /** * Identity of this AppWidget component. This component should be a {@link * android.content.BroadcastReceiver}, and it will be sent the AppWidget intents * {@link android.appwidget as described in the AppWidget package documentation}. @@ -209,6 +228,15 @@ public class AppWidgetProviderInfo implements Parcelable { */ public int widgetCategory; + /** + * Flags indicating various features supported by the widget. These are hints to the widget + * host, and do not actually change the behavior of the widget. + * + * @see #WIDGET_FEATURE_RECONFIGURABLE + * @see #WIDGET_FEATURE_HIDE_FROM_PICKER + */ + public int widgetFeatures; + /** @hide */ public ActivityInfo providerInfo; @@ -221,9 +249,7 @@ public class AppWidgetProviderInfo implements Parcelable { */ @SuppressWarnings("deprecation") public AppWidgetProviderInfo(Parcel in) { - if (0 != in.readInt()) { - this.provider = new ComponentName(in); - } + this.provider = in.readTypedObject(ComponentName.CREATOR); this.minWidth = in.readInt(); this.minHeight = in.readInt(); this.minResizeWidth = in.readInt(); @@ -231,16 +257,15 @@ public class AppWidgetProviderInfo implements Parcelable { this.updatePeriodMillis = in.readInt(); this.initialLayout = in.readInt(); this.initialKeyguardLayout = in.readInt(); - if (0 != in.readInt()) { - this.configure = new ComponentName(in); - } + this.configure = in.readTypedObject(ComponentName.CREATOR); this.label = in.readString(); this.icon = in.readInt(); this.previewImage = in.readInt(); this.autoAdvanceViewId = in.readInt(); this.resizeMode = in.readInt(); this.widgetCategory = in.readInt(); - this.providerInfo = in.readParcelable(null); + this.providerInfo = in.readTypedObject(ActivityInfo.CREATOR); + this.widgetFeatures = in.readInt(); } /** @@ -308,13 +333,8 @@ public class AppWidgetProviderInfo implements Parcelable { @Override @SuppressWarnings("deprecation") - public void writeToParcel(android.os.Parcel out, int flags) { - if (this.provider != null) { - out.writeInt(1); - this.provider.writeToParcel(out, flags); - } else { - out.writeInt(0); - } + public void writeToParcel(Parcel out, int flags) { + out.writeTypedObject(this.provider, flags); out.writeInt(this.minWidth); out.writeInt(this.minHeight); out.writeInt(this.minResizeWidth); @@ -322,19 +342,15 @@ public class AppWidgetProviderInfo implements Parcelable { out.writeInt(this.updatePeriodMillis); out.writeInt(this.initialLayout); out.writeInt(this.initialKeyguardLayout); - if (this.configure != null) { - out.writeInt(1); - this.configure.writeToParcel(out, flags); - } else { - out.writeInt(0); - } + out.writeTypedObject(this.configure, flags); out.writeString(this.label); out.writeInt(this.icon); out.writeInt(this.previewImage); out.writeInt(this.autoAdvanceViewId); out.writeInt(this.resizeMode); out.writeInt(this.widgetCategory); - out.writeParcelable(this.providerInfo, flags); + out.writeTypedObject(this.providerInfo, flags); + out.writeInt(this.widgetFeatures); } @Override @@ -357,6 +373,7 @@ public class AppWidgetProviderInfo implements Parcelable { that.resizeMode = this.resizeMode; that.widgetCategory = this.widgetCategory; that.providerInfo = this.providerInfo; + that.widgetFeatures = this.widgetFeatures; return that; } diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 158aebb478ac..c7be0f36ecef 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -2425,6 +2425,8 @@ public final class BluetoothAdapter { * * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean enableNoAutoConnect() { if (isEnabled()) { if (DBG) { diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index d982bb7ffb43..ad7a93cd6bbd 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -1098,6 +1098,8 @@ public final class BluetoothDevice implements Parcelable { * @return true on success, false on error * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean cancelBondProcess() { final IBluetooth service = sService; if (service == null) { @@ -1125,6 +1127,8 @@ public final class BluetoothDevice implements Parcelable { * @return true on success, false on error * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean removeBond() { final IBluetooth service = sService; if (service == null) { @@ -1174,6 +1178,7 @@ public final class BluetoothDevice implements Parcelable { * @hide */ @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH) public boolean isConnected() { final IBluetooth service = sService; if (service == null) { @@ -1197,6 +1202,7 @@ public final class BluetoothDevice implements Parcelable { * @hide */ @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH) public boolean isEncrypted() { final IBluetooth service = sService; if (service == null) { @@ -1444,6 +1450,8 @@ public final class BluetoothDevice implements Parcelable { * @return Whether the value has been successfully set. * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean setPhonebookAccessPermission(int value) { final IBluetooth service = sService; if (service == null) { diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index 90ae0e6313dc..838d3153d54c 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -16,8 +16,10 @@ package android.bluetooth; +import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SystemApi; import android.content.ComponentName; import android.content.Context; import android.os.Binder; @@ -416,6 +418,8 @@ public final class BluetoothHeadset implements BluetoothProfile { * @return false on immediate error, true otherwise * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean connect(BluetoothDevice device) { if (DBG) log("connect(" + device + ")"); final IBluetoothHeadset service = mService; @@ -456,6 +460,8 @@ public final class BluetoothHeadset implements BluetoothProfile { * @return false on immediate error, true otherwise * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean disconnect(BluetoothDevice device) { if (DBG) log("disconnect(" + device + ")"); final IBluetoothHeadset service = mService; @@ -543,6 +549,8 @@ public final class BluetoothHeadset implements BluetoothProfile { * @return true if priority is set, false on error * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean setPriority(BluetoothDevice device, int priority) { if (DBG) log("setPriority(" + device + ", " + priority + ")"); final IBluetoothHeadset service = mService; diff --git a/core/java/android/bluetooth/BluetoothHidDevice.java b/core/java/android/bluetooth/BluetoothHidDevice.java index 330a0bfad329..4a0048673c28 100644 --- a/core/java/android/bluetooth/BluetoothHidDevice.java +++ b/core/java/android/bluetooth/BluetoothHidDevice.java @@ -79,7 +79,7 @@ public final class BluetoothHidDevice implements BluetoothProfile { public static final byte SUBCLASS2_GAMEPAD = (byte) 0x02; public static final byte SUBCLASS2_REMOTE_CONTROL = (byte) 0x03; public static final byte SUBCLASS2_SENSING_DEVICE = (byte) 0x04; - public static final byte SUBCLASS2_DIGITIZER_TABLED = (byte) 0x05; + public static final byte SUBCLASS2_DIGITIZER_TABLET = (byte) 0x05; public static final byte SUBCLASS2_CARD_READER = (byte) 0x06; /** diff --git a/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java b/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java index 55c3a730a833..73a2e74de5b3 100644 --- a/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java +++ b/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java @@ -71,7 +71,7 @@ public final class PeriodicAdvertisingReport implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mSyncHandle); - dest.writeLong(mTxPower); + dest.writeInt(mTxPower); dest.writeInt(mRssi); dest.writeInt(mDataStatus); if (mData != null) { diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index b2952aab2334..1a5de5690cae 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -37,6 +37,7 @@ import android.util.Log; import java.util.Collections; import java.util.List; +import java.util.function.BiConsumer; /** * System level service for managing companion devices @@ -271,6 +272,8 @@ public final class CompanionDeviceManager { private Handler mHandler; private AssociationRequest mRequest; + final Object mLock = new Object(); + private CallbackProxy(AssociationRequest request, Callback callback, Handler handler) { mCallback = callback; mHandler = handler; @@ -280,38 +283,44 @@ public final class CompanionDeviceManager { @Override public void onSuccess(PendingIntent launcher) { - Handler handler = mHandler; - if (handler == null) return; - handler.post(() -> { - Callback callback = mCallback; - if (callback == null) return; - callback.onDeviceFound(launcher.getIntentSender()); - }); + lockAndPost(Callback::onDeviceFound, launcher.getIntentSender()); } @Override public void onFailure(CharSequence reason) { - Handler handler = mHandler; - if (handler == null) return; - handler.post(() -> { - Callback callback = mCallback; - if (callback == null) return; - callback.onFailure(reason); - }); + lockAndPost(Callback::onFailure, reason); + } + + <T> void lockAndPost(BiConsumer<Callback, T> action, T payload) { + synchronized (mLock) { + if (mHandler != null) { + mHandler.post(() -> { + Callback callback = null; + synchronized (mLock) { + callback = mCallback; + } + if (callback != null) { + action.accept(callback, payload); + } + }); + } + } } @Override public void onActivityDestroyed(Activity activity) { - if (activity != getActivity()) return; - try { - mService.stopScan(mRequest, this, getCallingPackage()); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); + synchronized (mLock) { + if (activity != getActivity()) return; + try { + mService.stopScan(mRequest, this, getCallingPackage()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + getActivity().getApplication().unregisterActivityLifecycleCallbacks(this); + mCallback = null; + mHandler = null; + mRequest = null; } - getActivity().getApplication().unregisterActivityLifecycleCallbacks(this); - mCallback = null; - mHandler = null; - mRequest = null; } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java index b7545bf55b58..6e9f09cbef0e 100644 --- a/core/java/android/content/AsyncTaskLoader.java +++ b/core/java/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/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java index ea6b7690b431..0d36bddc8665 100644 --- a/core/java/android/content/ComponentName.java +++ b/core/java/android/content/ComponentName.java @@ -21,9 +21,9 @@ import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; -import java.lang.Comparable; /** * Identifier for a specific application component @@ -33,7 +33,7 @@ import java.lang.Comparable; * pieces of information, encapsulated here, are required to identify * a component: the package (a String) it exists in, and the class (a String) * name inside of that package. - * + * */ public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> { private final String mPackage; @@ -91,7 +91,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co /** * Create a new component identifier. - * + * * @param pkg The name of the package that the component exists in. Can * not be null. * @param cls The name of the class inside of <var>pkg</var> that @@ -106,7 +106,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co /** * Create a new component identifier from a Context and class name. - * + * * @param pkg A Context for the package implementing the component, * from which the actual package name will be retrieved. * @param cls The name of the class inside of <var>pkg</var> that @@ -120,7 +120,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co /** * Create a new component identifier from a Context and Class object. - * + * * @param pkg A Context for the package implementing the component, from * which the actual package name will be retrieved. * @param cls The Class object of the desired component, from which the @@ -141,14 +141,14 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co public @NonNull String getPackageName() { return mPackage; } - + /** * Return the class name of this component. */ public @NonNull String getClassName() { return mClass; } - + /** * Return the class name, either fully qualified or in a shortened form * (with a leading '.') if it is a suffix of the package. @@ -163,7 +163,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co } return mClass; } - + private static void appendShortClassName(StringBuilder sb, String packageName, String className) { if (className.startsWith(packageName)) { @@ -195,26 +195,26 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co * class names contained in the ComponentName. You can later recover * the ComponentName from this string through * {@link #unflattenFromString(String)}. - * + * * @return Returns a new String holding the package and class names. This * is represented as the package name, concatenated with a '/' and then the * class name. - * + * * @see #unflattenFromString(String) */ public @NonNull String flattenToString() { return mPackage + "/" + mClass; } - + /** * The same as {@link #flattenToString()}, but abbreviates the class * name if it is a suffix of the package. The result can still be used * with {@link #unflattenFromString(String)}. - * + * * @return Returns a new String holding the package and class names. This * is represented as the package name, concatenated with a '/' and then the * class name. - * + * * @see #unflattenFromString(String) */ public @NonNull String flattenToShortString() { @@ -250,11 +250,11 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co * followed by a '.' then the final class name will be the concatenation * of the package name with the string following the '/'. Thus * "com.foo/.Blah" becomes package="com.foo" class="com.foo.Blah". - * + * * @param str The String that was returned by flattenToString(). * @return Returns a new ComponentName containing the package and class * names that were encoded in <var>str</var> - * + * * @see #flattenToString() */ public static @Nullable ComponentName unflattenFromString(@NonNull String str) { @@ -269,7 +269,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co } return new ComponentName(pkg, cls); } - + /** * Return string representation of this class without the class's name * as a prefix. @@ -283,6 +283,12 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co return "ComponentInfo{" + mPackage + "/" + mClass + "}"; } + /** Put this here so that individual services don't have to reimplement this. @hide */ + public void toProto(ProtoOutputStream proto) { + proto.write(ComponentNameProto.PACKAGE_NAME, mPackage); + proto.write(ComponentNameProto.CLASS_NAME, mClass); + } + @Override public boolean equals(Object obj) { try { @@ -311,7 +317,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co } return this.mClass.compareTo(that.mClass); } - + public int describeContents() { return 0; } @@ -324,10 +330,10 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co /** * Write a ComponentName to a Parcel, handling null pointers. Must be * read with {@link #readFromParcel(Parcel)}. - * + * * @param c The ComponentName to be written. * @param out The Parcel in which the ComponentName will be placed. - * + * * @see #readFromParcel(Parcel) */ public static void writeToParcel(ComponentName c, Parcel out) { @@ -337,23 +343,23 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co out.writeString(null); } } - + /** * Read a ComponentName from a Parcel that was previously written * with {@link #writeToParcel(ComponentName, Parcel)}, returning either * a null or new object as appropriate. - * + * * @param in The Parcel from which to read the ComponentName * @return Returns a new ComponentName matching the previously written * object, or null if a null had been written. - * + * * @see #writeToParcel(ComponentName, Parcel) */ public static ComponentName readFromParcel(Parcel in) { String pkg = in.readString(); return pkg != null ? new ComponentName(pkg, in) : null; } - + public static final Parcelable.Creator<ComponentName> CREATOR = new Parcelable.Creator<ComponentName>() { public ComponentName createFromParcel(Parcel in) { @@ -371,7 +377,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co * must not use this with data written by * {@link #writeToParcel(ComponentName, Parcel)} since it is not possible * to handle a null ComponentObject here. - * + * * @param in The Parcel containing the previously written ComponentName, * positioned at the location in the buffer where it was written. */ diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index f8c139feaae3..2d490a03bd76 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/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/core/java/android/content/Context.java b/core/java/android/content/Context.java index 1d651b9da864..a47433093988 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2801,10 +2801,17 @@ public abstract class Context { * example, if this Context is an Activity that is stopped, the service will * not be required to continue running until the Activity is resumed. * - * <p>This function will throw {@link SecurityException} if you do not + * <p>If the service does not support binding, it may return {@code null} from + * its {@link android.app.Service#onBind(Intent) onBind()} method. If it does, then + * the ServiceConnection's + * {@link ServiceConnection#onNullBinding(ComponentName) onNullBinding()} method + * will be invoked instead of + * {@link ServiceConnection#onServiceConnected(ComponentName, IBinder) onServiceConnected()}. + * + * <p>This method will throw {@link SecurityException} if the calling app does not * have permission to bind to the given service. * - * <p class="note">Note: this method <em>can not be called from a + * <p class="note">Note: this method <em>cannot be called from a * {@link BroadcastReceiver} component</em>. A pattern you can use to * communicate from a BroadcastReceiver to a Service is to call * {@link #startService} with the arguments containing the command to be @@ -2827,8 +2834,8 @@ public abstract class Context { * {@link #BIND_WAIVE_PRIORITY}. * @return If you have successfully bound to the service, {@code true} is returned; * {@code false} is returned if the connection is not made so you will not - * receive the service object. However, you should still call - * {@link #unbindService} to release the connection. + * receive the service object. You should still call {@link #unbindService} + * to release the connection even if this method returned {@code false}. * * @throws SecurityException If the caller does not have permission to access the service * or the service can not be found. @@ -2906,7 +2913,7 @@ public abstract class Context { @Nullable String profileFile, @Nullable Bundle arguments); /** @hide */ - @StringDef({ + @StringDef(suffix = { "_SERVICE" }, value = { POWER_SERVICE, WINDOW_SERVICE, LAYOUT_INFLATER_SERVICE, @@ -2940,7 +2947,7 @@ public abstract class Context { //@hide: LOWPAN_SERVICE, //@hide: WIFI_RTT_SERVICE, //@hide: ETHERNET_SERVICE, - WIFI_RTT_SERVICE, + WIFI_RTT_RANGING_SERVICE, NSD_SERVICE, AUDIO_SERVICE, FINGERPRINT_SERVICE, @@ -2994,6 +3001,7 @@ public abstract class Context { //@hide: CONTEXTHUB_SERVICE, SYSTEM_HEALTH_SERVICE, //@hide: INCIDENT_SERVICE, + //@hide: STATS_COMPANION_SERVICE, COMPANION_DEVICE_SERVICE }) @Retention(RetentionPolicy.SOURCE) @@ -3405,6 +3413,14 @@ public abstract class Context { public static final String NETWORKMANAGEMENT_SERVICE = "network_management"; /** + * Use with {@link #getSystemService} to retrieve a + * {@link com.android.server.slice.SliceManagerService} for managing slices. + * @hide + * @see #getSystemService + */ + public static final String SLICE_SERVICE = "slice"; + + /** * Use with {@link #getSystemService} to retrieve a {@link * android.app.usage.NetworkStatsManager} for querying network usage stats. * @@ -3414,6 +3430,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 @@ -3469,6 +3487,19 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a {@link + * android.net.wifi.rtt.WifiRttManager} for ranging devices with wifi + * + * Note: this is a replacement for WIFI_RTT_SERVICE above. It will + * be renamed once final implementation in place. + * + * @see #getSystemService + * @see android.net.wifi.rtt.WifiRttManager + * @hide + */ + public static final String WIFI_RTT_RANGING_SERVICE = "rttmanager2"; + + /** + * Use with {@link #getSystemService} to retrieve a {@link * android.net.lowpan.LowpanManager} for handling management of * LoWPAN access. * @@ -3592,7 +3623,6 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a - * {@link android.text.ClipboardManager} for accessing and modifying * {@link android.content.ClipboardManager} for accessing and modifying * the contents of the global clipboard. * @@ -4025,6 +4055,19 @@ public abstract class Context { public static final String INCIDENT_SERVICE = "incident"; /** + * Service to assist statsd in obtaining general stats. + * @hide + */ + 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. * @@ -4054,6 +4097,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/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java index c78871c30a80..7f24c51d37a1 100644 --- a/core/java/android/content/CursorLoader.java +++ b/core/java/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; @@ -142,7 +145,7 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> { } /** - * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * Starts an asynchronous load of the data. When the result is ready the callbacks * will be called on the UI thread. If a previous load has been completed and is still valid * the result may be passed to the callbacks immediately. * diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 8ff6699e81d1..6fb1afde22ec 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -53,6 +53,7 @@ import android.provider.OpenableColumns; import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; +import android.util.proto.ProtoOutputStream; import com.android.internal.util.XmlUtils; @@ -1727,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"; @@ -3443,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} */ @@ -4450,12 +4455,36 @@ public class Intent implements Parcelable, Cloneable { public static final String EXTRA_EPHEMERAL_TOKEN = "android.intent.extra.EPHEMERAL_TOKEN"; /** + * The action that triggered an instant application resolution. + * @hide + */ + public static final String EXTRA_INSTANT_APP_ACTION = "android.intent.extra.INSTANT_APP_ACTION"; + + /** + * A {@link Bundle} of metadata that describes the instanta application that needs to be + * installed. This data is populated from the response to + * {@link android.content.pm.InstantAppResolveInfo#getExtras()} as provided by the registered + * instant application resolver. + * @hide + */ + public static final String EXTRA_INSTANT_APP_EXTRAS = + "android.intent.extra.INSTANT_APP_EXTRAS"; + + /** * The version code of the app to install components from. + * @deprecated Use {@link #EXTRA_LONG_VERSION_CODE). * @hide */ + @Deprecated public static final String EXTRA_VERSION_CODE = "android.intent.extra.VERSION_CODE"; /** + * The version code of the app to install components from. + * @hide + */ + public static final String EXTRA_LONG_VERSION_CODE = "android.intent.extra.LONG_VERSION_CODE"; + + /** * The app that triggered the ephemeral installation. * @hide */ @@ -4826,7 +4855,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"; /** @@ -4880,8 +4915,9 @@ public class Intent implements Parcelable, Cloneable { * <li>Enumeration of features here is not meant to restrict capabilities of the quick viewer. * Quick viewer can implement features not listed below. * <li>Features included at this time are: {@link QuickViewConstants#FEATURE_VIEW}, - * {@link QuickViewConstants#FEATURE_EDIT}, {@link QuickViewConstants#FEATURE_DOWNLOAD}, - * {@link QuickViewConstants#FEATURE_SEND}, {@link QuickViewConstants#FEATURE_PRINT}. + * {@link QuickViewConstants#FEATURE_EDIT}, {@link QuickViewConstants#FEATURE_DELETE}, + * {@link QuickViewConstants#FEATURE_DOWNLOAD}, {@link QuickViewConstants#FEATURE_SEND}, + * {@link QuickViewConstants#FEATURE_PRINT}. * <p> * Requirements: * <li>Quick viewer shouldn't show a feature if the feature is absent in @@ -9371,6 +9407,57 @@ public class Intent implements Parcelable, Cloneable { } } + /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId, boolean secure, boolean comp, + boolean extras, boolean clip) { + long token = proto.start(fieldId); + if (mAction != null) { + proto.write(IntentProto.ACTION, mAction); + } + if (mCategories != null) { + for (String category : mCategories) { + proto.write(IntentProto.CATEGORIES, category); + } + } + if (mData != null) { + proto.write(IntentProto.DATA, secure ? mData.toSafeString() : mData.toString()); + } + if (mType != null) { + proto.write(IntentProto.TYPE, mType); + } + if (mFlags != 0) { + proto.write(IntentProto.FLAG, "0x" + Integer.toHexString(mFlags)); + } + if (mPackage != null) { + proto.write(IntentProto.PACKAGE, mPackage); + } + if (comp && mComponent != null) { + proto.write(IntentProto.COMPONENT, mComponent.flattenToShortString()); + } + if (mSourceBounds != null) { + proto.write(IntentProto.SOURCE_BOUNDS, mSourceBounds.toShortString()); + } + if (mClipData != null) { + StringBuilder b = new StringBuilder(); + if (clip) { + mClipData.toShortString(b); + } else { + mClipData.toShortStringShortItems(b, false); + } + proto.write(IntentProto.CLIP_DATA, b.toString()); + } + if (extras && mExtras != null) { + proto.write(IntentProto.EXTRAS, mExtras.toShortString()); + } + if (mContentUserHint != 0) { + proto.write(IntentProto.CONTENT_USER_HINT, mContentUserHint); + } + if (mSelector != null) { + proto.write(IntentProto.SELECTOR, mSelector.toShortString(secure, comp, extras, clip)); + } + proto.end(token); + } + /** * Call {@link #toUri} with 0 flags. * @deprecated Use {@link #toUri} instead. @@ -10071,6 +10158,27 @@ public class Intent implements Parcelable, Cloneable { return false; } + /** + * Convert the dock state to a human readable format. + * @hide + */ + public static String dockStateToString(int dock) { + switch (dock) { + case EXTRA_DOCK_STATE_HE_DESK: + return "EXTRA_DOCK_STATE_HE_DESK"; + case EXTRA_DOCK_STATE_LE_DESK: + return "EXTRA_DOCK_STATE_LE_DESK"; + case EXTRA_DOCK_STATE_CAR: + return "EXTRA_DOCK_STATE_CAR"; + case EXTRA_DOCK_STATE_DESK: + return "EXTRA_DOCK_STATE_DESK"; + case EXTRA_DOCK_STATE_UNDOCKED: + return "EXTRA_DOCK_STATE_UNDOCKED"; + default: + return Integer.toString(dock); + } + } + private static ClipData.Item makeClipItem(ArrayList<Uri> streams, ArrayList<CharSequence> texts, ArrayList<String> htmlTexts, int which) { Uri uri = streams != null ? streams.get(which) : null; diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index c9bce530be3e..a957aed8b806 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -26,6 +26,7 @@ import android.text.TextUtils; import android.util.AndroidException; import android.util.Log; import android.util.Printer; +import android.util.proto.ProtoOutputStream; import com.android.internal.util.XmlUtils; @@ -918,6 +919,15 @@ public class IntentFilter implements Parcelable { dest.writeInt(mPort); } + void writeToProto(ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + // The original host information is already contained in host and wild, no output now. + proto.write(AuthorityEntryProto.HOST, mHost); + proto.write(AuthorityEntryProto.WILD, mWild); + proto.write(AuthorityEntryProto.PORT, mPort); + proto.end(token); + } + public String getHost() { return mOrigHost; } @@ -1739,6 +1749,59 @@ public class IntentFilter implements Parcelable { } } + /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + if (mActions.size() > 0) { + Iterator<String> it = mActions.iterator(); + while (it.hasNext()) { + proto.write(IntentFilterProto.ACTIONS, it.next()); + } + } + if (mCategories != null) { + Iterator<String> it = mCategories.iterator(); + while (it.hasNext()) { + proto.write(IntentFilterProto.CATEGORIES, it.next()); + } + } + if (mDataSchemes != null) { + Iterator<String> it = mDataSchemes.iterator(); + while (it.hasNext()) { + proto.write(IntentFilterProto.DATA_SCHEMES, it.next()); + } + } + if (mDataSchemeSpecificParts != null) { + Iterator<PatternMatcher> it = mDataSchemeSpecificParts.iterator(); + while (it.hasNext()) { + it.next().writeToProto(proto, IntentFilterProto.DATA_SCHEME_SPECS); + } + } + if (mDataAuthorities != null) { + Iterator<AuthorityEntry> it = mDataAuthorities.iterator(); + while (it.hasNext()) { + it.next().writeToProto(proto, IntentFilterProto.DATA_AUTHORITIES); + } + } + if (mDataPaths != null) { + Iterator<PatternMatcher> it = mDataPaths.iterator(); + while (it.hasNext()) { + it.next().writeToProto(proto, IntentFilterProto.DATA_PATHS); + } + } + if (mDataTypes != null) { + Iterator<String> it = mDataTypes.iterator(); + while (it.hasNext()) { + proto.write(IntentFilterProto.DATA_TYPES, it.next()); + } + } + if (mPriority != 0 || mHasPartialTypes) { + proto.write(IntentFilterProto.PRIORITY, mPriority); + proto.write(IntentFilterProto.HAS_PARTIAL_TYPES, mHasPartialTypes); + } + proto.write(IntentFilterProto.GET_AUTO_VERIFY, getAutoVerify()); + proto.end(token); + } + public void dump(Printer du, String prefix) { StringBuilder sb = new StringBuilder(256); if (mActions.size() > 0) { diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java index 3faf13b601ad..80f9a14c6e18 100644 --- a/core/java/android/content/Loader.java +++ b/core/java/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/core/java/android/content/QuickViewConstants.java b/core/java/android/content/QuickViewConstants.java index 7455d0cb0944..a25513de3d47 100644 --- a/core/java/android/content/QuickViewConstants.java +++ b/core/java/android/content/QuickViewConstants.java @@ -33,7 +33,7 @@ public class QuickViewConstants { public static final String FEATURE_VIEW = "android:view"; /** - * Feature to view a document using system standard editing mechanism, like + * Feature to edit a document using system standard editing mechanism, like * {@link Intent#ACTION_EDIT}. * * @see Intent#EXTRA_QUICK_VIEW_FEATURES @@ -42,6 +42,15 @@ public class QuickViewConstants { public static final String FEATURE_EDIT = "android:edit"; /** + * Feature to delete an individual document. Quick viewer implementations must use + * Storage Access Framework to both verify delete permission and to delete content. + * + * @see DocumentsContract#Document#FLAG_SUPPORTS_DELETE + * @see DocumentsContract#deleteDocument(ContentResolver resolver, Uri documentUri) + */ + public static final String FEATURE_DELETE = "android:delete"; + + /** * Feature to view a document using system standard sending mechanism, like * {@link Intent#ACTION_SEND}. * diff --git a/core/java/android/content/ServiceConnection.java b/core/java/android/content/ServiceConnection.java index 6ff4900212ff..c16dbbe33aab 100644 --- a/core/java/android/content/ServiceConnection.java +++ b/core/java/android/content/ServiceConnection.java @@ -63,4 +63,21 @@ public interface ServiceConnection { */ default void onBindingDied(ComponentName name) { } + + /** + * Called when the service being bound has returned {@code null} from its + * {@link android.app.Service#onBind(Intent) onBind()} method. This indicates + * that the attempting service binding represented by this ServiceConnection + * will never become usable. + * + * <p class="note">The app which requested the binding must still call + * {@link Context#unbindService(ServiceConnection)} to release the tracking + * resources associated with this ServiceConnection even if this callback was + * invoked following {@link Context#bindService Context.bindService() bindService()}. + * + * @param name The concrete component name of the service whose binding + * has been rejected by the Service implementation. + */ + default void onNullBinding(ComponentName name) { + } } diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java index 4b09feda2173..d3652e8825b5 100644 --- a/core/java/android/content/SharedPreferences.java +++ b/core/java/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/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index e0c3f75e223e..f8cdce64c139 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -17,9 +17,11 @@ package android.content.pm; import android.annotation.IntDef; +import android.annotation.TestApi; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Configuration.NativeConfig; +import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; import android.util.Printer; @@ -33,8 +35,7 @@ import java.lang.annotation.RetentionPolicy; * from the AndroidManifest.xml's <activity> and * <receiver> tags. */ -public class ActivityInfo extends ComponentInfo - implements Parcelable { +public class ActivityInfo extends ComponentInfo implements Parcelable { // NOTE: When adding new data members be sure to update the copy-constructor, Parcel // constructor, and writeToParcel. @@ -180,6 +181,7 @@ public class ActivityInfo extends ComponentInfo * Activity explicitly requested to be resizeable. * @hide */ + @TestApi public static final int RESIZE_MODE_RESIZEABLE = 2; /** * Activity is resizeable and supported picture-in-picture mode. This flag is now deprecated @@ -778,6 +780,13 @@ public class ActivityInfo extends ComponentInfo * constant starts at the high bits. */ public static final int CONFIG_FONT_SCALE = 0x40000000; + /** + * Bit indicating changes to window configuration that isn't exposed to apps. + * This is for internal use only and apps don't handle it. + * @hide + * {@link Configuration}. + */ + public static final int CONFIG_WINDOW_CONFIGURATION = 0x20000000; /** @hide * Unfortunately the constants for config changes in native code are @@ -1147,6 +1156,7 @@ public class ActivityInfo extends ComponentInfo dest.writeString(permission); dest.writeString(taskAffinity); dest.writeString(targetActivity); + dest.writeString(launchToken); dest.writeInt(flags); dest.writeInt(screenOrientation); dest.writeInt(configChanges); @@ -1175,6 +1185,86 @@ public class ActivityInfo extends ComponentInfo dest.writeFloat(maxAspectRatio); } + /** + * Determines whether the {@link Activity} is considered translucent or floating. + * @hide + */ + public static boolean isTranslucentOrFloating(TypedArray attributes) { + final boolean isTranslucent = + attributes.getBoolean(com.android.internal.R.styleable.Window_windowIsTranslucent, + false); + final boolean isSwipeToDismiss = !attributes.hasValue( + com.android.internal.R.styleable.Window_windowIsTranslucent) + && attributes.getBoolean( + com.android.internal.R.styleable.Window_windowSwipeToDismiss, false); + final boolean isFloating = + attributes.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, + false); + + return isFloating || isTranslucent || isSwipeToDismiss; + } + + /** + * Convert the screen orientation constant to a human readable format. + * @hide + */ + public static String screenOrientationToString(int orientation) { + switch (orientation) { + case SCREEN_ORIENTATION_UNSET: + return "SCREEN_ORIENTATION_UNSET"; + case SCREEN_ORIENTATION_UNSPECIFIED: + return "SCREEN_ORIENTATION_UNSPECIFIED"; + case SCREEN_ORIENTATION_LANDSCAPE: + return "SCREEN_ORIENTATION_LANDSCAPE"; + case SCREEN_ORIENTATION_PORTRAIT: + return "SCREEN_ORIENTATION_PORTRAIT"; + case SCREEN_ORIENTATION_USER: + return "SCREEN_ORIENTATION_USER"; + case SCREEN_ORIENTATION_BEHIND: + return "SCREEN_ORIENTATION_BEHIND"; + case SCREEN_ORIENTATION_SENSOR: + return "SCREEN_ORIENTATION_SENSOR"; + case SCREEN_ORIENTATION_NOSENSOR: + return "SCREEN_ORIENTATION_NOSENSOR"; + case SCREEN_ORIENTATION_SENSOR_LANDSCAPE: + return "SCREEN_ORIENTATION_SENSOR_LANDSCAPE"; + case SCREEN_ORIENTATION_SENSOR_PORTRAIT: + return "SCREEN_ORIENTATION_SENSOR_PORTRAIT"; + case SCREEN_ORIENTATION_REVERSE_LANDSCAPE: + return "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"; + case SCREEN_ORIENTATION_REVERSE_PORTRAIT: + return "SCREEN_ORIENTATION_REVERSE_PORTRAIT"; + case SCREEN_ORIENTATION_FULL_SENSOR: + return "SCREEN_ORIENTATION_FULL_SENSOR"; + case SCREEN_ORIENTATION_USER_LANDSCAPE: + return "SCREEN_ORIENTATION_USER_LANDSCAPE"; + case SCREEN_ORIENTATION_USER_PORTRAIT: + return "SCREEN_ORIENTATION_USER_PORTRAIT"; + case SCREEN_ORIENTATION_FULL_USER: + return "SCREEN_ORIENTATION_FULL_USER"; + case SCREEN_ORIENTATION_LOCKED: + return "SCREEN_ORIENTATION_LOCKED"; + default: + return Integer.toString(orientation); + } + } + + /** + * @hide + */ + public static String colorModeToString(@ColorMode int colorMode) { + switch (colorMode) { + case COLOR_MODE_DEFAULT: + return "COLOR_MODE_DEFAULT"; + case COLOR_MODE_WIDE_COLOR_GAMUT: + return "COLOR_MODE_WIDE_COLOR_GAMUT"; + case COLOR_MODE_HDR: + return "COLOR_MODE_HDR"; + default: + return Integer.toString(colorMode); + } + } + public static final Parcelable.Creator<ActivityInfo> CREATOR = new Parcelable.Creator<ActivityInfo>() { public ActivityInfo createFromParcel(Parcel source) { @@ -1193,6 +1283,7 @@ public class ActivityInfo extends ComponentInfo permission = source.readString(); taskAffinity = source.readString(); targetActivity = source.readString(); + launchToken = source.readString(); flags = source.readInt(); screenOrientation = source.readInt(); configChanges = source.readInt(); diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 664bcbca6aba..84b1ff3219a2 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -19,6 +19,7 @@ package android.content.pm; import static android.os.Build.VERSION_CODES.DONUT; import android.annotation.IntDef; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.Context; @@ -375,7 +376,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * {@code DownloadManager}, {@code MediaPlayer}) will refuse app's requests to use cleartext * traffic. Third-party libraries are encouraged to honor this flag as well. * - * <p>NOTE: {@code WebView} does not honor this flag. + * <p>NOTE: {@code WebView} honors this flag for applications targeting API level 26 and up. * * <p>This flag is ignored on Android N and above if an Android Network Security Config is * present. @@ -586,24 +587,40 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public static final int PRIVATE_FLAG_VIRTUAL_PRELOAD = 1 << 16; + /** + * Value for {@linl #privateFlags}: whether this app is pre-installed on the + * OEM partition of the system image. + * @hide + */ + public static final int PRIVATE_FLAG_OEM = 1 << 17; + + /** + * Value for {@linl #privateFlags}: whether this app is pre-installed on the + * vendor partition of the system image. + * @hide + */ + public static final int PRIVATE_FLAG_VENDOR = 1 << 18; + /** @hide */ @IntDef(flag = true, prefix = { "PRIVATE_FLAG_" }, value = { - PRIVATE_FLAG_HIDDEN, + PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE, + PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION, + PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE, + PRIVATE_FLAG_BACKUP_IN_FOREGROUND, PRIVATE_FLAG_CANT_SAVE_STATE, - PRIVATE_FLAG_FORWARD_LOCK, - PRIVATE_FLAG_PRIVILEGED, - PRIVATE_FLAG_HAS_DOMAIN_URLS, PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE, PRIVATE_FLAG_DIRECT_BOOT_AWARE, + PRIVATE_FLAG_FORWARD_LOCK, + PRIVATE_FLAG_HAS_DOMAIN_URLS, + PRIVATE_FLAG_HIDDEN, PRIVATE_FLAG_INSTANT, + PRIVATE_FLAG_ISOLATED_SPLIT_LOADING, + PRIVATE_FLAG_OEM, PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE, + PRIVATE_FLAG_PRIVILEGED, PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER, - PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE, - PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE, - PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION, - PRIVATE_FLAG_BACKUP_IN_FOREGROUND, PRIVATE_FLAG_STATIC_SHARED_LIBRARY, - PRIVATE_FLAG_ISOLATED_SPLIT_LOADING, + PRIVATE_FLAG_VENDOR, PRIVATE_FLAG_VIRTUAL_PRELOAD, }) @Retention(RetentionPolicy.SOURCE) @@ -879,7 +896,30 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * The app's declared version code. * @hide */ - public int versionCode; + public long versionCode; + + /** + * The user-visible SDK version (ex. 26) of the framework against which the application claims + * to have been compiled, or {@code 0} if not specified. + * <p> + * This property is the compile-time equivalent of + * {@link android.os.Build.VERSION#CODENAME Build.VERSION.SDK_INT}. + * + * @hide For platform use only; we don't expect developers to need to read this value. + */ + public int compileSdkVersion; + + /** + * The development codename (ex. "O", "REL") of the framework against which the application + * claims to have been compiled, or {@code null} if not specified. + * <p> + * This property is the compile-time equivalent of + * {@link android.os.Build.VERSION#CODENAME Build.VERSION.CODENAME}. + * + * @hide For platform use only; we don't expect developers to need to read this value. + */ + @Nullable + public String compileSdkVersionCodename; /** * When false, indicates that all components within this application are @@ -1283,7 +1323,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(uid); dest.writeInt(minSdkVersion); dest.writeInt(targetSdkVersion); - dest.writeInt(versionCode); + dest.writeLong(versionCode); dest.writeInt(enabled ? 1 : 0); dest.writeInt(enabledSetting); dest.writeInt(installLocation); @@ -1297,6 +1337,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(targetSandboxVersion); dest.writeString(classLoaderName); dest.writeStringArray(splitClassLoaderNames); + dest.writeInt(compileSdkVersion); + dest.writeString(compileSdkVersionCodename); } public static final Parcelable.Creator<ApplicationInfo> CREATOR @@ -1350,7 +1392,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { uid = source.readInt(); minSdkVersion = source.readInt(); targetSdkVersion = source.readInt(); - versionCode = source.readInt(); + versionCode = source.readLong(); enabled = source.readInt() != 0; enabledSetting = source.readInt(); installLocation = source.readInt(); @@ -1364,6 +1406,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { targetSandboxVersion = source.readInt(); classLoaderName = source.readString(); splitClassLoaderNames = source.readStringArray(); + compileSdkVersion = source.readInt(); + compileSdkVersionCodename = source.readString(); } /** @@ -1455,98 +1499,89 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } } - /** - * @hide - */ - public boolean isForwardLocked() { - return (privateFlags & ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK) != 0; + /** @hide */ + public boolean isDefaultToDeviceProtectedStorage() { + return (privateFlags + & ApplicationInfo.PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE) != 0; } - /** - * @hide - */ - @TestApi - public boolean isSystemApp() { - return (flags & ApplicationInfo.FLAG_SYSTEM) != 0; + /** @hide */ + public boolean isDirectBootAware() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_DIRECT_BOOT_AWARE) != 0; } - /** - * @hide - */ - @TestApi - public boolean isPrivilegedApp() { - return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0; + /** @hide */ + public boolean isEncryptionAware() { + return isDirectBootAware() || isPartiallyDirectBootAware(); } - /** - * @hide - */ - public boolean isUpdatedSystemApp() { - return (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; + /** @hide */ + public boolean isExternal() { + return (flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; } /** @hide */ - public boolean isInternal() { - return (flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) == 0; + public boolean isExternalAsec() { + return TextUtils.isEmpty(volumeUuid) && isExternal(); } /** @hide */ - public boolean isExternalAsec() { - return TextUtils.isEmpty(volumeUuid) - && (flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; + public boolean isForwardLocked() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK) != 0; } /** @hide */ - public boolean isDefaultToDeviceProtectedStorage() { - return (privateFlags - & ApplicationInfo.PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE) != 0; + public boolean isInstantApp() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_INSTANT) != 0; } /** @hide */ - public boolean isDirectBootAware() { - return (privateFlags & ApplicationInfo.PRIVATE_FLAG_DIRECT_BOOT_AWARE) != 0; + public boolean isInternal() { + return (flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) == 0; } /** @hide */ - public boolean isPartiallyDirectBootAware() { - return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE) != 0; + public boolean isOem() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0; } /** @hide */ - public boolean isEncryptionAware() { - return isDirectBootAware() || isPartiallyDirectBootAware(); + public boolean isPartiallyDirectBootAware() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE) != 0; } - /** - * @hide - */ - public boolean isInstantApp() { - return (privateFlags & ApplicationInfo.PRIVATE_FLAG_INSTANT) != 0; + /** @hide */ + @TestApi + public boolean isPrivilegedApp() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0; } - /** - * @hide - */ + /** @hide */ public boolean isRequiredForSystemUser() { return (privateFlags & ApplicationInfo.PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER) != 0; } - /** - * Returns true if the app has declared in its manifest that it wants its split APKs to be - * loaded into isolated Contexts, with their own ClassLoaders and Resources objects. - * @hide - */ - public boolean requestsIsolatedSplitLoading() { - return (privateFlags & ApplicationInfo.PRIVATE_FLAG_ISOLATED_SPLIT_LOADING) != 0; - } - - /** - * @hide - */ + /** @hide */ public boolean isStaticSharedLibrary() { return (privateFlags & ApplicationInfo.PRIVATE_FLAG_STATIC_SHARED_LIBRARY) != 0; } + /** @hide */ + @TestApi + public boolean isSystemApp() { + return (flags & ApplicationInfo.FLAG_SYSTEM) != 0; + } + + /** @hide */ + public boolean isUpdatedSystemApp() { + return (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0; + } + + /** @hide */ + public boolean isVendor() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0; + } + /** * Returns whether or not this application was installed as a virtual preload. */ @@ -1555,6 +1590,15 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } /** + * Returns true if the app has declared in its manifest that it wants its split APKs to be + * loaded into isolated Contexts, with their own ClassLoaders and Resources objects. + * @hide + */ + public boolean requestsIsolatedSplitLoading() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_ISOLATED_SPLIT_LOADING) != 0; + } + + /** * @hide */ @Override protected ApplicationInfo getApplicationInfo() { diff --git a/core/java/android/content/pm/AuxiliaryResolveInfo.java b/core/java/android/content/pm/AuxiliaryResolveInfo.java index 067363d43adf..6bdcefbe974e 100644 --- a/core/java/android/content/pm/AuxiliaryResolveInfo.java +++ b/core/java/android/content/pm/AuxiliaryResolveInfo.java @@ -45,7 +45,7 @@ public final class AuxiliaryResolveInfo extends IntentFilter { /** Opaque token to track the instant application resolution */ public final String token; /** The version code of the package */ - public final int versionCode; + public final long versionCode; /** An intent to start upon failure to install */ public final Intent failureIntent; @@ -71,7 +71,7 @@ public final class AuxiliaryResolveInfo extends IntentFilter { public AuxiliaryResolveInfo(@NonNull String packageName, @Nullable String splitName, @Nullable ComponentName failureActivity, - int versionCode, + long versionCode, @Nullable Intent failureIntent) { super(); this.packageName = packageName; diff --git a/core/java/android/content/pm/FeatureInfo.java b/core/java/android/content/pm/FeatureInfo.java index 9ee6fa2431a3..ff9fd8ec31c5 100644 --- a/core/java/android/content/pm/FeatureInfo.java +++ b/core/java/android/content/pm/FeatureInfo.java @@ -18,6 +18,7 @@ package android.content.pm; import android.os.Parcel; import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; /** * Definition of a single optional hardware or software feature of an Android @@ -113,6 +114,18 @@ public class FeatureInfo implements Parcelable { dest.writeInt(flags); } + /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + if (name != null) { + proto.write(FeatureInfoProto.NAME, name); + } + proto.write(FeatureInfoProto.VERSION, version); + proto.write(FeatureInfoProto.GLES_VERSION, getGlEsVersion()); + proto.write(FeatureInfoProto.FLAGS, flags); + proto.end(token); + } + public static final Creator<FeatureInfo> CREATOR = new Creator<FeatureInfo>() { @Override public FeatureInfo createFromParcel(Parcel source) { diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 0e706456b9b2..56a0def27602 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -48,6 +48,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.content.pm.VerifierDeviceIdentity; import android.content.pm.VersionedPackage; +import android.content.pm.dex.IArtManager; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; @@ -543,9 +544,10 @@ interface IPackageManager { void forceDexOpt(String packageName); /** - * Execute the background dexopt job immediately. + * Execute the background dexopt job immediately on packages in packageNames. + * If null, then execute on all packages. */ - boolean runBackgroundDexoptJob(); + boolean runBackgroundDexoptJob(in List<String> packageNames); /** * Reconcile the information we have about the secondary dex files belonging to @@ -554,14 +556,6 @@ interface IPackageManager { */ void reconcileSecondaryDexFiles(String packageName); - /** - * Update status of external media on the package manager to scan and - * install packages installed on the external media. Like say the - * StorageManagerService uses this to call into the package manager to update - * status of sdcard. - */ - void updateExternalMediaStatus(boolean mounted, boolean reportStatus); - PackageCleanItem nextPackageToClean(in PackageCleanItem lastPackage); int getMoveStatus(int moveId); @@ -664,4 +658,6 @@ interface IPackageManager { ComponentName getInstantAppInstallerComponent(); String getInstantAppAndroidId(String packageName, int userId); + + IArtManager getArtManager(); } diff --git a/core/java/android/content/pm/InstantAppResolveInfo.java b/core/java/android/content/pm/InstantAppResolveInfo.java index 22e994f4cfa8..19cb9323ba93 100644 --- a/core/java/android/content/pm/InstantAppResolveInfo.java +++ b/core/java/android/content/pm/InstantAppResolveInfo.java @@ -19,8 +19,7 @@ package android.content.pm; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; -import android.content.IntentFilter; -import android.net.Uri; +import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -44,10 +43,18 @@ public final class InstantAppResolveInfo implements Parcelable { /** The filters used to match domain */ private final List<InstantAppIntentFilter> mFilters; /** The version code of the app that this class resolves to */ - private final int mVersionCode; + private final long mVersionCode; + /** Data about the app that should be passed along to the Instant App installer on resolve */ + private final Bundle mExtras; public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters, int versionCode) { + this(digest, packageName, filters, (long) versionCode, null /* extras */); + } + + public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName, + @Nullable List<InstantAppIntentFilter> filters, long versionCode, + @Nullable Bundle extras) { // validate arguments if ((packageName == null && (filters != null && filters.size() != 0)) || (packageName != null && (filters == null || filters.size() == 0))) { @@ -62,11 +69,13 @@ public final class InstantAppResolveInfo implements Parcelable { } mPackageName = packageName; mVersionCode = versionCode; + mExtras = extras; } public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName, @Nullable List<InstantAppIntentFilter> filters) { - this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/); + this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/, + null /* extras */); } InstantAppResolveInfo(Parcel in) { @@ -74,7 +83,8 @@ public final class InstantAppResolveInfo implements Parcelable { mPackageName = in.readString(); mFilters = new ArrayList<InstantAppIntentFilter>(); in.readList(mFilters, null /*loader*/); - mVersionCode = in.readInt(); + mVersionCode = in.readLong(); + mExtras = in.readBundle(); } public byte[] getDigestBytes() { @@ -93,10 +103,23 @@ public final class InstantAppResolveInfo implements Parcelable { return mFilters; } + /** + * @deprecated Use {@link #getLongVersionCode} instead. + */ + @Deprecated public int getVersionCode() { + return (int) (mVersionCode & 0xffffffff); + } + + public long getLongVersionCode() { return mVersionCode; } + @Nullable + public Bundle getExtras() { + return mExtras; + } + @Override public int describeContents() { return 0; @@ -107,7 +130,8 @@ public final class InstantAppResolveInfo implements Parcelable { out.writeParcelable(mDigest, flags); out.writeString(mPackageName); out.writeList(mFilters); - out.writeInt(mVersionCode); + out.writeLong(mVersionCode); + out.writeBundle(mExtras); } public static final Parcelable.Creator<InstantAppResolveInfo> CREATOR diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index aa9562ff040f..d09ba0b55a3c 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -20,8 +20,8 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; -import android.annotation.SystemService; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SystemService; import android.annotation.TestApi; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; @@ -37,10 +37,10 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Rect; +import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; -import android.graphics.drawable.AdaptiveIconDrawable; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -265,6 +265,14 @@ public class LauncherApps { /** * Include pinned shortcuts in the result. + * + * <p>If you are the selected assistant app, and wishes to fetch all shortcuts that the + * user owns on the launcher (or by other launchers, in case the user has multiple), use + * {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} instead. + * + * <p>If you're a regular launcher app, there's no way to get shortcuts pinned by other + * launchers, and {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} will be ignored. So use this + * flag to get own pinned shortcuts. */ public static final int FLAG_MATCH_PINNED = 1 << 1; @@ -282,12 +290,34 @@ public class LauncherApps { public static final int FLAG_GET_MANIFEST = FLAG_MATCH_MANIFEST; /** - * Does not retrieve CHOOSER only shortcuts. - * TODO: Add another flag for MATCH_ALL_PINNED + * Include all pinned shortcuts by any launchers, not just by the caller, + * in the result. + * + * <p>The caller must be the selected assistant app to use this flag, or have the system + * {@code ACCESS_SHORTCUTS} permission. + * + * <p>If you are the selected assistant app, and wishes to fetch all shortcuts that the + * user owns on the launcher (or by other launchers, in case the user has multiple), use + * {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} instead. + * + * <p>If you're a regular launcher app (or any app that's not the selected assistant app) + * then this flag will be ignored. + */ + public static final int FLAG_MATCH_PINNED_BY_ANY_LAUNCHER = 1 << 10; + + /** + * FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED | FLAG_MATCH_MANIFEST * @hide */ public static final int FLAG_MATCH_ALL_KINDS = - FLAG_GET_DYNAMIC | FLAG_GET_PINNED | FLAG_GET_MANIFEST; + FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED | FLAG_MATCH_MANIFEST; + + /** + * FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED | FLAG_MATCH_MANIFEST | FLAG_MATCH_ALL_PINNED + * @hide + */ + public static final int FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED = + FLAG_MATCH_ALL_KINDS | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER; /** @hide kept for unit tests */ @Deprecated @@ -319,6 +349,7 @@ public class LauncherApps { FLAG_MATCH_PINNED, FLAG_MATCH_MANIFEST, FLAG_GET_KEY_FIELDS_ONLY, + FLAG_MATCH_MANIFEST, }) @Retention(RetentionPolicy.SOURCE) public @interface QueryFlags {} @@ -655,9 +686,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 @@ -678,6 +713,21 @@ public class LauncherApps { } } + private List<ShortcutInfo> maybeUpdateDisabledMessage(List<ShortcutInfo> shortcuts) { + if (shortcuts == null) { + return null; + } + for (int i = shortcuts.size() - 1; i >= 0; i--) { + final ShortcutInfo si = shortcuts.get(i); + final String message = ShortcutInfo.getDisabledReasonForRestoreIssue(mContext, + si.getDisabledReason()); + if (message != null) { + si.setDisabledMessage(message); + } + } + return shortcuts; + } + /** * Returns {@link ShortcutInfo}s that match {@code query}. * @@ -698,10 +748,16 @@ public class LauncherApps { @NonNull UserHandle user) { logErrorForInvalidProfileAccess(user); try { - return mService.getShortcuts(mContext.getPackageName(), + // Note this is the only case we need to update the disabled message for shortcuts + // that weren't restored. + // The restore problem messages are only shown by the user, and publishers will never + // see them. The only other API that the launcher gets shortcuts is the shortcut + // changed callback, but that only returns shortcuts with the "key" information, so + // that won't return disabled message. + return maybeUpdateDisabledMessage(mService.getShortcuts(mContext.getPackageName(), query.mChangedSince, query.mPackage, query.mShortcutIds, query.mActivity, query.mQueryFlags, user) - .getList(); + .getList()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/content/pm/PackageBackwardCompatibility.java b/core/java/android/content/pm/PackageBackwardCompatibility.java index 4de160b0bf88..cee25994a271 100644 --- a/core/java/android/content/pm/PackageBackwardCompatibility.java +++ b/core/java/android/content/pm/PackageBackwardCompatibility.java @@ -16,8 +16,8 @@ package android.content.pm; -import android.annotation.Nullable; import android.content.pm.PackageParser.Package; +import android.os.Build; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -36,6 +36,8 @@ public class PackageBackwardCompatibility { private static final String ANDROID_TEST_RUNNER = "android.test.runner"; + private static final String APACHE_HTTP_LEGACY = "org.apache.http.legacy"; + /** * Modify the shared libraries in the supplied {@link Package} to maintain backwards * compatibility. @@ -47,13 +49,21 @@ public class PackageBackwardCompatibility { ArrayList<String> usesLibraries = pkg.usesLibraries; ArrayList<String> usesOptionalLibraries = pkg.usesOptionalLibraries; - usesLibraries = orgApacheHttpLegacy(usesLibraries); - usesOptionalLibraries = orgApacheHttpLegacy(usesOptionalLibraries); + // Packages targeted at <= O_MR1 expect the classes in the org.apache.http.legacy library + // to be accessible so this maintains backward compatibility by adding the + // org.apache.http.legacy library to those packages. + if (apkTargetsApiLevelLessThanOrEqualToOMR1(pkg)) { + boolean apacheHttpLegacyPresent = isLibraryPresent( + usesLibraries, usesOptionalLibraries, APACHE_HTTP_LEGACY); + if (!apacheHttpLegacyPresent) { + usesLibraries = ArrayUtils.add(usesLibraries, APACHE_HTTP_LEGACY); + } + } // android.test.runner has a dependency on android.test.mock so if android.test.runner // is present but android.test.mock is not then add android.test.mock. - boolean androidTestMockPresent = ArrayUtils.contains(usesLibraries, ANDROID_TEST_MOCK) - || ArrayUtils.contains(usesOptionalLibraries, ANDROID_TEST_MOCK); + boolean androidTestMockPresent = isLibraryPresent( + usesLibraries, usesOptionalLibraries, ANDROID_TEST_MOCK); if (ArrayUtils.contains(usesLibraries, ANDROID_TEST_RUNNER) && !androidTestMockPresent) { usesLibraries.add(ANDROID_TEST_MOCK); } @@ -66,13 +76,14 @@ public class PackageBackwardCompatibility { pkg.usesOptionalLibraries = usesOptionalLibraries; } - private static ArrayList<String> orgApacheHttpLegacy(@Nullable ArrayList<String> libraries) { - // "org.apache.http.legacy" is now a part of the boot classpath so it doesn't need - // to be an explicit dependency. - // - // A future change will remove this library from the boot classpath, at which point - // all apps that target SDK 21 and earlier will have it automatically added to their - // dependency lists. - return ArrayUtils.remove(libraries, "org.apache.http.legacy"); + private static boolean apkTargetsApiLevelLessThanOrEqualToOMR1(Package pkg) { + int targetSdkVersion = pkg.applicationInfo.targetSdkVersion; + return targetSdkVersion <= Build.VERSION_CODES.O_MR1; + } + + private static boolean isLibraryPresent(ArrayList<String> usesLibraries, + ArrayList<String> usesOptionalLibraries, String apacheHttpLegacy) { + return ArrayUtils.contains(usesLibraries, apacheHttpLegacy) + || ArrayUtils.contains(usesOptionalLibraries, apacheHttpLegacy); } } diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 3230ee715571..5a91e94781d7 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -16,9 +16,14 @@ package android.content.pm; +import android.annotation.IntDef; +import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Overall information about the contents of a package. This corresponds * to all of the information collected from AndroidManifest.xml. @@ -36,13 +41,56 @@ public class PackageInfo implements Parcelable { public String[] splitNames; /** + * @deprecated Use {@link #getLongVersionCode()} instead, which includes both + * this and the additional + * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} attribute. * The version number of this package, as specified by the <manifest> * tag's {@link android.R.styleable#AndroidManifest_versionCode versionCode} * attribute. + * @see #getLongVersionCode() */ + @Deprecated public int versionCode; /** + * @hide + * The major version number of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifest_versionCode versionCodeMajor} + * attribute. + * @see #getLongVersionCode() + */ + public int versionCodeMajor; + + /** + * Return {@link android.R.styleable#AndroidManifest_versionCode versionCode} and + * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} combined + * together as a single long value. The + * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} is placed in + * the upper 32 bits. + */ + public long getLongVersionCode() { + return composeLongVersionCode(versionCodeMajor, versionCode); + } + + /** + * Set the full version code in this PackageInfo, updating {@link #versionCode} + * with the lower bits. + * @see #getLongVersionCode() + */ + public void setLongVersionCode(long longVersionCode) { + versionCodeMajor = (int) (longVersionCode>>32); + versionCode = (int) longVersionCode; + } + + /** + * @hide Internal implementation for composing a minor and major version code in to + * a single long version code. + */ + public static long composeLongVersionCode(int major, int minor) { + return (((long) major) << 32) | (((long) minor) & 0xffffffffL); + } + + /** * The version name of this package, as specified by the <manifest> * tag's {@link android.R.styleable#AndroidManifest_versionName versionName} * attribute. @@ -286,30 +334,73 @@ public class PackageInfo implements Parcelable { /** @hide */ public int overlayPriority; - /** - * Flag for use with {@link #overlayFlags}. Marks the overlay as static, meaning it cannot + * Flag for use with {@link #mOverlayFlags}. Marks the overlay as static, meaning it cannot * be enabled/disabled at runtime. - * @hide */ - public static final int FLAG_OVERLAY_STATIC = 1 << 1; + static final int FLAG_OVERLAY_STATIC = 1 << 1; /** - * Flag for use with {@link #overlayFlags}. Marks the overlay as trusted (not 3rd party). - * @hide + * Flag for use with {@link #mOverlayFlags}. Marks the overlay as trusted (not 3rd party). */ - public static final int FLAG_OVERLAY_TRUSTED = 1 << 2; + static final int FLAG_OVERLAY_TRUSTED = 1 << 2; + + @IntDef(flag = true, prefix = "FLAG_OVERLAY_", value = { + FLAG_OVERLAY_STATIC, + FLAG_OVERLAY_TRUSTED + }) + @Retention(RetentionPolicy.SOURCE) + @interface OverlayFlags {} /** * Modifiers that affect the state of this overlay. See {@link #FLAG_OVERLAY_STATIC}, * {@link #FLAG_OVERLAY_TRUSTED}. - * @hide */ - public int overlayFlags; + @OverlayFlags int mOverlayFlags; + + /** + * The user-visible SDK version (ex. 26) of the framework against which the application claims + * to have been compiled, or {@code 0} if not specified. + * <p> + * This property is the compile-time equivalent of + * {@link android.os.Build.VERSION#SDK_INT Build.VERSION.SDK_INT}. + * + * @hide For platform use only; we don't expect developers to need to read this value. + */ + public int compileSdkVersion; + + /** + * The development codename (ex. "O", "REL") of the framework against which the application + * claims to have been compiled, or {@code null} if not specified. + * <p> + * This property is the compile-time equivalent of + * {@link android.os.Build.VERSION#CODENAME Build.VERSION.CODENAME}. + * + * @hide For platform use only; we don't expect developers to need to read this value. + */ + @Nullable + public String compileSdkVersionCodename; public PackageInfo() { } + /** + * Returns true if the package is a valid Runtime Overlay package. + * @hide + */ + public boolean isOverlayPackage() { + return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_TRUSTED) != 0; + } + + /** + * Returns true if the package is a valid static Runtime Overlay package. Static overlays + * are not updatable outside of a system update and are safe to load in the system process. + * @hide + */ + public boolean isStaticOverlayPackage() { + return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_STATIC) != 0; + } + @Override public String toString() { return "PackageInfo{" @@ -327,6 +418,7 @@ public class PackageInfo implements Parcelable { dest.writeString(packageName); dest.writeStringArray(splitNames); dest.writeInt(versionCode); + dest.writeInt(versionCodeMajor); dest.writeString(versionName); dest.writeInt(baseRevisionCode); dest.writeIntArray(splitRevisionCodes); @@ -361,7 +453,9 @@ public class PackageInfo implements Parcelable { dest.writeString(requiredAccountType); dest.writeString(overlayTarget); dest.writeInt(overlayPriority); - dest.writeInt(overlayFlags); + dest.writeInt(mOverlayFlags); + dest.writeInt(compileSdkVersion); + dest.writeString(compileSdkVersionCodename); } public static final Parcelable.Creator<PackageInfo> CREATOR @@ -381,6 +475,7 @@ public class PackageInfo implements Parcelable { packageName = source.readString(); splitNames = source.createStringArray(); versionCode = source.readInt(); + versionCodeMajor = source.readInt(); versionName = source.readString(); baseRevisionCode = source.readInt(); splitRevisionCodes = source.createIntArray(); @@ -413,7 +508,9 @@ public class PackageInfo implements Parcelable { requiredAccountType = source.readString(); overlayTarget = source.readString(); overlayPriority = source.readInt(); - overlayFlags = source.readInt(); + mOverlayFlags = source.readInt(); + compileSdkVersion = source.readInt(); + compileSdkVersionCodename = source.readString(); // The component lists were flattened with the redundant ApplicationInfo // instances omitted. Distribute the canonical one here as appropriate. diff --git a/core/java/android/content/pm/PackageInfoLite.java b/core/java/android/content/pm/PackageInfoLite.java index 1efe082b7fdf..bbf020d76c92 100644 --- a/core/java/android/content/pm/PackageInfoLite.java +++ b/core/java/android/content/pm/PackageInfoLite.java @@ -38,9 +38,27 @@ public class PackageInfoLite implements Parcelable { /** * The android:versionCode of the package. + * @deprecated Use {@link #getLongVersionCode()} instead, which includes both + * this and the additional + * {@link android.R.styleable#AndroidManifest_versionCode versionCodeMajor} attribute. */ + @Deprecated public int versionCode; + /** + * @hide + * The android:versionCodeMajor of the package. + */ + public int versionCodeMajor; + + /** + * Return {@link #versionCode} and {@link #versionCodeMajor} combined together as a + * single long value. The {@link #versionCodeMajor} is placed in the upper 32 bits. + */ + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode); + } + /** Revision code of base APK */ public int baseRevisionCode; /** Revision codes of any split APKs, ordered by parsed splitName */ @@ -55,10 +73,10 @@ public class PackageInfoLite implements Parcelable { /** * Specifies the recommended install location. Can be one of - * {@link #PackageHelper.RECOMMEND_INSTALL_INTERNAL} to install on internal storage - * {@link #PackageHelper.RECOMMEND_INSTALL_EXTERNAL} to install on external media - * {@link PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE} for storage errors - * {@link PackageHelper.RECOMMEND_FAILED_INVALID_APK} for parse errors. + * {@link PackageHelper#RECOMMEND_INSTALL_INTERNAL} to install on internal storage, + * {@link PackageHelper#RECOMMEND_INSTALL_EXTERNAL} to install on external media, + * {@link PackageHelper#RECOMMEND_FAILED_INSUFFICIENT_STORAGE} for storage errors, + * or {@link PackageHelper#RECOMMEND_FAILED_INVALID_APK} for parse errors. */ public int recommendedInstallLocation; public int installLocation; @@ -82,6 +100,7 @@ public class PackageInfoLite implements Parcelable { dest.writeString(packageName); dest.writeStringArray(splitNames); dest.writeInt(versionCode); + dest.writeInt(versionCodeMajor); dest.writeInt(baseRevisionCode); dest.writeIntArray(splitRevisionCodes); dest.writeInt(recommendedInstallLocation); @@ -111,6 +130,7 @@ public class PackageInfoLite implements Parcelable { packageName = source.readString(); splitNames = source.createStringArray(); versionCode = source.readInt(); + versionCodeMajor = source.readInt(); baseRevisionCode = source.readInt(); splitRevisionCodes = source.createIntArray(); recommendedInstallLocation = source.readInt(); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 5673361420cc..77c5743fc882 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -81,6 +81,9 @@ import java.util.List; * <li>All APKs must have unique split names. * <li>All installations must contain a single base APK. * </ul> + * <p> + * The ApiDemos project contains examples of using this API: + * <code>ApiDemos/src/com/example/android/apis/content/InstallApk*.java</code>. */ public class PackageInstaller { private static final String TAG = "PackageInstaller"; @@ -444,6 +447,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 +482,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); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index ef8f84bd1690..ff02c4030941 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -42,6 +42,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.pm.PackageParser.PackageParserException; +import android.content.pm.dex.ArtManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.Rect; @@ -871,8 +872,8 @@ public abstract class PackageManager { public static final int INSTALL_REASON_USER = 4; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} on success. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * on success. * * @hide */ @@ -880,8 +881,8 @@ public abstract class PackageManager { public static final int INSTALL_SUCCEEDED = 1; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the package is already installed. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the package is already installed. * * @hide */ @@ -889,8 +890,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_ALREADY_EXISTS = -1; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the package archive file is invalid. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the package archive file is invalid. * * @hide */ @@ -898,8 +899,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_INVALID_APK = -2; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the URI passed in is invalid. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the URI passed in is invalid. * * @hide */ @@ -907,9 +908,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_INVALID_URI = -3; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the package manager service found that - * the device didn't have enough storage space to install the app. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the package manager service found that the device didn't have enough storage space to + * install the app. * * @hide */ @@ -917,9 +918,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if a package is already installed with - * the same name. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if a package is already installed with the same name. * * @hide */ @@ -927,9 +927,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the requested shared user does not - * exist. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the requested shared user does not exist. * * @hide */ @@ -937,10 +936,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_NO_SHARED_USER = -6; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if a previously installed package of the - * same name has a different signature than the new package (and the old - * package's data was not removed). + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if a previously installed package of the same name has a different signature than the new + * package (and the old package's data was not removed). * * @hide */ @@ -948,10 +946,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package is requested a shared - * user which is already installed on the device and does not have matching - * signature. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package is requested a shared user which is already installed on the device and + * does not have matching signature. * * @hide */ @@ -959,9 +956,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package uses a shared library - * that is not available. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package uses a shared library that is not available. * * @hide */ @@ -969,9 +965,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package uses a shared library - * that is not available. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package uses a shared library that is not available. * * @hide */ @@ -979,10 +974,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package failed while - * optimizing and validating its dex files, either because there was not - * enough storage or the validation failed. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package failed while optimizing and validating its dex files, either because there + * was not enough storage or the validation failed. * * @hide */ @@ -990,9 +984,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_DEXOPT = -11; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package failed because the - * current SDK version is older than that required by the package. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package failed because the current SDK version is older than that required by the + * package. * * @hide */ @@ -1000,10 +994,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_OLDER_SDK = -12; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package failed because it - * contains a content provider with the same authority as a provider already - * installed in the system. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package failed because it contains a content provider with the same authority as a + * provider already installed in the system. * * @hide */ @@ -1011,9 +1004,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package failed because the - * current SDK version is newer than that required by the package. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package failed because the current SDK version is newer than that required by the + * package. * * @hide */ @@ -1021,10 +1014,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_NEWER_SDK = -14; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package failed because it has - * specified that it is a test-only package and the caller has not supplied - * the {@link #INSTALL_ALLOW_TEST} flag. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package failed because it has specified that it is a test-only package and the + * caller has not supplied the {@link #INSTALL_ALLOW_TEST} flag. * * @hide */ @@ -1032,9 +1024,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_TEST_ONLY = -15; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the package being installed contains - * native code, but none that is compatible with the device's CPU_ABI. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the package being installed contains native code, but none that is compatible with the + * device's CPU_ABI. * * @hide */ @@ -1042,9 +1034,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package uses a feature that is - * not available. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package uses a feature that is not available. * * @hide */ @@ -1053,9 +1044,9 @@ public abstract class PackageManager { // ------ Errors related to sdcard /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if a secure container mount point - * couldn't be accessed on external media. + * Installation return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if a secure container mount point couldn't be + * accessed on external media. * * @hide */ @@ -1063,9 +1054,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_CONTAINER_ERROR = -18; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package couldn't be installed - * in the specified install location. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package couldn't be installed in the specified install location. * * @hide */ @@ -1073,9 +1063,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package couldn't be installed - * in the specified install location because the media is not available. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package couldn't be installed in the specified install location because the media + * is not available. * * @hide */ @@ -1083,9 +1073,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package couldn't be installed - * because the verification timed out. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package couldn't be installed because the verification timed out. * * @hide */ @@ -1093,9 +1082,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package couldn't be installed - * because the verification did not succeed. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package couldn't be installed because the verification did not succeed. * * @hide */ @@ -1103,9 +1091,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the package changed from what the - * calling program expected. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the package changed from what the calling program expected. * * @hide */ @@ -1113,28 +1100,25 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package is assigned a - * different UID than it previously held. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package is assigned a different UID than it previously held. * * @hide */ public static final int INSTALL_FAILED_UID_CHANGED = -24; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package has an older version - * code than the currently installed package. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package has an older version code than the currently installed package. * * @hide */ public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the old package has target SDK high - * enough to support runtime permission and the new package has target SDK - * low enough to not support runtime permissions. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the old package has target SDK high enough to support runtime permission and the new + * package has target SDK low enough to not support runtime permissions. * * @hide */ @@ -1142,9 +1126,8 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE = -26; /** - * Installation return code: this is passed to the - * {@link IPackageInstallObserver} if the new package attempts to downgrade the - * target sandbox version of the app. + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package attempts to downgrade the target sandbox version of the app. * * @hide */ @@ -1152,9 +1135,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE = -27; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser was given a path that is - * not a file, or does not end with the expected '.apk' extension. + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser was given a path that is not a + * file, or does not end with the expected '.apk' extension. * * @hide */ @@ -1162,8 +1145,8 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_NOT_APK = -100; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser was unable to retrieve the + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser was unable to retrieve the * AndroidManifest.xml file. * * @hide @@ -1172,8 +1155,8 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser encountered an unexpected + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser encountered an unexpected * exception. * * @hide @@ -1182,9 +1165,9 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser did not find any - * certificates in the .apk. + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser did not find any certificates in + * the .apk. * * @hide */ @@ -1192,9 +1175,9 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser found inconsistent - * certificates on the files in the .apk. + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser found inconsistent certificates on + * the files in the .apk. * * @hide */ @@ -1202,8 +1185,8 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser encountered a + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser encountered a * CertificateEncodingException in one of the files in the .apk. * * @hide @@ -1212,9 +1195,9 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser encountered a bad or - * missing package name in the manifest. + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser encountered a bad or missing + * package name in the manifest. * * @hide */ @@ -1222,9 +1205,9 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser encountered a bad shared - * user id name in the manifest. + * Installation parse return code: tthis is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser encountered a bad shared user id + * name in the manifest. * * @hide */ @@ -1232,8 +1215,8 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser encountered some structural + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser encountered some structural * problem in the manifest. * * @hide @@ -1242,9 +1225,9 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108; /** - * Installation parse return code: this is passed to the - * {@link IPackageInstallObserver} if the parser did not find any actionable - * tags (instrumentation or application) in the manifest. + * Installation parse return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the parser did not find any actionable tags + * (instrumentation or application) in the manifest. * * @hide */ @@ -1252,9 +1235,9 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109; /** - * Installation failed return code: this is passed to the - * {@link IPackageInstallObserver} if the system failed to install the - * package because of system issues. + * Installation failed return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the system failed to install the package + * because of system issues. * * @hide */ @@ -1262,24 +1245,23 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; /** - * Installation failed return code: this is passed to the - * {@link IPackageInstallObserver} if the system failed to install the - * package because the user is restricted from installing apps. + * Installation failed return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the system failed to install the package + * because the user is restricted from installing apps. * * @hide */ public static final int INSTALL_FAILED_USER_RESTRICTED = -111; /** - * Installation failed return code: this is passed to the - * {@link IPackageInstallObserver} if the system failed to install the - * package because it is attempting to define a permission that is already - * defined by some existing package. + * Installation failed return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the system failed to install the package + * because it is attempting to define a permission that is already defined by some existing + * package. * <p> - * The package name of the app which has already defined the permission is - * passed to a {@link PackageInstallObserver}, if any, as the - * {@link #EXTRA_FAILURE_EXISTING_PACKAGE} string extra; and the name of the - * permission being redefined is passed in the + * The package name of the app which has already defined the permission is passed to a + * {@link PackageInstallObserver}, if any, as the {@link #EXTRA_FAILURE_EXISTING_PACKAGE} string + * extra; and the name of the permission being redefined is passed in the * {@link #EXTRA_FAILURE_EXISTING_PERMISSION} string extra. * * @hide @@ -1287,10 +1269,9 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112; /** - * Installation failed return code: this is passed to the - * {@link IPackageInstallObserver} if the system failed to install the - * package because its packaged native code did not match any of the ABIs - * supported by the system. + * Installation failed return code: this is passed in the + * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the system failed to install the package + * because its packaged native code did not match any of the ABIs supported by the system. * * @hide */ @@ -1322,6 +1303,7 @@ public abstract class PackageManager { DELETE_ALL_USERS, DELETE_SYSTEM_APP, DELETE_DONT_KILL_APP, + DELETE_CHATTY, }) @Retention(RetentionPolicy.SOURCE) public @interface DeleteFlags {} @@ -1363,6 +1345,14 @@ public abstract class PackageManager { public static final int DELETE_DONT_KILL_APP = 0x00000008; /** + * Flag parameter for {@link #deletePackage} to indicate that package deletion + * should be chatty. + * + * @hide + */ + public static final int DELETE_CHATTY = 0x80000000; + + /** * Return code for when package deletion succeeds. This is passed to the * {@link IPackageDeleteObserver} if the system succeeded in deleting the * package. @@ -2330,6 +2320,16 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: The device supports Wi-Fi RTT (IEEE 802.11mc). + * + * @hide RTT_API + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_WIFI_RTT = "android.hardware.wifi.rtt"; + + + /** + * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device supports LoWPAN networking. * @hide */ @@ -2635,13 +2635,22 @@ public abstract class PackageManager { /** * Extra field name for the version code of a package pending verification. - * + * @deprecated Use {@link #EXTRA_VERIFICATION_LONG_VERSION_CODE} instead. * @hide */ + @Deprecated public static final String EXTRA_VERIFICATION_VERSION_CODE = "android.content.pm.extra.VERIFICATION_VERSION_CODE"; /** + * Extra field name for the long version code of a package pending verification. + * + * @hide + */ + public static final String EXTRA_VERIFICATION_LONG_VERSION_CODE = + "android.content.pm.extra.VERIFICATION_LONG_VERSION_CODE"; + + /** * Extra field name for the ID of a intent filter pending verification. * Passed to an intent filter verifier and is used to call back to * {@link #verifyIntentFilter} @@ -4708,16 +4717,6 @@ public abstract class PackageManager { @Deprecated public abstract void installPackage( Uri packageURI, - IPackageInstallObserver observer, - @InstallFlags int flags, - String installerPackageName); - /** - * @deprecated replaced by {@link PackageInstaller} - * @hide - */ - @Deprecated - public abstract void installPackage( - Uri packageURI, PackageInstallObserver observer, @InstallFlags int flags, String installerPackageName); @@ -5733,25 +5732,6 @@ public abstract class PackageManager { } /** {@hide} */ - public static class LegacyPackageInstallObserver extends PackageInstallObserver { - private final IPackageInstallObserver mLegacy; - - public LegacyPackageInstallObserver(IPackageInstallObserver legacy) { - mLegacy = legacy; - } - - @Override - public void onPackageInstalled(String basePackageName, int returnCode, String msg, - Bundle extras) { - if (mLegacy == null) return; - try { - mLegacy.packageInstalled(basePackageName, returnCode); - } catch (RemoteException ignored) { - } - } - } - - /** {@hide} */ public static class LegacyPackageDeleteObserver extends PackageDeleteObserver { private final IPackageDeleteObserver mLegacy; @@ -5872,4 +5852,14 @@ public abstract class PackageManager { @SystemApi public abstract void registerDexModule(String dexModulePath, @Nullable DexModuleRegisterCallback callback); + + /** + * Returns the {@link ArtManager} associated with this package manager. + * + * @hide + */ + @SystemApi + public @NonNull ArtManager getArtManager() { + throw new UnsupportedOperationException("getArtManager not implemented in subclass"); + } } diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java index 4c981cdb2511..713cd109ef87 100644 --- a/core/java/android/content/pm/PackageManagerInternal.java +++ b/core/java/android/content/pm/PackageManagerInternal.java @@ -16,6 +16,9 @@ package android.content.pm; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager.ApplicationInfoFlags; @@ -25,6 +28,8 @@ import android.content.pm.PackageManager.ResolveInfoFlags; import android.os.Bundle; import android.util.SparseArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -33,6 +38,20 @@ import java.util.List; * @hide Only for use within the system server. */ public abstract class PackageManagerInternal { + public static final int PACKAGE_SYSTEM = 0; + public static final int PACKAGE_SETUP_WIZARD = 1; + public static final int PACKAGE_INSTALLER = 2; + public static final int PACKAGE_VERIFIER = 3; + public static final int PACKAGE_BROWSER = 4; + @IntDef(value = { + PACKAGE_SYSTEM, + PACKAGE_SETUP_WIZARD, + PACKAGE_INSTALLER, + PACKAGE_VERIFIER, + PACKAGE_BROWSER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface KnownPackage {} /** * Provider for package names. @@ -145,6 +164,14 @@ public abstract class PackageManagerInternal { @PackageInfoFlags int flags, int filterCallingUid, int userId); /** + * Do a straight uid lookup for the given package/application in the given user. + * @see PackageManager#getPackageUidAsUser(String, int, int) + * @return The app's uid, or < 0 if the package was not found in that user + */ + public abstract int getPackageUid(String packageName, + @PackageInfoFlags int flags, int userId); + + /** * Retrieve all of the information we know about a particular package/application. * @param filterCallingUid The results will be filtered in the context of this UID instead * of the calling UID. @@ -172,6 +199,13 @@ public abstract class PackageManagerInternal { @ResolveInfoFlags int flags, int filterCallingUid, int userId); /** + * Retrieve all services that can be performed for the given intent. + * @see PackageManager#queryIntentServices(Intent, int) + */ + public abstract List<ResolveInfo> queryIntentServices( + Intent intent, int flags, int callingUid, int userId); + + /** * Interface to {@link com.android.server.pm.PackageManagerService#getHomeActivitiesAsUser}. */ public abstract ComponentName getHomeActivitiesAsUser(List<ResolveInfo> allHomeCandidates, @@ -311,6 +345,12 @@ public abstract class PackageManagerInternal { public abstract boolean isPackagePersistent(String packageName); /** + * Returns whether or not the given package represents a legacy system application released + * prior to runtime permissions. + */ + public abstract boolean isLegacySystemApp(PackageParser.Package pkg); + + /** * Get all overlay packages for a user. * @param userId The user for which to get the overlays. * @return A list of overlay packages. An empty list is returned if the @@ -343,14 +383,19 @@ public abstract class PackageManagerInternal { * Resolves an activity intent, allowing instant apps to be resolved. */ public abstract ResolveInfo resolveIntent(Intent intent, String resolvedType, - int flags, int userId); + int flags, int userId, boolean resolveForStart); /** * Resolves a service intent, allowing instant apps to be resolved. */ - public abstract ResolveInfo resolveService(Intent intent, String resolvedType, + public abstract ResolveInfo resolveService(Intent intent, String resolvedType, int flags, int userId, int callingUid); + /** + * Resolves a content provider intent. + */ + public abstract ProviderInfo resolveContentProvider(String name, int flags, int userId); + /** * Track the creator of a new isolated uid. * @param isolatedUid The newly created isolated uid. @@ -383,4 +428,57 @@ public abstract class PackageManagerInternal { * Updates a package last used time. */ public abstract void notifyPackageUse(String packageName, int reason); + + /** + * Returns a package object for the given package name. + */ + public abstract @Nullable PackageParser.Package getPackage(@NonNull String packageName); + + /** + * Returns a package object for the disabled system package name. + */ + public abstract @Nullable PackageParser.Package getDisabledPackage(@NonNull String packageName); + + /** + * Returns whether or not the component is the resolver activity. + */ + public abstract boolean isResolveActivityComponent(@NonNull ComponentInfo component); + + /** + * Returns the package name for a known package. + */ + public abstract @Nullable String getKnownPackageName( + @KnownPackage int knownPackage, int userId); + + /** + * Returns whether the package is an instant app. + */ + public abstract boolean isInstantApp(String packageName, int userId); + + /** + * Returns whether the package is an instant app. + */ + public abstract @Nullable String getInstantAppPackageName(int uid); + + /** + * Returns whether or not access to the application should be filtered. + * <p> + * Access may be limited based upon whether the calling or target applications + * are instant applications. + * + * @see #canAccessInstantApps(int) + */ + public abstract boolean filterAppAccess( + @Nullable PackageParser.Package pkg, int callingUid, int userId); + + /* + * NOTE: The following methods are temporary until permissions are extracted from + * the package manager into a component specifically for handling permissions. + */ + /** Returns the flags for the given permission. */ + public abstract @Nullable int getPermissionFlagsTEMP(@NonNull String permName, + @NonNull String packageName, int userId); + /** Updates the flags for the given permission. */ + public abstract void updatePermissionFlagsTEMP(@NonNull String permName, + @NonNull String packageName, int flagMask, int flagValues, int userId); } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 4689f45098e2..97c2b7d11354 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -42,6 +42,7 @@ import static android.os.Build.VERSION_CODES.O; import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; +import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -96,16 +97,19 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; import libcore.io.IoUtils; - import libcore.util.EmptyArray; + 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; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; import java.security.GeneralSecurityException; import java.security.KeyFactory; @@ -159,8 +163,6 @@ public class PackageParser { private static final boolean MULTI_PACKAGE_APK_ENABLED = Build.IS_DEBUGGABLE && SystemProperties.getBoolean(PROPERTY_CHILD_PACKAGES_ENABLED, false); - private static final int MAX_PACKAGES_PER_APK = 5; - public static final int APK_SIGNING_UNKNOWN = 0; public static final int APK_SIGNING_V1 = 1; public static final int APK_SIGNING_V2 = 2; @@ -392,6 +394,7 @@ public class PackageParser { public static class PackageLite { public final String packageName; public final int versionCode; + public final int versionCodeMajor; public final int installLocation; public final VerifierInfo[] verifiers; @@ -434,6 +437,7 @@ public class PackageParser { String[] splitCodePaths, int[] splitRevisionCodes) { this.packageName = baseApk.packageName; this.versionCode = baseApk.versionCode; + this.versionCodeMajor = baseApk.versionCodeMajor; this.installLocation = baseApk.installLocation; this.verifiers = baseApk.verifiers; this.splitNames = splitNames; @@ -474,6 +478,7 @@ public class PackageParser { public final String configForSplit; public final String usesSplitName; public final int versionCode; + public final int versionCodeMajor; public final int revisionCode; public final int installLocation; public final VerifierInfo[] verifiers; @@ -487,11 +492,11 @@ public class PackageParser { public final boolean isolatedSplits; public ApkLite(String codePath, String packageName, String splitName, boolean isFeatureSplit, - String configForSplit, String usesSplitName, int versionCode, int revisionCode, - int installLocation, List<VerifierInfo> verifiers, Signature[] signatures, - Certificate[][] certificates, boolean coreApp, boolean debuggable, - boolean multiArch, boolean use32bitAbi, boolean extractNativeLibs, - boolean isolatedSplits) { + String configForSplit, String usesSplitName, int versionCode, int versionCodeMajor, + int revisionCode, int installLocation, List<VerifierInfo> verifiers, + Signature[] signatures, Certificate[][] certificates, boolean coreApp, + boolean debuggable, boolean multiArch, boolean use32bitAbi, + boolean extractNativeLibs, boolean isolatedSplits) { this.codePath = codePath; this.packageName = packageName; this.splitName = splitName; @@ -499,6 +504,7 @@ public class PackageParser { this.configForSplit = configForSplit; this.usesSplitName = usesSplitName; this.versionCode = versionCode; + this.versionCodeMajor = versionCodeMajor; this.revisionCode = revisionCode; this.installLocation = installLocation; this.verifiers = verifiers.toArray(new VerifierInfo[verifiers.size()]); @@ -511,6 +517,10 @@ public class PackageParser { this.extractNativeLibs = extractNativeLibs; this.isolatedSplits = isolatedSplits; } + + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode); + } } /** @@ -661,6 +671,7 @@ public class PackageParser { pi.packageName = p.packageName; pi.splitNames = p.splitNames; pi.versionCode = p.mVersionCode; + pi.versionCodeMajor = p.mVersionCodeMajor; pi.baseRevisionCode = p.baseRevisionCode; pi.splitRevisionCodes = p.splitRevisionCodes; pi.versionName = p.mVersionName; @@ -680,13 +691,15 @@ public class PackageParser { pi.overlayPriority = p.mOverlayPriority; if (p.mIsStaticOverlay) { - pi.overlayFlags |= PackageInfo.FLAG_OVERLAY_STATIC; + pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_STATIC; } if (p.mTrustedOverlay) { - pi.overlayFlags |= PackageInfo.FLAG_OVERLAY_TRUSTED; + pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_TRUSTED; } + pi.compileSdkVersion = p.mCompileSdkVersion; + pi.compileSdkVersionCodename = p.mCompileSdkVersionCodename; pi.firstInstallTime = firstInstallTime; pi.lastUpdateTime = lastUpdateTime; if ((flags&PackageManager.GET_GIDS) != 0) { @@ -825,21 +838,33 @@ public class PackageParser { } } - public final static int PARSE_IS_SYSTEM = 1<<0; - public final static int PARSE_CHATTY = 1<<1; - public final static int PARSE_MUST_BE_APK = 1<<2; - public final static int PARSE_IGNORE_PROCESSES = 1<<3; - public final static int PARSE_FORWARD_LOCK = 1<<4; - public final static int PARSE_EXTERNAL_STORAGE = 1<<5; - public final static int PARSE_IS_SYSTEM_DIR = 1<<6; - public final static int PARSE_IS_PRIVILEGED = 1<<7; - public final static int PARSE_COLLECT_CERTIFICATES = 1<<8; - public final static int PARSE_TRUSTED_OVERLAY = 1<<9; - public final static int PARSE_ENFORCE_CODE = 1<<10; - /** @deprecated remove when fixing b/34761192 */ + public static final int PARSE_MUST_BE_APK = 1 << 0; + public static final int PARSE_IGNORE_PROCESSES = 1 << 1; + public static final int PARSE_FORWARD_LOCK = 1 << 2; + public static final int PARSE_EXTERNAL_STORAGE = 1 << 3; + public static final int PARSE_IS_SYSTEM_DIR = 1 << 4; + public static final int PARSE_COLLECT_CERTIFICATES = 1 << 5; + public static final int PARSE_ENFORCE_CODE = 1 << 6; + public static final int PARSE_FORCE_SDK = 1 << 7; + /** @deprecated remove when fixing b/68860689 */ @Deprecated - public final static int PARSE_IS_EPHEMERAL = 1<<11; - public final static int PARSE_FORCE_SDK = 1<<12; + public static final int PARSE_IS_EPHEMERAL = 1 << 8; + public static final int PARSE_CHATTY = 1 << 31; + + @IntDef(flag = true, prefix = { "PARSE_" }, value = { + PARSE_CHATTY, + PARSE_COLLECT_CERTIFICATES, + PARSE_ENFORCE_CODE, + PARSE_EXTERNAL_STORAGE, + PARSE_FORCE_SDK, + PARSE_FORWARD_LOCK, + PARSE_IGNORE_PROCESSES, + PARSE_IS_EPHEMERAL, + PARSE_IS_SYSTEM_DIR, + PARSE_MUST_BE_APK, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ParseFlags {} private static final Comparator<String> sSplitNameComparator = new SplitNameComparator(); @@ -1250,9 +1275,12 @@ public class PackageParser { } } - pkg.setCodePath(packageDir.getAbsolutePath()); + pkg.setCodePath(packageDir.getCanonicalPath()); pkg.setUse32bitAbi(lite.use32bitAbi); return pkg; + } catch (IOException e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed to get path: " + lite.baseCodePath, e); } finally { IoUtils.closeQuietly(assetLoader); } @@ -1281,9 +1309,12 @@ public class PackageParser { try { final Package pkg = parseBaseApk(apkFile, assets, flags); - pkg.setCodePath(apkFile.getAbsolutePath()); + pkg.setCodePath(apkFile.getCanonicalPath()); pkg.setUse32bitAbi(lite.use32bitAbi); return pkg; + } catch (IOException e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed to get path: " + apkFile, e); } finally { IoUtils.closeQuietly(assets); } @@ -1513,7 +1544,7 @@ public class PackageParser { * populating {@link Package#mSignatures}. Also asserts that all APK * contents are signed correctly and consistently. */ - public static void collectCertificates(Package pkg, int parseFlags) + public static void collectCertificates(Package pkg, @ParseFlags int parseFlags) throws PackageParserException { collectCertificatesInternal(pkg, parseFlags); final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0; @@ -1525,7 +1556,7 @@ public class PackageParser { } } - private static void collectCertificatesInternal(Package pkg, int parseFlags) + private static void collectCertificatesInternal(Package pkg, @ParseFlags int parseFlags) throws PackageParserException { pkg.mCertificates = null; pkg.mSignatures = null; @@ -1545,7 +1576,7 @@ public class PackageParser { } } - private static void collectCertificates(Package pkg, File apkFile, int parseFlags) + private static void collectCertificates(Package pkg, File apkFile, @ParseFlags int parseFlags) throws PackageParserException { final String apkPath = apkFile.getAbsolutePath(); @@ -1717,13 +1748,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); @@ -1846,6 +1897,7 @@ public class PackageParser { int installLocation = PARSE_DEFAULT_INSTALL_LOCATION; int versionCode = 0; + int versionCodeMajor = 0; int revisionCode = 0; boolean coreApp = false; boolean debuggable = false; @@ -1864,6 +1916,8 @@ public class PackageParser { PARSE_DEFAULT_INSTALL_LOCATION); } else if (attr.equals("versionCode")) { versionCode = attrs.getAttributeIntValue(i, 0); + } else if (attr.equals("versionCodeMajor")) { + versionCodeMajor = attrs.getAttributeIntValue(i, 0); } else if (attr.equals("revisionCode")) { revisionCode = attrs.getAttributeIntValue(i, 0); } else if (attr.equals("coreApp")) { @@ -1929,9 +1983,9 @@ public class PackageParser { } return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit, - configForSplit, usesSplitName, versionCode, revisionCode, installLocation, - verifiers, signatures, certificates, coreApp, debuggable, multiArch, use32bitAbi, - extractNativeLibs, isolatedSplits); + configForSplit, usesSplitName, versionCode, versionCodeMajor, revisionCode, + installLocation, verifiers, signatures, certificates, coreApp, debuggable, + multiArch, use32bitAbi, extractNativeLibs, isolatedSplits); } /** @@ -1951,14 +2005,6 @@ public class PackageParser { */ private boolean parseBaseApkChild(Package parentPkg, Resources res, XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException, IOException { - // Let ppl not abuse this mechanism by limiting the packages per APK - if (parentPkg.childPackages != null && parentPkg.childPackages.size() + 2 - > MAX_PACKAGES_PER_APK) { - outError[0] = "Maximum number of packages per APK is: " + MAX_PACKAGES_PER_APK; - mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; - return false; - } - // Make sure we have a valid child package name String childPackageName = parser.getAttributeValue(null, "package"); if (validateName(childPackageName, true, false) != null) { @@ -2060,8 +2106,11 @@ public class PackageParser { TypedArray sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifest); - pkg.mVersionCode = pkg.applicationInfo.versionCode = sa.getInteger( + pkg.mVersionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_versionCode, 0); + pkg.mVersionCodeMajor = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_versionCodeMajor, 0); + pkg.applicationInfo.versionCode = pkg.getLongVersionCode(); pkg.baseRevisionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_revisionCode, 0); pkg.mVersionName = sa.getNonConfigurationString( @@ -2072,6 +2121,16 @@ public class PackageParser { pkg.coreApp = parser.getAttributeBooleanValue(null, "coreApp", false); + pkg.mCompileSdkVersion = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_compileSdkVersion, 0); + pkg.applicationInfo.compileSdkVersion = pkg.mCompileSdkVersion; + pkg.mCompileSdkVersionCodename = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifest_compileSdkVersionCodename, 0); + if (pkg.mCompileSdkVersionCodename != null) { + pkg.mCompileSdkVersionCodename = pkg.mCompileSdkVersionCodename.intern(); + } + pkg.applicationInfo.compileSdkVersionCodename = pkg.mCompileSdkVersionCodename; + sa.recycle(); return parseBaseApkCommon(pkg, null, res, parser, flags, outError); @@ -2438,7 +2497,7 @@ public class PackageParser { sa.recycle(); - if (name != null && (flags&PARSE_IS_SYSTEM) != 0) { + if (name != null) { if (pkg.protectedBroadcasts == null) { pkg.protectedBroadcasts = new ArrayList<String>(); } @@ -2876,7 +2935,7 @@ public class PackageParser { 1, additionalCertSha256Digests.length); pkg.usesStaticLibraries = ArrayUtils.add(pkg.usesStaticLibraries, lname); - pkg.usesStaticLibrariesVersions = ArrayUtils.appendInt( + pkg.usesStaticLibrariesVersions = ArrayUtils.appendLong( pkg.usesStaticLibrariesVersions, version, true); pkg.usesStaticLibrariesCertDigests = ArrayUtils.appendElement(String[].class, pkg.usesStaticLibrariesCertDigests, certSha256Digests, true); @@ -3233,13 +3292,12 @@ public class PackageParser { perm.info.descriptionRes = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestPermissionGroup_description, 0); + perm.info.requestRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermissionGroup_request, 0); perm.info.flags = sa.getInt( com.android.internal.R.styleable.AndroidManifestPermissionGroup_permissionGroupFlags, 0); perm.info.priority = sa.getInt( com.android.internal.R.styleable.AndroidManifestPermissionGroup_priority, 0); - if (perm.info.priority > 0 && (flags&PARSE_IS_SYSTEM) == 0) { - perm.info.priority = 0; - } sa.recycle(); @@ -3287,6 +3345,9 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestPermission_description, 0); + perm.info.requestRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermission_request, 0); + perm.info.protectionLevel = sa.getInt( com.android.internal.R.styleable.AndroidManifestPermission_protectionLevel, PermissionInfo.PROTECTION_NORMAL); @@ -3361,6 +3422,7 @@ public class PackageParser { } perm.info.descriptionRes = 0; + perm.info.requestRes = 0; perm.info.protectionLevel = PermissionInfo.PROTECTION_NORMAL; perm.tree = true; @@ -3541,17 +3603,14 @@ public class PackageParser { ai.descriptionRes = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestApplication_description, 0); - if ((flags&PARSE_IS_SYSTEM) != 0) { - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestApplication_persistent, - false)) { - // Check if persistence is based on a feature being present - final String requiredFeature = sa.getNonResourceString( - com.android.internal.R.styleable. - AndroidManifestApplication_persistentWhenFeatureAvailable); - if (requiredFeature == null || mCallback.hasFeature(requiredFeature)) { - ai.flags |= ApplicationInfo.FLAG_PERSISTENT; - } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_persistent, + false)) { + // Check if persistence is based on a feature being present + final String requiredFeature = sa.getNonResourceString(com.android.internal.R.styleable + .AndroidManifestApplication_persistentWhenFeatureAvailable); + if (requiredFeature == null || mCallback.hasFeature(requiredFeature)) { + ai.flags |= ApplicationInfo.FLAG_PERSISTENT; } } @@ -3722,17 +3781,15 @@ public class PackageParser { ai.flags |= ApplicationInfo.FLAG_IS_GAME; } - if (false) { - if (sa.getBoolean( - com.android.internal.R.styleable.AndroidManifestApplication_cantSaveState, - false)) { - ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE; + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_cantSaveState, + false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE; - // A heavy-weight application can not be in a custom process. - // We can do direct compare because we intern all strings. - if (ai.processName != null && ai.processName != ai.packageName) { - outError[0] = "cantSaveState applications can not use custom processes"; - } + // A heavy-weight application can not be in a custom process. + // We can do direct compare because we intern all strings. + if (ai.processName != null && !ai.processName.equals(ai.packageName)) { + outError[0] = "cantSaveState applications can not use custom processes"; } } } @@ -3833,6 +3890,9 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestStaticLibrary_name); final int version = sa.getInt( com.android.internal.R.styleable.AndroidManifestStaticLibrary_version, -1); + final int versionMajor = sa.getInt( + com.android.internal.R.styleable.AndroidManifestStaticLibrary_versionMajor, + 0); sa.recycle(); @@ -3860,7 +3920,12 @@ public class PackageParser { } owner.staticSharedLibName = lname.intern(); - owner.staticSharedLibVersion = version; + if (version >= 0) { + owner.staticSharedLibVersion = + PackageInfo.composeLongVersionCode(versionMajor, version); + } else { + owner.staticSharedLibVersion = version; + } ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_STATIC_SHARED_LIBRARY; XmlUtils.skipCurrentTag(parser); @@ -4423,13 +4488,6 @@ public class PackageParser { if (sa.getBoolean(R.styleable.AndroidManifestActivity_singleUser, false)) { a.info.flags |= ActivityInfo.FLAG_SINGLE_USER; - if (a.info.exported && (flags & PARSE_IS_PRIVILEGED) == 0) { - Slog.w(TAG, "Activity exported request ignored due to singleUser: " - + a.className + " at " + mArchiveSourcePath + " " - + parser.getPositionDescription()); - a.info.exported = false; - setExported = true; - } } a.info.encryptionAware = a.info.directBootAware = sa.getBoolean( @@ -5018,12 +5076,6 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestProvider_singleUser, false)) { p.info.flags |= ProviderInfo.FLAG_SINGLE_USER; - if (p.info.exported && (flags & PARSE_IS_PRIVILEGED) == 0) { - Slog.w(TAG, "Provider exported request ignored due to singleUser: " - + p.className + " at " + mArchiveSourcePath + " " - + parser.getPositionDescription()); - p.info.exported = false; - } } p.info.encryptionAware = p.info.directBootAware = sa.getBoolean( @@ -5345,13 +5397,6 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestService_singleUser, false)) { s.info.flags |= ServiceInfo.FLAG_SINGLE_USER; - if (s.info.exported && (flags & PARSE_IS_PRIVILEGED) == 0) { - Slog.w(TAG, "Service exported request ignored due to singleUser: " - + s.className + " at " + mArchiveSourcePath + " " - + parser.getPositionDescription()); - s.info.exported = false; - setExported = true; - } } s.info.encryptionAware = s.info.directBootAware = sa.getBoolean( @@ -5881,11 +5926,11 @@ public class PackageParser { public ArrayList<Package> childPackages; public String staticSharedLibName = null; - public int staticSharedLibVersion = 0; + public long staticSharedLibVersion = 0; public ArrayList<String> libraryNames = null; public ArrayList<String> usesLibraries = null; public ArrayList<String> usesStaticLibraries = null; - public int[] usesStaticLibrariesVersions = null; + public long[] usesStaticLibrariesVersions = null; public String[][] usesStaticLibrariesCertDigests = null; public ArrayList<String> usesOptionalLibraries = null; public String[] usesLibraryFiles = null; @@ -5902,6 +5947,14 @@ public class PackageParser { // The version code declared for this package. public int mVersionCode; + // The major version code declared for this package. + public int mVersionCodeMajor; + + // Return long containing mVersionCode and mVersionCodeMajor. + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(mVersionCodeMajor, mVersionCode); + } + // The version name declared for this package. public String mVersionName; @@ -5959,6 +6012,9 @@ public class PackageParser { public boolean mIsStaticOverlay; public boolean mTrustedOverlay; + public int mCompileSdkVersion; + public String mCompileSdkVersionCodename; + /** * Data used to feed the KeySetManagerService */ @@ -6234,48 +6290,53 @@ public class PackageParser { return false; } - /** - * @hide - */ + /** @hide */ + public boolean isExternal() { + return applicationInfo.isExternal(); + } + + /** @hide */ public boolean isForwardLocked() { return applicationInfo.isForwardLocked(); } - /** - * @hide - */ - public boolean isSystemApp() { - return applicationInfo.isSystemApp(); + /** @hide */ + public boolean isOem() { + return applicationInfo.isOem(); } - /** - * @hide - */ - public boolean isPrivilegedApp() { + /** @hide */ + public boolean isVendor() { + return applicationInfo.isVendor(); + } + + /** @hide */ + public boolean isPrivileged() { return applicationInfo.isPrivilegedApp(); } - /** - * @hide - */ + /** @hide */ + public boolean isSystem() { + return applicationInfo.isSystemApp(); + } + + /** @hide */ public boolean isUpdatedSystemApp() { return applicationInfo.isUpdatedSystemApp(); } - /** - * @hide - */ + /** @hide */ public boolean canHaveOatDir() { // The following app types CANNOT have oat directory // - non-updated system apps // - forward-locked apps or apps installed in ASEC containers - return (!isSystemApp() || isUpdatedSystemApp()) + return (!isSystem() || isUpdatedSystemApp()) && !isForwardLocked() && !applicationInfo.isExternalAsec(); } public boolean isMatch(int flags) { if ((flags & PackageManager.MATCH_SYSTEM_ONLY) != 0) { - return isSystemApp(); + return isSystem(); } return true; } @@ -6368,7 +6429,7 @@ public class PackageParser { if (staticSharedLibName != null) { staticSharedLibName = staticSharedLibName.intern(); } - staticSharedLibVersion = dest.readInt(); + staticSharedLibVersion = dest.readLong(); libraryNames = dest.createStringArrayList(); internStringArrayList(libraryNames); usesLibraries = dest.createStringArrayList(); @@ -6382,8 +6443,8 @@ public class PackageParser { usesStaticLibraries = new ArrayList<>(libCount); dest.readStringList(usesStaticLibraries); internStringArrayList(usesStaticLibraries); - usesStaticLibrariesVersions = new int[libCount]; - dest.readIntArray(usesStaticLibrariesVersions); + usesStaticLibrariesVersions = new long[libCount]; + dest.readLongArray(usesStaticLibrariesVersions); usesStaticLibrariesCertDigests = new String[libCount][]; for (int i = 0; i < libCount; i++) { usesStaticLibrariesCertDigests[i] = dest.createStringArray(); @@ -6401,6 +6462,7 @@ public class PackageParser { mAdoptPermissions = dest.createStringArrayList(); mAppMetaData = dest.readBundle(); mVersionCode = dest.readInt(); + mVersionCodeMajor = dest.readInt(); mVersionName = dest.readString(); if (mVersionName != null) { mVersionName = mVersionName.intern(); @@ -6450,6 +6512,8 @@ public class PackageParser { mOverlayPriority = dest.readInt(); mIsStaticOverlay = (dest.readInt() == 1); mTrustedOverlay = (dest.readInt() == 1); + mCompileSdkVersion = dest.readInt(); + mCompileSdkVersionCodename = dest.readString(); mSigningKeys = (ArraySet<PublicKey>) dest.readArraySet(boot); mUpgradeKeySets = (ArraySet<String>) dest.readArraySet(boot); @@ -6521,7 +6585,7 @@ public class PackageParser { dest.writeParcelableList(childPackages, flags); dest.writeString(staticSharedLibName); - dest.writeInt(staticSharedLibVersion); + dest.writeLong(staticSharedLibVersion); dest.writeStringList(libraryNames); dest.writeStringList(usesLibraries); dest.writeStringList(usesOptionalLibraries); @@ -6532,7 +6596,7 @@ public class PackageParser { } else { dest.writeInt(usesStaticLibraries.size()); dest.writeStringList(usesStaticLibraries); - dest.writeIntArray(usesStaticLibrariesVersions); + dest.writeLongArray(usesStaticLibrariesVersions); for (String[] usesStaticLibrariesCertDigest : usesStaticLibrariesCertDigests) { dest.writeStringArray(usesStaticLibrariesCertDigest); } @@ -6545,6 +6609,7 @@ public class PackageParser { dest.writeStringList(mAdoptPermissions); dest.writeBundle(mAppMetaData); dest.writeInt(mVersionCode); + dest.writeInt(mVersionCodeMajor); dest.writeString(mVersionName); dest.writeString(mSharedUserId); dest.writeInt(mSharedUserLabel); @@ -6573,6 +6638,8 @@ public class PackageParser { dest.writeInt(mOverlayPriority); dest.writeInt(mIsStaticOverlay ? 1 : 0); dest.writeInt(mTrustedOverlay ? 1 : 0); + dest.writeInt(mCompileSdkVersion); + dest.writeString(mCompileSdkVersionCodename); dest.writeArraySet(mSigningKeys); dest.writeArraySet(mUpgradeKeySets); writeKeySetMapping(dest, mKeySetMapping); @@ -6860,6 +6927,11 @@ public class PackageParser { dest.writeParcelable(group, flags); } + /** @hide */ + public boolean isAppOp() { + return info.isAppOp(); + } + private Permission(Parcel in) { super(in); final ClassLoader boot = Object.class.getClassLoader(); diff --git a/core/java/android/content/pm/PermissionGroupInfo.java b/core/java/android/content/pm/PermissionGroupInfo.java index 452bf0d2b6a1..7c4478d0b689 100644 --- a/core/java/android/content/pm/PermissionGroupInfo.java +++ b/core/java/android/content/pm/PermissionGroupInfo.java @@ -16,6 +16,8 @@ package android.content.pm; +import android.annotation.StringRes; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -34,6 +36,15 @@ public class PermissionGroupInfo extends PackageItemInfo implements Parcelable { public int descriptionRes; /** + * A string resource identifier (in the package's resources) used to request the permissions. + * From the "request" attribute or, if not set, 0. + * + * @hide + */ + @SystemApi + public @StringRes int requestRes; + + /** * The description string provided in the AndroidManifest file, if any. You * probably don't want to use this, since it will be null if the description * is in a resource. You probably want @@ -64,6 +75,7 @@ public class PermissionGroupInfo extends PackageItemInfo implements Parcelable { public PermissionGroupInfo(PermissionGroupInfo orig) { super(orig); descriptionRes = orig.descriptionRes; + requestRes = orig.requestRes; nonLocalizedDescription = orig.nonLocalizedDescription; flags = orig.flags; priority = orig.priority; @@ -106,6 +118,7 @@ public class PermissionGroupInfo extends PackageItemInfo implements Parcelable { public void writeToParcel(Parcel dest, int parcelableFlags) { super.writeToParcel(dest, parcelableFlags); dest.writeInt(descriptionRes); + dest.writeInt(requestRes); TextUtils.writeToParcel(nonLocalizedDescription, dest, parcelableFlags); dest.writeInt(flags); dest.writeInt(priority); @@ -124,6 +137,7 @@ public class PermissionGroupInfo extends PackageItemInfo implements Parcelable { private PermissionGroupInfo(Parcel source) { super(source); descriptionRes = source.readInt(); + requestRes = source.readInt(); nonLocalizedDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); flags = source.readInt(); priority = source.readInt(); diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java index 797db5497390..21bd7f0ce763 100644 --- a/core/java/android/content/pm/PermissionInfo.java +++ b/core/java/android/content/pm/PermissionInfo.java @@ -135,6 +135,26 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { public static final int PROTECTION_FLAG_RUNTIME_ONLY = 0x2000; /** + * Additional flag for {@link #protectionLevel}, corresponding + * to the <code>oem</code> value of + * {@link android.R.attr#protectionLevel}. + * + * @hide + */ + @SystemApi + public static final int PROTECTION_FLAG_OEM = 0x4000; + + /** + * Additional flag for {${link #protectionLevel}, corresponding + * to the <code>vendorPrivileged</code> value of + * {@link android.R.attr#protectionLevel}. + * + * @hide + */ + @TestApi + public static final int PROTECTION_FLAG_VENDOR_PRIVILEGED = 0x8000; + + /** * Mask for {@link #protectionLevel}: the basic protection type. */ public static final int PROTECTION_MASK_BASE = 0xf; @@ -146,12 +166,18 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { /** * The level of access this permission is protecting, as per - * {@link android.R.attr#protectionLevel}. Values may be - * {@link #PROTECTION_NORMAL}, {@link #PROTECTION_DANGEROUS}, or - * {@link #PROTECTION_SIGNATURE}. May also include the additional - * flags {@link #PROTECTION_FLAG_SYSTEM} or {@link #PROTECTION_FLAG_DEVELOPMENT} - * (which only make sense in combination with the base - * {@link #PROTECTION_SIGNATURE}. + * {@link android.R.attr#protectionLevel}. Consists of + * a base permission type and zero or more flags: + * + * <pre> + * int basePermissionType = protectionLevel & {@link #PROTECTION_MASK_BASE}; + * int permissionFlags = protectionLevel & {@link #PROTECTION_MASK_FLAGS}; + * </pre> + * + * <p></p>Base permission types are {@link #PROTECTION_NORMAL}, + * {@link #PROTECTION_DANGEROUS}, {@link #PROTECTION_SIGNATURE} + * and the deprecated {@link #PROTECTION_SIGNATURE_OR_SYSTEM}. + * Flags are listed under {@link android.R.attr#protectionLevel}. */ public int protectionLevel; @@ -195,6 +221,15 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { public int descriptionRes; /** + * A string resource identifier (in the package's resources) used to request the permissions. + * From the "request" attribute or, if not set, 0. + * + * @hide + */ + @SystemApi + public int requestRes; + + /** * The description string provided in the AndroidManifest file, if any. You * probably don't want to use this, since it will be null if the description * is in a resource. You probably want @@ -207,13 +242,19 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { if (level == PROTECTION_SIGNATURE_OR_SYSTEM) { level = PROTECTION_SIGNATURE | PROTECTION_FLAG_PRIVILEGED; } + if ((level & PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0 + && (level & PROTECTION_FLAG_PRIVILEGED) == 0) { + // 'vendorPrivileged' must be 'privileged'. If not, + // drop the vendorPrivileged. + level = level & ~PROTECTION_FLAG_VENDOR_PRIVILEGED; + } return level; } /** @hide */ public static String protectionToString(int level) { String protLevel = "????"; - switch (level&PROTECTION_MASK_BASE) { + switch (level & PROTECTION_MASK_BASE) { case PermissionInfo.PROTECTION_DANGEROUS: protLevel = "dangerous"; break; @@ -227,36 +268,42 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { protLevel = "signatureOrSystem"; break; } - if ((level&PermissionInfo.PROTECTION_FLAG_PRIVILEGED) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_PRIVILEGED) != 0) { protLevel += "|privileged"; } - if ((level&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) { protLevel += "|development"; } - if ((level&PermissionInfo.PROTECTION_FLAG_APPOP) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_APPOP) != 0) { protLevel += "|appop"; } - if ((level&PermissionInfo.PROTECTION_FLAG_PRE23) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_PRE23) != 0) { protLevel += "|pre23"; } - if ((level&PermissionInfo.PROTECTION_FLAG_INSTALLER) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_INSTALLER) != 0) { protLevel += "|installer"; } - if ((level&PermissionInfo.PROTECTION_FLAG_VERIFIER) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_VERIFIER) != 0) { protLevel += "|verifier"; } - if ((level&PermissionInfo.PROTECTION_FLAG_PREINSTALLED) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_PREINSTALLED) != 0) { protLevel += "|preinstalled"; } - if ((level&PermissionInfo.PROTECTION_FLAG_SETUP) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_SETUP) != 0) { protLevel += "|setup"; } - if ((level&PermissionInfo.PROTECTION_FLAG_INSTANT) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_INSTANT) != 0) { protLevel += "|instant"; } - if ((level&PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY) != 0) { + if ((level & PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY) != 0) { protLevel += "|runtime"; } + if ((level & PermissionInfo.PROTECTION_FLAG_OEM) != 0) { + protLevel += "|oem"; + } + if ((level & PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0) { + protLevel += "|vendorPrivileged"; + } return protLevel; } @@ -269,6 +316,7 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { flags = orig.flags; group = orig.group; descriptionRes = orig.descriptionRes; + requestRes = orig.requestRes; nonLocalizedDescription = orig.nonLocalizedDescription; } @@ -296,30 +344,53 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { return null; } + @Override public String toString() { return "PermissionInfo{" + Integer.toHexString(System.identityHashCode(this)) + " " + name + "}"; } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel dest, int parcelableFlags) { super.writeToParcel(dest, parcelableFlags); dest.writeInt(protectionLevel); dest.writeInt(flags); dest.writeString(group); dest.writeInt(descriptionRes); + dest.writeInt(requestRes); TextUtils.writeToParcel(nonLocalizedDescription, dest, parcelableFlags); } + /** @hide */ + public int calculateFootprint() { + int size = name.length(); + if (nonLocalizedLabel != null) { + size += nonLocalizedLabel.length(); + } + if (nonLocalizedDescription != null) { + size += nonLocalizedDescription.length(); + } + return size; + } + + /** @hide */ + public boolean isAppOp() { + return (protectionLevel & PermissionInfo.PROTECTION_FLAG_APPOP) != 0; + } + public static final Creator<PermissionInfo> CREATOR = new Creator<PermissionInfo>() { + @Override public PermissionInfo createFromParcel(Parcel source) { return new PermissionInfo(source); } + @Override public PermissionInfo[] newArray(int size) { return new PermissionInfo[size]; } @@ -331,6 +402,7 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { flags = source.readInt(); group = source.readString(); descriptionRes = source.readInt(); + requestRes = source.readInt(); nonLocalizedDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); } } diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index aea843adbd48..56d61efdcb25 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -361,7 +361,7 @@ public abstract class RegisteredServicesCache<V> { } IntArray updatedUids = null; for (ServiceInfo<V> service : allServices) { - int versionCode = service.componentInfo.applicationInfo.versionCode; + long versionCode = service.componentInfo.applicationInfo.versionCode; String pkg = service.componentInfo.packageName; ApplicationInfo newAppInfo = null; try { diff --git a/core/java/android/content/pm/SharedLibraryInfo.java b/core/java/android/content/pm/SharedLibraryInfo.java index 7d301a3154f0..2f1b256dccdf 100644 --- a/core/java/android/content/pm/SharedLibraryInfo.java +++ b/core/java/android/content/pm/SharedLibraryInfo.java @@ -73,8 +73,7 @@ public final class SharedLibraryInfo implements Parcelable { private final String mName; - // TODO: Make long when we change the paltform to use longs - private final int mVersion; + private final long mVersion; private final @Type int mType; private final VersionedPackage mDeclaringPackage; private final List<VersionedPackage> mDependentPackages; @@ -90,7 +89,7 @@ public final class SharedLibraryInfo implements Parcelable { * * @hide */ - public SharedLibraryInfo(String name, int version, int type, + public SharedLibraryInfo(String name, long version, int type, VersionedPackage declaringPackage, List<VersionedPackage> dependentPackages) { mName = name; mVersion = version; @@ -100,7 +99,7 @@ public final class SharedLibraryInfo implements Parcelable { } private SharedLibraryInfo(Parcel parcel) { - this(parcel.readString(), parcel.readInt(), parcel.readInt(), + this(parcel.readString(), parcel.readLong(), parcel.readInt(), parcel.readParcelable(null), parcel.readArrayList(null)); } @@ -124,6 +123,14 @@ public final class SharedLibraryInfo implements Parcelable { } /** + * @deprecated Use {@link #getLongVersion()} instead. + */ + @Deprecated + public @IntRange(from = -1) int getVersion() { + return mVersion < 0 ? (int) mVersion : (int) (mVersion & 0x7fffffff); + } + + /** * Gets the version of the library. For {@link #TYPE_STATIC static} libraries * this is the declared version and for {@link #TYPE_DYNAMIC dynamic} and * {@link #TYPE_BUILTIN builtin} it is {@link #VERSION_UNDEFINED} as these @@ -131,7 +138,7 @@ public final class SharedLibraryInfo implements Parcelable { * * @return The version. */ - public @IntRange(from = -1) int getVersion() { + public @IntRange(from = -1) long getLongVersion() { return mVersion; } @@ -192,7 +199,7 @@ public final class SharedLibraryInfo implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeString(mName); - parcel.writeInt(mVersion); + parcel.writeLong(mVersion); parcel.writeInt(mType); parcel.writeParcelable(mDeclaringPackage, flags); parcel.writeList(mDependentPackages); diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index d3a3560c7229..9ff077576bfd 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -18,6 +18,7 @@ package android.content.pm; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.TaskStackBuilder; import android.content.ComponentName; @@ -100,6 +101,13 @@ public final class ShortcutInfo implements Parcelable { /** @hide When this is set, the bitmap icon is waiting to be saved. */ public static final int FLAG_ICON_FILE_PENDING_SAVE = 1 << 11; + /** + * "Shadow" shortcuts are the ones that are restored, but the owner package hasn't been + * installed yet. + * @hide + */ + public static final int FLAG_SHADOW = 1 << 12; + /** @hide */ @IntDef(flag = true, value = { @@ -158,6 +166,124 @@ public final class ShortcutInfo implements Parcelable { public @interface CloneFlags {} /** + * Shortcut is not disabled. + */ + public static final int DISABLED_REASON_NOT_DISABLED = 0; + + /** + * Shortcut has been disabled by the publisher app with the + * {@link ShortcutManager#disableShortcuts(List)} API. + */ + public static final int DISABLED_REASON_BY_APP = 1; + + /** + * Shortcut has been disabled due to changes to the publisher app. (e.g. a manifest shortcut + * no longer exists.) + */ + public static final int DISABLED_REASON_APP_CHANGED = 2; + + /** + * A disabled reason that's equal to or bigger than this is due to backup and restore issue. + * A shortcut with such a reason wil be visible to the launcher, but not to the publisher. + * ({@link #isVisibleToPublisher()} will be false.) + */ + private static final int DISABLED_REASON_RESTORE_ISSUE_START = 100; + + /** + * Shortcut has been restored from the previous device, but the publisher app on the current + * device is of a lower version. The shortcut will not be usable until the app is upgraded to + * the same version or higher. + */ + public static final int DISABLED_REASON_VERSION_LOWER = 100; + + /** + * Shortcut has not been restored because the publisher app does not support backup and restore. + */ + public static final int DISABLED_REASON_BACKUP_NOT_SUPPORTED = 101; + + /** + * Shortcut has not been restored because the publisher app's signature has changed. + */ + public static final int DISABLED_REASON_SIGNATURE_MISMATCH = 102; + + /** + * Shortcut has not been restored for unknown reason. + */ + public static final int DISABLED_REASON_OTHER_RESTORE_ISSUE = 103; + + /** @hide */ + @IntDef(value = { + DISABLED_REASON_NOT_DISABLED, + DISABLED_REASON_BY_APP, + DISABLED_REASON_APP_CHANGED, + DISABLED_REASON_VERSION_LOWER, + DISABLED_REASON_BACKUP_NOT_SUPPORTED, + DISABLED_REASON_SIGNATURE_MISMATCH, + DISABLED_REASON_OTHER_RESTORE_ISSUE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DisabledReason{} + + /** + * Return a label for disabled reasons, which are *not* supposed to be shown to the user. + * @hide + */ + public static String getDisabledReasonDebugString(@DisabledReason int disabledReason) { + switch (disabledReason) { + case DISABLED_REASON_NOT_DISABLED: + return "[Not disabled]"; + case DISABLED_REASON_BY_APP: + return "[Disabled: by app]"; + case DISABLED_REASON_APP_CHANGED: + return "[Disabled: app changed]"; + case DISABLED_REASON_VERSION_LOWER: + return "[Disabled: lower version]"; + case DISABLED_REASON_BACKUP_NOT_SUPPORTED: + return "[Disabled: backup not supported]"; + case DISABLED_REASON_SIGNATURE_MISMATCH: + return "[Disabled: signature mismatch]"; + case DISABLED_REASON_OTHER_RESTORE_ISSUE: + return "[Disabled: unknown restore issue]"; + } + return "[Disabled: unknown reason:" + disabledReason + "]"; + } + + /** + * Return a label for a disabled reason for shortcuts that are disabled due to a backup and + * restore issue. If the reason is not due to backup & restore, then it'll return null. + * + * This method returns localized, user-facing strings, which will be returned by + * {@link #getDisabledMessage()}. + * + * @hide + */ + public static String getDisabledReasonForRestoreIssue(Context context, + @DisabledReason int disabledReason) { + final Resources res = context.getResources(); + + switch (disabledReason) { + case DISABLED_REASON_VERSION_LOWER: + return res.getString( + com.android.internal.R.string.shortcut_restored_on_lower_version); + case DISABLED_REASON_BACKUP_NOT_SUPPORTED: + return res.getString( + com.android.internal.R.string.shortcut_restore_not_supported); + case DISABLED_REASON_SIGNATURE_MISMATCH: + return res.getString( + com.android.internal.R.string.shortcut_restore_signature_mismatch); + case DISABLED_REASON_OTHER_RESTORE_ISSUE: + return res.getString( + com.android.internal.R.string.shortcut_restore_unknown_issue); + } + return null; + } + + /** @hide */ + public static boolean isDisabledForRestoreIssue(@DisabledReason int disabledReason) { + return disabledReason >= DISABLED_REASON_RESTORE_ISSUE_START; + } + + /** * Shortcut category for messaging related actions, such as chat. */ public static final String SHORTCUT_CATEGORY_CONVERSATION = "android.shortcut.conversation"; @@ -240,6 +366,11 @@ public final class ShortcutInfo implements Parcelable { private final int mUserId; + /** @hide */ + public static final int VERSION_CODE_UNKNOWN = -1; + + private int mDisabledReason; + private ShortcutInfo(Builder b) { mUserId = b.mContext.getUserId(); @@ -352,6 +483,7 @@ public final class ShortcutInfo implements Parcelable { mActivity = source.mActivity; mFlags = source.mFlags; mLastChangedTimestamp = source.mLastChangedTimestamp; + mDisabledReason = source.mDisabledReason; // Just always keep it since it's cheep. mIconResId = source.mIconResId; @@ -615,13 +747,23 @@ public final class ShortcutInfo implements Parcelable { /** * @hide + * + * @isUpdating set true if it's "update", as opposed to "replace". */ - public void ensureUpdatableWith(ShortcutInfo source) { + public void ensureUpdatableWith(ShortcutInfo source, boolean isUpdating) { + if (isUpdating) { + Preconditions.checkState(isVisibleToPublisher(), + "[Framework BUG] Invisible shortcuts can't be updated"); + } Preconditions.checkState(mUserId == source.mUserId, "Owner User ID must match"); Preconditions.checkState(mId.equals(source.mId), "ID must match"); Preconditions.checkState(mPackageName.equals(source.mPackageName), "Package name must match"); - Preconditions.checkState(!isImmutable(), "Target ShortcutInfo is immutable"); + + if (isVisibleToPublisher()) { + // Don't do this check for restore-blocked shortcuts. + Preconditions.checkState(!isImmutable(), "Target ShortcutInfo is immutable"); + } } /** @@ -638,7 +780,7 @@ public final class ShortcutInfo implements Parcelable { * @hide */ public void copyNonNullFieldsFrom(ShortcutInfo source) { - ensureUpdatableWith(source); + ensureUpdatableWith(source, /*isUpdating=*/ true); if (source.mActivity != null) { mActivity = source.mActivity; @@ -1169,6 +1311,19 @@ public final class ShortcutInfo implements Parcelable { return mDisabledMessageResId; } + /** @hide */ + public void setDisabledReason(@DisabledReason int reason) { + mDisabledReason = reason; + } + + /** + * Returns why a shortcut has been disabled. + */ + @DisabledReason + public int getDisabledReason() { + return mDisabledReason; + } + /** * Return the shortcut's categories. * @@ -1403,6 +1558,21 @@ public final class ShortcutInfo implements Parcelable { return hasFlags(FLAG_IMMUTABLE); } + /** @hide */ + public boolean isDynamicVisible() { + return isDynamic() && isVisibleToPublisher(); + } + + /** @hide */ + public boolean isPinnedVisible() { + return isPinned() && isVisibleToPublisher(); + } + + /** @hide */ + public boolean isManifestVisible() { + return isDeclaredInManifest() && isVisibleToPublisher(); + } + /** * Return if a shortcut is immutable, in which case it cannot be modified with any of * {@link ShortcutManager} APIs. @@ -1491,6 +1661,18 @@ public final class ShortcutInfo implements Parcelable { } /** + * When the system wasn't able to restore a shortcut, it'll still be registered to the system + * but disabled, and such shortcuts will not be visible to the publisher. They're still visible + * to launchers though. + * + * @hide + */ + @TestApi + public boolean isVisibleToPublisher() { + return !isDisabledForRestoreIssue(mDisabledReason); + } + + /** * Return whether a shortcut only contains "key" information only or not. If true, only the * following fields are available. * <ul> @@ -1668,6 +1850,7 @@ public final class ShortcutInfo implements Parcelable { mFlags = source.readInt(); mIconResId = source.readInt(); mLastChangedTimestamp = source.readLong(); + mDisabledReason = source.readInt(); if (source.readInt() == 0) { return; // key information only. @@ -1711,6 +1894,7 @@ public final class ShortcutInfo implements Parcelable { dest.writeInt(mFlags); dest.writeInt(mIconResId); dest.writeLong(mLastChangedTimestamp); + dest.writeInt(mDisabledReason); if (hasKeyFieldsOnly()) { dest.writeInt(0); @@ -1763,21 +1947,43 @@ public final class ShortcutInfo implements Parcelable { return 0; } + /** * Return a string representation, intended for logging. Some fields will be retracted. */ @Override public String toString() { - return toStringInner(/* secure =*/ true, /* includeInternalData =*/ false); + return toStringInner(/* secure =*/ true, /* includeInternalData =*/ false, + /*indent=*/ null); } /** @hide */ public String toInsecureString() { - return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true); + return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true, + /*indent=*/ null); } - private String toStringInner(boolean secure, boolean includeInternalData) { + /** @hide */ + public String toDumpString(String indent) { + return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true, indent); + } + + private void addIndentOrComma(StringBuilder sb, String indent) { + if (indent != null) { + sb.append("\n "); + sb.append(indent); + } else { + sb.append(", "); + } + } + + private String toStringInner(boolean secure, boolean includeInternalData, String indent) { final StringBuilder sb = new StringBuilder(); + + if (indent != null) { + sb.append(indent); + } + sb.append("ShortcutInfo {"); sb.append("id="); @@ -1786,48 +1992,59 @@ public final class ShortcutInfo implements Parcelable { sb.append(", flags=0x"); sb.append(Integer.toHexString(mFlags)); sb.append(" ["); + if ((mFlags & FLAG_SHADOW) != 0) { + // Note the shadow flag isn't actually used anywhere and it's just for dumpsys, so + // we don't have an isXxx for this. + sb.append("Sdw"); + } if (!isEnabled()) { - sb.append("X"); + sb.append("Dis"); } if (isImmutable()) { sb.append("Im"); } if (isManifestShortcut()) { - sb.append("M"); + sb.append("Man"); } if (isDynamic()) { - sb.append("D"); + sb.append("Dyn"); } if (isPinned()) { - sb.append("P"); + sb.append("Pin"); } if (hasIconFile()) { - sb.append("If"); + sb.append("Ic-f"); } if (isIconPendingSave()) { - sb.append("^"); + sb.append("Pens"); } if (hasIconResource()) { - sb.append("Ir"); + sb.append("Ic-r"); } if (hasKeyFieldsOnly()) { - sb.append("K"); + sb.append("Key"); } if (hasStringResourcesResolved()) { - sb.append("Sr"); + sb.append("Str"); } if (isReturnedByServer()) { - sb.append("V"); + sb.append("Rets"); } sb.append("]"); - sb.append(", packageName="); + addIndentOrComma(sb, indent); + + sb.append("packageName="); sb.append(mPackageName); - sb.append(", activity="); + addIndentOrComma(sb, indent); + + sb.append("activity="); sb.append(mActivity); - sb.append(", shortLabel="); + addIndentOrComma(sb, indent); + + sb.append("shortLabel="); sb.append(secure ? "***" : mTitle); sb.append(", resId="); sb.append(mTitleResId); @@ -1835,7 +2052,9 @@ public final class ShortcutInfo implements Parcelable { sb.append(mTitleResName); sb.append("]"); - sb.append(", longLabel="); + addIndentOrComma(sb, indent); + + sb.append("longLabel="); sb.append(secure ? "***" : mText); sb.append(", resId="); sb.append(mTextResId); @@ -1843,7 +2062,9 @@ public final class ShortcutInfo implements Parcelable { sb.append(mTextResName); sb.append("]"); - sb.append(", disabledMessage="); + addIndentOrComma(sb, indent); + + sb.append("disabledMessage="); sb.append(secure ? "***" : mDisabledMessage); sb.append(", resId="); sb.append(mDisabledMessageResId); @@ -1851,19 +2072,32 @@ public final class ShortcutInfo implements Parcelable { sb.append(mDisabledMessageResName); sb.append("]"); - sb.append(", categories="); + addIndentOrComma(sb, indent); + + sb.append("disabledReason="); + sb.append(getDisabledReasonDebugString(mDisabledReason)); + + addIndentOrComma(sb, indent); + + sb.append("categories="); sb.append(mCategories); - sb.append(", icon="); + addIndentOrComma(sb, indent); + + sb.append("icon="); sb.append(mIcon); - sb.append(", rank="); + addIndentOrComma(sb, indent); + + sb.append("rank="); sb.append(mRank); sb.append(", timestamp="); sb.append(mLastChangedTimestamp); - sb.append(", intents="); + addIndentOrComma(sb, indent); + + sb.append("intents="); if (mIntents == null) { sb.append("null"); } else { @@ -1885,12 +2119,15 @@ public final class ShortcutInfo implements Parcelable { } } - sb.append(", extras="); + addIndentOrComma(sb, indent); + + sb.append("extras="); sb.append(mExtras); if (includeInternalData) { + addIndentOrComma(sb, indent); - sb.append(", iconRes="); + sb.append("iconRes="); sb.append(mIconResId); sb.append("["); sb.append(mIconResName); @@ -1912,7 +2149,7 @@ public final class ShortcutInfo implements Parcelable { CharSequence disabledMessage, int disabledMessageResId, String disabledMessageResName, Set<String> categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras, long lastChangedTimestamp, - int flags, int iconResId, String iconResName, String bitmapPath) { + int flags, int iconResId, String iconResName, String bitmapPath, int disabledReason) { mUserId = userId; mId = id; mPackageName = packageName; @@ -1937,5 +2174,6 @@ public final class ShortcutInfo implements Parcelable { mIconResId = iconResId; mIconResName = iconResName; mBitmapPath = bitmapPath; + mDisabledReason = disabledReason; } } diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java index 7b7d8ae42528..e6f682d22b14 100644 --- a/core/java/android/content/pm/ShortcutServiceInternal.java +++ b/core/java/android/content/pm/ShortcutServiceInternal.java @@ -46,7 +46,7 @@ public abstract class ShortcutServiceInternal { @NonNull String callingPackage, long changedSince, @Nullable String packageName, @Nullable List<String> shortcutIds, @Nullable ComponentName componentName, @ShortcutQuery.QueryFlags int flags, - int userId); + int userId, int callingPid, int callingUid); public abstract boolean isPinnedByCaller(int launcherUserId, @NonNull String callingPackage, @@ -58,7 +58,8 @@ public abstract class ShortcutServiceInternal { public abstract Intent[] createShortcutIntents( int launcherUserId, @NonNull String callingPackage, - @NonNull String packageName, @NonNull String shortcutId, int userId); + @NonNull String packageName, @NonNull String shortcutId, int userId, + int callingPid, int callingUid); public abstract void addListener(@NonNull ShortcutChangeListener listener); @@ -70,11 +71,17 @@ public abstract class ShortcutServiceInternal { @NonNull String packageName, @NonNull String shortcutId, int userId); public abstract boolean hasShortcutHostPermission(int launcherUserId, - @NonNull String callingPackage); + @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); public abstract boolean isRequestPinItemSupported(int callingUserId, int requestType); + + public abstract boolean isForegroundDefaultLauncher(@NonNull String callingPackage, + int callingUid); } diff --git a/core/java/android/content/pm/VersionedPackage.java b/core/java/android/content/pm/VersionedPackage.java index 29c5efe7c77a..395346641b1e 100644 --- a/core/java/android/content/pm/VersionedPackage.java +++ b/core/java/android/content/pm/VersionedPackage.java @@ -28,7 +28,7 @@ import java.lang.annotation.RetentionPolicy; */ public final class VersionedPackage implements Parcelable { private final String mPackageName; - private final int mVersionCode; + private final long mVersionCode; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -47,9 +47,21 @@ public final class VersionedPackage implements Parcelable { mVersionCode = versionCode; } + /** + * Creates a new instance. Use {@link PackageManager#VERSION_CODE_HIGHEST} + * to refer to the highest version code of this package. + * @param packageName The package name. + * @param versionCode The version code. + */ + public VersionedPackage(@NonNull String packageName, + @VersionCode long versionCode) { + mPackageName = packageName; + mVersionCode = versionCode; + } + private VersionedPackage(Parcel parcel) { mPackageName = parcel.readString(); - mVersionCode = parcel.readInt(); + mVersionCode = parcel.readLong(); } /** @@ -62,11 +74,19 @@ public final class VersionedPackage implements Parcelable { } /** + * @deprecated use {@link #getLongVersionCode()} instead. + */ + @Deprecated + public @VersionCode int getVersionCode() { + return (int) (mVersionCode & 0x7fffffff); + } + + /** * Gets the version code. * * @return The version code. */ - public @VersionCode int getVersionCode() { + public @VersionCode long getLongVersionCode() { return mVersionCode; } @@ -83,7 +103,7 @@ public final class VersionedPackage implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeString(mPackageName); - parcel.writeInt(mVersionCode); + parcel.writeLong(mVersionCode); } public static final Creator<VersionedPackage> CREATOR = new Creator<VersionedPackage>() { diff --git a/core/java/android/content/pm/crossprofile/CrossProfileApps.java b/core/java/android/content/pm/crossprofile/CrossProfileApps.java new file mode 100644 index 000000000000..c9f184a7da53 --- /dev/null +++ b/core/java/android/content/pm/crossprofile/CrossProfileApps.java @@ -0,0 +1,153 @@ +/* + * 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.content.res.Resources; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; + +import com.android.internal.R; +import com.android.internal.util.UserIcons; + +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; + private final UserManager mUserManager; + private final Resources mResources; + + /** @hide */ + public CrossProfileApps(Context context, ICrossProfileApps service) { + mContext = context; + mService = service; + mUserManager = context.getSystemService(UserManager.class); + mResources = context.getResources(); + } + + /** + * 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(); + } + } + + /** + * Return a label that calling app can show to user for the semantic of profile switching -- + * launching its own activity in specified user profile. For example, it may return + * "Switch to work" if the given user handle is the managed profile one. + * + * @param userHandle The UserHandle of the target profile, must be one of the users returned by + * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will + * be thrown. + * @return a label that calling app can show user for the semantic of launching its own + * activity in the specified user profile. + * + * @see #startMainActivity(ComponentName, UserHandle, Rect, Bundle) + */ + public @NonNull CharSequence getProfileSwitchingLabel(@NonNull UserHandle userHandle) { + verifyCanAccessUser(userHandle); + + final int stringRes = mUserManager.isManagedProfile(userHandle.getIdentifier()) + ? R.string.managed_profile_label + : R.string.user_owner_label; + return mResources.getString(stringRes); + } + + /** + * Return an icon that calling app can show to user for the semantic of profile switching -- + * launching its own activity in specified user profile. For example, it may return a briefcase + * icon if the given user handle is the managed profile one. + * + * @param userHandle The UserHandle of the target profile, must be one of the users returned by + * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will + * be thrown. + * @return an icon that calling app can show user for the semantic of launching its own + * activity in specified user profile. + * + * @see #startMainActivity(ComponentName, UserHandle, Rect, Bundle) + */ + public @NonNull Drawable getProfileSwitchingIcon(@NonNull UserHandle userHandle) { + verifyCanAccessUser(userHandle); + + final boolean isManagedProfile = + mUserManager.isManagedProfile(userHandle.getIdentifier()); + if (isManagedProfile) { + return mResources.getDrawable(R.drawable.ic_corp_badge, null); + } else { + return UserIcons.getDefaultUserIcon( + mResources, UserHandle.USER_SYSTEM, true /* light */); + } + } + + private void verifyCanAccessUser(UserHandle userHandle) { + if (!getTargetUserProfiles().contains(userHandle)) { + throw new SecurityException("Not allowed to access " + userHandle); + } + } +} diff --git a/core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl b/core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl new file mode 100644 index 000000000000..dd8d04f6cf0e --- /dev/null +++ b/core/java/android/content/pm/crossprofile/ICrossProfileApps.aidl @@ -0,0 +1,31 @@ +/* + * 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.content.ComponentName; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.UserHandle; + +/** + * @hide + */ +interface ICrossProfileApps { + void startActivityAsUser(in String callingPackage, in ComponentName component, in Rect sourceBounds, in Bundle startActivityOptions, in UserHandle user); + List<UserHandle> getTargetUserProfiles(in String callingPackage); +}
\ No newline at end of file diff --git a/core/java/android/content/pm/dex/ArtManager.java b/core/java/android/content/pm/dex/ArtManager.java new file mode 100644 index 000000000000..201cd8d32cc1 --- /dev/null +++ b/core/java/android/content/pm/dex/ArtManager.java @@ -0,0 +1,156 @@ +/** + * 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.content.pm.dex; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Slog; + +/** + * Class for retrieving various kinds of information related to the runtime artifacts of + * packages that are currently installed on the device. + * + * @hide + */ +@SystemApi +public class ArtManager { + private static final String TAG = "ArtManager"; + + /** The snapshot failed because the package was not found. */ + public static final int SNAPSHOT_FAILED_PACKAGE_NOT_FOUND = 0; + /** The snapshot failed because the package code path does not exist. */ + public static final int SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND = 1; + /** The snapshot failed because of an internal error (e.g. error during opening profiles). */ + public static final int SNAPSHOT_FAILED_INTERNAL_ERROR = 2; + + private IArtManager mArtManager; + + /** + * @hide + */ + public ArtManager(@NonNull IArtManager manager) { + mArtManager = manager; + } + + /** + * Snapshots the runtime profile for an apk belonging to the package {@code packageName}. + * The apk is identified by {@code codePath}. The calling process must have + * {@code android.permission.READ_RUNTIME_PROFILE} permission. + * + * The result will be posted on {@code handler} using the given {@code callback}. + * The profile being available as a read-only {@link android.os.ParcelFileDescriptor}. + * + * @param packageName the target package name + * @param codePath the code path for which the profile should be retrieved + * @param callback the callback which should be used for the result + * @param handler the handler which should be used to post the result + */ + @RequiresPermission(android.Manifest.permission.READ_RUNTIME_PROFILES) + public void snapshotRuntimeProfile(@NonNull String packageName, @NonNull String codePath, + @NonNull SnapshotRuntimeProfileCallback callback, @NonNull Handler handler) { + Slog.d(TAG, "Requesting profile snapshot for " + packageName + ":" + codePath); + + SnapshotRuntimeProfileCallbackDelegate delegate = + new SnapshotRuntimeProfileCallbackDelegate(callback, handler.getLooper()); + try { + mArtManager.snapshotRuntimeProfile(packageName, codePath, delegate); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Returns true if runtime profiles are enabled, false otherwise. + * + * The calling process must have {@code android.permission.READ_RUNTIME_PROFILE} permission. + */ + @RequiresPermission(android.Manifest.permission.READ_RUNTIME_PROFILES) + public boolean isRuntimeProfilingEnabled() { + try { + return mArtManager.isRuntimeProfilingEnabled(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + return false; + } + + /** + * Callback used for retrieving runtime profiles. + */ + public abstract static class SnapshotRuntimeProfileCallback { + /** + * Called when the profile snapshot finished with success. + * + * @param profileReadFd the file descriptor that can be used to read the profile. Note that + * the file might be empty (which is valid profile). + */ + public abstract void onSuccess(ParcelFileDescriptor profileReadFd); + + /** + * Called when the profile snapshot finished with an error. + * + * @param errCode the error code {@see SNAPSHOT_FAILED_PACKAGE_NOT_FOUND, + * SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND, SNAPSHOT_FAILED_INTERNAL_ERROR}. + */ + public abstract void onError(int errCode); + } + + private static class SnapshotRuntimeProfileCallbackDelegate + extends android.content.pm.dex.ISnapshotRuntimeProfileCallback.Stub + implements Handler.Callback { + private static final int MSG_SNAPSHOT_OK = 1; + private static final int MSG_ERROR = 2; + private final ArtManager.SnapshotRuntimeProfileCallback mCallback; + private final Handler mHandler; + + private SnapshotRuntimeProfileCallbackDelegate( + ArtManager.SnapshotRuntimeProfileCallback callback, Looper looper) { + mCallback = callback; + mHandler = new Handler(looper, this); + } + + @Override + public void onSuccess(ParcelFileDescriptor profileReadFd) { + mHandler.obtainMessage(MSG_SNAPSHOT_OK, profileReadFd).sendToTarget(); + } + + @Override + public void onError(int errCode) { + mHandler.obtainMessage(MSG_ERROR, errCode, 0).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_SNAPSHOT_OK: + mCallback.onSuccess((ParcelFileDescriptor) msg.obj); + break; + case MSG_ERROR: + mCallback.onError(msg.arg1); + break; + default: return false; + } + return true; + } + } +} diff --git a/core/java/android/content/pm/dex/IArtManager.aidl b/core/java/android/content/pm/dex/IArtManager.aidl new file mode 100644 index 000000000000..8cbb452344b2 --- /dev/null +++ b/core/java/android/content/pm/dex/IArtManager.aidl @@ -0,0 +1,44 @@ +/* +** 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.content.pm.dex; + +import android.content.pm.dex.ISnapshotRuntimeProfileCallback; + +/** + * A system service that provides access to runtime and compiler artifacts. + * + * @hide + */ +interface IArtManager { + /** + * Snapshots the runtime profile for an apk belonging to the package {@param packageName}. + * The apk is identified by {@param codePath}. The calling process must have + * {@code android.permission.READ_RUNTIME_PROFILE} permission. + * + * The result will be posted on {@param callback} with the profile being available as a + * read-only {@link android.os.ParcelFileDescriptor}. + */ + oneway void snapshotRuntimeProfile(in String packageName, + in String codePath, in ISnapshotRuntimeProfileCallback callback); + + /** + * Returns true if runtime profiles are enabled, false otherwise. + * + * The calling process must have {@code android.permission.READ_RUNTIME_PROFILE} permission. + */ + boolean isRuntimeProfilingEnabled(); +} diff --git a/core/java/android/content/pm/IPackageInstallObserver.aidl b/core/java/android/content/pm/dex/ISnapshotRuntimeProfileCallback.aidl index 613336537317..3b4838fc7824 100644 --- a/core/java/android/content/pm/IPackageInstallObserver.aidl +++ b/core/java/android/content/pm/dex/ISnapshotRuntimeProfileCallback.aidl @@ -1,6 +1,5 @@ /* -** -** Copyright 2007, 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. @@ -15,13 +14,16 @@ ** limitations under the License. */ -package android.content.pm; +package android.content.pm.dex; + +import android.os.ParcelFileDescriptor; /** - * API for installation callbacks from the Package Manager. + * Callback used to post the result of a profile-snapshot operation. + * * @hide */ -oneway interface IPackageInstallObserver { - void packageInstalled(in String packageName, int returnCode); +oneway interface ISnapshotRuntimeProfileCallback { + void onSuccess(in ParcelFileDescriptor profileReadFd); + void onError(int errCode); } - diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index f0adcd6cfb3e..78665609bdd4 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/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/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index f7cccd56f079..26efda108bb6 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -16,18 +16,29 @@ package android.content.res; +import static android.content.ConfigurationProto.DENSITY_DPI; +import static android.content.ConfigurationProto.FONT_SCALE; +import static android.content.ConfigurationProto.ORIENTATION; +import static android.content.ConfigurationProto.SCREEN_HEIGHT_DP; +import static android.content.ConfigurationProto.SCREEN_LAYOUT; +import static android.content.ConfigurationProto.SCREEN_WIDTH_DP; +import static android.content.ConfigurationProto.SMALLEST_SCREEN_WIDTH_DP; +import static android.content.ConfigurationProto.UI_MODE; +import static android.content.ConfigurationProto.WINDOW_CONFIGURATION; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; +import android.app.WindowConfiguration; import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.Config; -import android.graphics.Rect; import android.os.Build; import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import android.view.DisplayInfo; +import android.util.proto.ProtoOutputStream; import android.view.View; import com.android.internal.util.XmlUtils; @@ -42,7 +53,6 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Locale; - /** * This class describes all device configuration information that can * impact the resources the application retrieves. This includes both @@ -297,14 +307,13 @@ public final class Configuration implements Parcelable, Comparable<Configuration public int screenLayout; /** + * Configuration relating to the windowing state of the object associated with this + * Configuration. Contents of this field are not intended to affect resources, but need to be + * communicated and propagated at the same time as the rest of Configuration. * @hide - * {@link android.graphics.Rect} defining app bounds. The dimensions override usages of - * {@link DisplayInfo#appHeight} and {@link DisplayInfo#appWidth} and mirrors these values at - * the display level. Lower levels can override these values to provide custom bounds to enforce - * features such as a max aspect ratio. - * TODO(b/36812336): Move appBounds out of {@link Configuration}. */ - public Rect appBounds; + @TestApi + public final WindowConfiguration windowConfiguration = new WindowConfiguration(); /** @hide */ static public int resetScreenLayout(int curLayout) { @@ -895,9 +904,9 @@ public final class Configuration implements Parcelable, Comparable<Configuration compatScreenWidthDp = o.compatScreenWidthDp; compatScreenHeightDp = o.compatScreenHeightDp; compatSmallestScreenWidthDp = o.compatSmallestScreenWidthDp; - setAppBounds(o.appBounds); assetsSeq = o.assetsSeq; seq = o.seq; + windowConfiguration.setTo(o.windowConfiguration); } public String toString() { @@ -1046,9 +1055,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration case NAVIGATIONHIDDEN_YES: sb.append("/h"); break; default: sb.append("/"); sb.append(navigationHidden); break; } - if (appBounds != null) { - sb.append(" appBounds="); sb.append(appBounds); - } + sb.append(" winConfig="); sb.append(windowConfiguration); if (assetsSeq != 0) { sb.append(" as.").append(assetsSeq); } @@ -1060,6 +1067,55 @@ public final class Configuration implements Parcelable, Comparable<Configuration } /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.content.ConfigurationProto} + * + * @param protoOutputStream Stream to write the Configuration object to. + * @param fieldId Field Id of the Configuration as defined in the parent message + * @hide + */ + public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + final long token = protoOutputStream.start(fieldId); + protoOutputStream.write(FONT_SCALE, fontScale); + protoOutputStream.write(SCREEN_LAYOUT, screenLayout); + protoOutputStream.write(ORIENTATION, orientation); + protoOutputStream.write(UI_MODE, uiMode); + protoOutputStream.write(SCREEN_WIDTH_DP, screenWidthDp); + protoOutputStream.write(SCREEN_HEIGHT_DP, screenHeightDp); + protoOutputStream.write(SMALLEST_SCREEN_WIDTH_DP, smallestScreenWidthDp); + protoOutputStream.write(DENSITY_DPI, densityDpi); + windowConfiguration.writeToProto(protoOutputStream, WINDOW_CONFIGURATION); + protoOutputStream.end(token); + } + + /** + * Convert the UI mode to a human readable format. + * @hide + */ + public static String uiModeToString(int uiMode) { + switch (uiMode) { + case UI_MODE_TYPE_UNDEFINED: + return "UI_MODE_TYPE_UNDEFINED"; + case UI_MODE_TYPE_NORMAL: + return "UI_MODE_TYPE_NORMAL"; + case UI_MODE_TYPE_DESK: + return "UI_MODE_TYPE_DESK"; + case UI_MODE_TYPE_CAR: + return "UI_MODE_TYPE_CAR"; + case UI_MODE_TYPE_TELEVISION: + return "UI_MODE_TYPE_TELEVISION"; + case UI_MODE_TYPE_APPLIANCE: + return "UI_MODE_TYPE_APPLIANCE"; + case UI_MODE_TYPE_WATCH: + return "UI_MODE_TYPE_WATCH"; + case UI_MODE_TYPE_VR_HEADSET: + return "UI_MODE_TYPE_VR_HEADSET"; + default: + return Integer.toString(uiMode); + } + } + + /** * Set this object to the system defaults. */ public void setToDefaults() { @@ -1083,8 +1139,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration smallestScreenWidthDp = compatSmallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; densityDpi = DENSITY_DPI_UNDEFINED; assetsSeq = ASSETS_SEQ_UNDEFINED; - appBounds = null; seq = 0; + windowConfiguration.setToDefaults(); } /** @@ -1183,7 +1239,6 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_ORIENTATION; orientation = delta.orientation; } - if (((delta.screenLayout & SCREENLAYOUT_SIZE_MASK) != SCREENLAYOUT_SIZE_UNDEFINED) && (delta.screenLayout & SCREENLAYOUT_SIZE_MASK) != (screenLayout & SCREENLAYOUT_SIZE_MASK)) { @@ -1271,10 +1326,6 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (delta.compatSmallestScreenWidthDp != SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) { compatSmallestScreenWidthDp = delta.compatSmallestScreenWidthDp; } - if (delta.appBounds != null && !delta.appBounds.equals(appBounds)) { - changed |= ActivityInfo.CONFIG_SCREEN_SIZE; - setAppBounds(delta.appBounds); - } if (delta.assetsSeq != ASSETS_SEQ_UNDEFINED && delta.assetsSeq != assetsSeq) { changed |= ActivityInfo.CONFIG_ASSETS_PATHS; assetsSeq = delta.assetsSeq; @@ -1282,6 +1333,9 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (delta.seq != 0) { seq = delta.seq; } + if (windowConfiguration.updateFrom(delta.windowConfiguration) != 0) { + changed |= ActivityInfo.CONFIG_WINDOW_CONFIGURATION; + } return changed; } @@ -1433,13 +1487,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_ASSETS_PATHS; } - // Make sure that one of the values is not null and that they are not equal. - if ((compareUndefined || delta.appBounds != null) - && appBounds != delta.appBounds - && (appBounds == null || (!publicOnly && !appBounds.equals(delta.appBounds)) - || (publicOnly && (appBounds.width() != delta.appBounds.width() - || appBounds.height() != delta.appBounds.height())))) { - changed |= ActivityInfo.CONFIG_SCREEN_SIZE; + // WindowConfiguration differences aren't considered public... + if (!publicOnly + && windowConfiguration.diff(delta.windowConfiguration, compareUndefined) != 0) { + changed |= ActivityInfo.CONFIG_WINDOW_CONFIGURATION; } return changed; @@ -1537,7 +1588,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeInt(compatScreenWidthDp); dest.writeInt(compatScreenHeightDp); dest.writeInt(compatSmallestScreenWidthDp); - dest.writeValue(appBounds); + dest.writeValue(windowConfiguration); dest.writeInt(assetsSeq); dest.writeInt(seq); } @@ -1573,7 +1624,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration compatScreenWidthDp = source.readInt(); compatScreenHeightDp = source.readInt(); compatSmallestScreenWidthDp = source.readInt(); - appBounds = (Rect) source.readValue(null); + windowConfiguration.setTo((WindowConfiguration) source.readValue(null)); assetsSeq = source.readInt(); seq = source.readInt(); } @@ -1663,21 +1714,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (n != 0) return n; n = this.assetsSeq - that.assetsSeq; if (n != 0) return n; - - if (this.appBounds == null && that.appBounds != null) { - return 1; - } else if (this.appBounds != null && that.appBounds == null) { - return -1; - } else if (this.appBounds != null && that.appBounds != null) { - n = this.appBounds.left - that.appBounds.left; - if (n != 0) return n; - n = this.appBounds.top - that.appBounds.top; - if (n != 0) return n; - n = this.appBounds.right - that.appBounds.right; - if (n != 0) return n; - n = this.appBounds.bottom - that.appBounds.bottom; - if (n != 0) return n; - } + n = windowConfiguration.compareTo(that.windowConfiguration); + if (n != 0) return n; // if (n != 0) return n; return n; @@ -1768,33 +1806,6 @@ public final class Configuration implements Parcelable, Comparable<Configuration /** * @hide * - * Helper method for setting the app bounds. - */ - public void setAppBounds(Rect rect) { - if (rect == null) { - appBounds = null; - return; - } - - setAppBounds(rect.left, rect.top, rect.right, rect.bottom); - } - - /** - * @hide - * - * Helper method for setting the app bounds. - */ - public void setAppBounds(int left, int top, int right, int bottom) { - if (appBounds == null) { - appBounds = new Rect(); - } - - appBounds.set(left, top, right, bottom); - } - - /** - * @hide - * * Clears the locale without changing layout direction. */ public void clearLocales() { @@ -2094,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; @@ -2282,6 +2294,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (base.assetsSeq != change.assetsSeq) { delta.assetsSeq = change.assetsSeq; } + + if (!base.windowConfiguration.equals(change.windowConfiguration)) { + delta.windowConfiguration.setTo(change.windowConfiguration); + } return delta; } @@ -2354,10 +2370,9 @@ public final class Configuration implements Parcelable, Comparable<Configuration SMALLEST_SCREEN_WIDTH_DP_UNDEFINED); configOut.densityDpi = XmlUtils.readIntAttribute(parser, XML_ATTR_DENSITY, DENSITY_DPI_UNDEFINED); - configOut.appBounds = - Rect.unflattenFromString(XmlUtils.readStringAttribute(parser, XML_ATTR_APP_BOUNDS)); - // For persistence, we don't care about assetsSeq, so do not read it out. + // For persistence, we don't care about assetsSeq and WindowConfiguration, so do not read it + // out. } @@ -2427,11 +2442,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration XmlUtils.writeIntAttribute(xml, XML_ATTR_DENSITY, config.densityDpi); } - if (config.appBounds != null) { - XmlUtils.writeStringAttribute(xml, XML_ATTR_APP_BOUNDS, - config.appBounds.flattenToString()); - } - - // For persistence, we do not care about assetsSeq, so do not write it out. + // For persistence, we do not care about assetsSeq and window configuration, so do not write + // it out. } } diff --git a/core/java/android/content/res/FontResourcesParser.java b/core/java/android/content/res/FontResourcesParser.java index 042eb87f6fb9..6a4aae66c848 100644 --- a/core/java/android/content/res/FontResourcesParser.java +++ b/core/java/android/content/res/FontResourcesParser.java @@ -15,7 +15,6 @@ */ package android.content.res; -import com.android.internal.R; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Typeface; @@ -23,6 +22,8 @@ import android.util.AttributeSet; import android.util.Log; import android.util.Xml; +import com.android.internal.R; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -78,12 +79,17 @@ public class FontResourcesParser { private final @NonNull String mFileName; private int mWeight; private int mItalic; + private int mTtcIndex; + private String mVariationSettings; private int mResourceId; - public FontFileResourceEntry(@NonNull String fileName, int weight, int italic) { + public FontFileResourceEntry(@NonNull String fileName, int weight, int italic, + @Nullable String variationSettings, int ttcIndex) { mFileName = fileName; mWeight = weight; mItalic = italic; + mVariationSettings = variationSettings; + mTtcIndex = ttcIndex; } public @NonNull String getFileName() { @@ -97,6 +103,14 @@ public class FontResourcesParser { public int getItalic() { return mItalic; } + + public @Nullable String getVariationSettings() { + return mVariationSettings; + } + + public int getTtcIndex() { + return mTtcIndex; + } } // A class represents file based font-family element in xml file. @@ -203,6 +217,9 @@ 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(); while (parser.next() != XmlPullParser.END_TAG) { @@ -211,7 +228,7 @@ public class FontResourcesParser { if (filename == null) { return null; } - return new FontFileResourceEntry(filename, weight, italic); + return new FontFileResourceEntry(filename, weight, italic, variationSettings, ttcIndex); } private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index a75372ff6170..f84ec65fe0ae 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -16,8 +16,7 @@ package android.database; -import dalvik.system.CloseGuard; - +import android.annotation.BytesLong; import android.content.res.Resources; import android.database.sqlite.SQLiteClosable; import android.database.sqlite.SQLiteException; @@ -26,8 +25,10 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.Process; import android.util.Log; -import android.util.SparseIntArray; import android.util.LongSparseArray; +import android.util.SparseIntArray; + +import dalvik.system.CloseGuard; /** * A buffer containing multiple cursor rows. @@ -94,19 +95,29 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { * @param name The name of the cursor window, or null if none. */ public CursorWindow(String name) { + this(name, getCursorWindowSize()); + } + + /** + * Creates a new empty cursor window and gives it a name. + * <p> + * The cursor initially has no rows or columns. Call {@link #setNumColumns(int)} to + * set the number of columns before adding any rows to the cursor. + * </p> + * + * @param name The name of the cursor window, or null if none. + * @param windowSizeBytes Size of cursor window in bytes. + * <p><strong>Note:</strong> Memory is dynamically allocated as data rows are added to the + * window. Depending on the amount of data stored, the actual amount of memory allocated can be + * lower than specified size, but cannot exceed it. + */ + public CursorWindow(String name, @BytesLong long windowSizeBytes) { mStartPos = 0; mName = name != null && name.length() != 0 ? name : "<unnamed>"; - if (sCursorWindowSize < 0) { - /** The cursor window size. resource xml file specifies the value in kB. - * convert it to bytes here by multiplying with 1024. - */ - sCursorWindowSize = Resources.getSystem().getInteger( - com.android.internal.R.integer.config_cursorWindowSize) * 1024; - } - mWindowPtr = nativeCreate(mName, sCursorWindowSize); + mWindowPtr = nativeCreate(mName, (int) windowSizeBytes); if (mWindowPtr == 0) { throw new CursorWindowAllocationException("Cursor window allocation of " + - (sCursorWindowSize / 1024) + " kb failed. " + printStats()); + windowSizeBytes + " bytes failed. " + printStats()); } mCloseGuard.open("close"); recordNewWindow(Binder.getCallingPid(), mWindowPtr); @@ -773,6 +784,16 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { return "# Open Cursors=" + total + s; } + private static int getCursorWindowSize() { + if (sCursorWindowSize < 0) { + // The cursor window size. resource xml file specifies the value in kB. + // convert it to bytes here by multiplying with 1024. + sCursorWindowSize = Resources.getSystem().getInteger( + com.android.internal.R.integer.config_cursorWindowSize) * 1024; + } + return sCursorWindowSize; + } + @Override public String toString() { return getName() + " {" + Long.toHexString(mWindowPtr) + "}"; diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 8cd3d7b5bc68..3d019f07cb84 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -1408,6 +1408,12 @@ public class DatabaseUtils { } else if (prefixSql.equals("END")) { return STATEMENT_COMMIT; } else if (prefixSql.equals("ROL")) { + boolean isRollbackToSavepoint = sql.toUpperCase(Locale.ROOT).contains(" TO "); + if (isRollbackToSavepoint) { + Log.w(TAG, "Statement '" + sql + + "' may not work on API levels 16-27, use ';" + sql + "' instead"); + return STATEMENT_OTHER; + } return STATEMENT_ABORT; } else if (prefixSql.equals("BEG")) { return STATEMENT_BEGIN; diff --git a/core/java/android/database/MergeCursor.java b/core/java/android/database/MergeCursor.java index 2c25db765a2b..272cfa24181c 100644 --- a/core/java/android/database/MergeCursor.java +++ b/core/java/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/core/java/android/database/OWNERS b/core/java/android/database/OWNERS new file mode 100644 index 000000000000..84e81e4285d1 --- /dev/null +++ b/core/java/android/database/OWNERS @@ -0,0 +1,2 @@ +fkupolov@google.com +omakoto@google.com
\ No newline at end of file diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index f894f0536b52..2c93a7fe26cd 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -104,7 +104,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private PreparedStatement mPreparedStatementPool; // The recent operations log. - private final OperationLog mRecentOperations = new OperationLog(); + private final OperationLog mRecentOperations; // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY) private long mConnectionPtr; @@ -162,6 +162,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen SQLiteDatabaseConfiguration configuration, int connectionId, boolean primaryConnection) { mPool = pool; + mRecentOperations = new OperationLog(mPool); mConfiguration = new SQLiteDatabaseConfiguration(configuration); mConnectionId = connectionId; mIsPrimaryConnection = primaryConnection; @@ -288,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); } } } @@ -307,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; } @@ -413,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. @@ -1298,6 +1305,11 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS]; private int mIndex; private int mGeneration; + private final SQLiteConnectionPool mPool; + + OperationLog(SQLiteConnectionPool pool) { + mPool = pool; + } public int beginOperation(String kind, String sql, Object[] bindArgs) { synchronized (mOperations) { @@ -1381,8 +1393,10 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } operation.mEndTime = SystemClock.uptimeMillis(); operation.mFinished = true; + final long execTime = operation.mEndTime - operation.mStartTime; + mPool.onStatementExecuted(execTime); return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery( - operation.mEndTime - operation.mStartTime); + execTime); } return false; } @@ -1426,11 +1440,16 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen int index = mIndex; Operation operation = mOperations[index]; if (operation != null) { + // Note: SimpleDateFormat is not thread-safe, cannot be compile-time created, + // and is relatively expensive to create during preloading. This method is only + // used when dumping a connection, which is a rare (mainly error) case. + SimpleDateFormat opDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); int n = 0; do { StringBuilder msg = new StringBuilder(); msg.append(" ").append(n).append(": ["); - msg.append(operation.getFormattedStartTime()); + String formattedStartTime = opDF.format(new Date(operation.mStartWallTime)); + msg.append(formattedStartTime); msg.append("] "); operation.describe(msg, verbose); printer.println(msg.toString()); @@ -1518,12 +1537,5 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen return methodName; } - private String getFormattedStartTime() { - // Note: SimpleDateFormat is not thread-safe, cannot be compile-time created, and is - // relatively expensive to create during preloading. This method is only used - // when dumping a connection, which is a rare (mainly error) case. So: - // DO NOT CACHE. - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(mStartWallTime)); - } } } diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java index b66bf18fca1d..5adb11964c8c 100644 --- a/core/java/android/database/sqlite/SQLiteConnectionPool.java +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.LockSupport; /** @@ -102,6 +103,8 @@ public final class SQLiteConnectionPool implements Closeable { @GuardedBy("mLock") private IdleConnectionHandler mIdleConnectionHandler; + private final AtomicLong mTotalExecutionTimeCounter = new AtomicLong(0); + // Describes what should happen to an acquired connection when it is returned to the pool. enum AcquiredConnectionStatus { // The connection should be returned to the pool as usual. @@ -523,6 +526,10 @@ public final class SQLiteConnectionPool implements Closeable { mConnectionLeaked.set(true); } + void onStatementExecuted(long executionTimeMs) { + mTotalExecutionTimeCounter.addAndGet(executionTimeMs); + } + // Can't throw. private void closeAvailableConnectionsAndLogExceptionsLocked() { closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); @@ -563,6 +570,16 @@ public final class SQLiteConnectionPool implements Closeable { mAvailableNonPrimaryConnections.clear(); } + /** + * Close non-primary connections that are not currently in use. This method is safe to use + * in finalize block as it doesn't throw RuntimeExceptions. + */ + void closeAvailableNonPrimaryConnectionsAndLogExceptions() { + synchronized (mLock) { + closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); + } + } + // Can't throw. private void closeExcessConnectionsAndLogExceptionsLocked() { int availableCount = mAvailableNonPrimaryConnections.size(); @@ -1076,6 +1093,7 @@ public final class SQLiteConnectionPool implements Closeable { printer.println("Connection pool for " + mConfiguration.path + ":"); printer.println(" Open: " + mIsOpen); printer.println(" Max connections: " + mMaxConnectionPoolSize); + printer.println(" Total execution time: " + mTotalExecutionTimeCounter); if (mConfiguration.isLookasideConfigSet()) { printer.println(" Lookaside config: sz=" + mConfiguration.lookasideSlotSize + " cnt=" + mConfiguration.lookasideSlotCount); diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index 2dc5ca43e7a1..13e6f7182e8a 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/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/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index df0e262b712f..09bb9c69dc09 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/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; } @@ -1735,7 +1740,8 @@ public final class SQLiteDatabase extends SQLiteClosable { private int executeSql(String sql, Object[] bindArgs) throws SQLException { acquireReference(); try { - if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { + final int statementType = DatabaseUtils.getSqlStatementType(sql); + if (statementType == DatabaseUtils.STATEMENT_ATTACH) { boolean disableWal = false; synchronized (mLock) { if (!mHasAttachedDbsLocked) { @@ -1749,11 +1755,14 @@ public final class SQLiteDatabase extends SQLiteClosable { } } - SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); - try { + try (SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs)) { return statement.executeUpdateDelete(); } finally { - statement.close(); + // If schema was updated, close non-primary connections, otherwise they might + // have outdated schema information + if (statementType == DatabaseUtils.STATEMENT_DDL) { + mConnectionPoolLocked.closeAvailableNonPrimaryConnectionsAndLogExceptions(); + } } } finally { releaseReference(); @@ -2070,15 +2079,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 +2310,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 +2391,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 +2432,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 +2444,8 @@ public final class SQLiteDatabase extends SQLiteClosable { mOpenFlags = params.mOpenFlags; mCursorFactory = params.mCursorFactory; mErrorHandler = params.mErrorHandler; + mJournalMode = params.mJournalMode; + mSyncMode = params.mSyncMode; } /** @@ -2532,6 +2577,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 +2608,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 +2623,6 @@ public final class SQLiteDatabase extends SQLiteClosable { }) @Retention(RetentionPolicy.SOURCE) public @interface DatabaseOpenFlags {} + } + diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java index 34c9b3395d1a..a14df1ebcbc6 100644 --- a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java +++ b/core/java/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/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java index 94d5555c4c24..d6d9764c7c38 100644 --- a/core/java/android/database/sqlite/SQLiteGlobal.java +++ b/core/java/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/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index cc9e0f4ded84..49f357e6cb10 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/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/core/java/android/hardware/GeomagneticField.java b/core/java/android/hardware/GeomagneticField.java index eb26ee50dc84..94f2ac085965 100644 --- a/core/java/android/hardware/GeomagneticField.java +++ b/core/java/android/hardware/GeomagneticField.java @@ -26,7 +26,7 @@ import java.util.GregorianCalendar; * <p>This uses the World Magnetic Model produced by the United States National * Geospatial-Intelligence Agency. More details about the model can be found at * <a href="http://www.ngdc.noaa.gov/geomag/WMM/DoDWMM.shtml">http://www.ngdc.noaa.gov/geomag/WMM/DoDWMM.shtml</a>. - * This class currently uses WMM-2010 which is valid until 2015, but should + * This class currently uses WMM-2015 which is valid until 2020, but should * produce acceptable results for several years after that. Future versions of * Android may use a newer version of the model. */ @@ -48,69 +48,69 @@ public class GeomagneticField { static private final float EARTH_REFERENCE_RADIUS_KM = 6371.2f; // These coefficients and the formulae used below are from: - // NOAA Technical Report: The US/UK World Magnetic Model for 2010-2015 + // NOAA Technical Report: The US/UK World Magnetic Model for 2015-2020 static private final float[][] G_COEFF = new float[][] { { 0.0f }, - { -29496.6f, -1586.3f }, - { -2396.6f, 3026.1f, 1668.6f }, - { 1340.1f, -2326.2f, 1231.9f, 634.0f }, - { 912.6f, 808.9f, 166.7f, -357.1f, 89.4f }, - { -230.9f, 357.2f, 200.3f, -141.1f, -163.0f, -7.8f }, - { 72.8f, 68.6f, 76.0f, -141.4f, -22.8f, 13.2f, -77.9f }, - { 80.5f, -75.1f, -4.7f, 45.3f, 13.9f, 10.4f, 1.7f, 4.9f }, - { 24.4f, 8.1f, -14.5f, -5.6f, -19.3f, 11.5f, 10.9f, -14.1f, -3.7f }, - { 5.4f, 9.4f, 3.4f, -5.2f, 3.1f, -12.4f, -0.7f, 8.4f, -8.5f, -10.1f }, - { -2.0f, -6.3f, 0.9f, -1.1f, -0.2f, 2.5f, -0.3f, 2.2f, 3.1f, -1.0f, -2.8f }, - { 3.0f, -1.5f, -2.1f, 1.7f, -0.5f, 0.5f, -0.8f, 0.4f, 1.8f, 0.1f, 0.7f, 3.8f }, - { -2.2f, -0.2f, 0.3f, 1.0f, -0.6f, 0.9f, -0.1f, 0.5f, -0.4f, -0.4f, 0.2f, -0.8f, 0.0f } }; + { -29438.5f, -1501.1f }, + { -2445.3f, 3012.5f, 1676.6f }, + { 1351.1f, -2352.3f, 1225.6f, 581.9f }, + { 907.2f, 813.7f, 120.3f, -335.0f, 70.3f }, + { -232.6f, 360.1f, 192.4f, -141.0f, -157.4f, 4.3f }, + { 69.5f, 67.4f, 72.8f, -129.8f, -29.0f, 13.2f, -70.9f }, + { 81.6f, -76.1f, -6.8f, 51.9f, 15.0f, 9.3f, -2.8f, 6.7f }, + { 24.0f, 8.6f, -16.9f, -3.2f, -20.6f, 13.3f, 11.7f, -16.0f, -2.0f }, + { 5.4f, 8.8f, 3.1f, -3.1f, 0.6f, -13.3f, -0.1f, 8.7f, -9.1f, -10.5f }, + { -1.9f, -6.5f, 0.2f, 0.6f, -0.6f, 1.7f, -0.7f, 2.1f, 2.3f, -1.8f, -3.6f }, + { 3.1f, -1.5f, -2.3f, 2.1f, -0.9f, 0.6f, -0.7f, 0.2f, 1.7f, -0.2f, 0.4f, 3.5f }, + { -2.0f, -0.3f, 0.4f, 1.3f, -0.9f, 0.9f, 0.1f, 0.5f, -0.4f, -0.4f, 0.2f, -0.9f, 0.0f } }; static private final float[][] H_COEFF = new float[][] { { 0.0f }, - { 0.0f, 4944.4f }, - { 0.0f, -2707.7f, -576.1f }, - { 0.0f, -160.2f, 251.9f, -536.6f }, - { 0.0f, 286.4f, -211.2f, 164.3f, -309.1f }, - { 0.0f, 44.6f, 188.9f, -118.2f, 0.0f, 100.9f }, - { 0.0f, -20.8f, 44.1f, 61.5f, -66.3f, 3.1f, 55.0f }, - { 0.0f, -57.9f, -21.1f, 6.5f, 24.9f, 7.0f, -27.7f, -3.3f }, - { 0.0f, 11.0f, -20.0f, 11.9f, -17.4f, 16.7f, 7.0f, -10.8f, 1.7f }, - { 0.0f, -20.5f, 11.5f, 12.8f, -7.2f, -7.4f, 8.0f, 2.1f, -6.1f, 7.0f }, - { 0.0f, 2.8f, -0.1f, 4.7f, 4.4f, -7.2f, -1.0f, -3.9f, -2.0f, -2.0f, -8.3f }, - { 0.0f, 0.2f, 1.7f, -0.6f, -1.8f, 0.9f, -0.4f, -2.5f, -1.3f, -2.1f, -1.9f, -1.8f }, - { 0.0f, -0.9f, 0.3f, 2.1f, -2.5f, 0.5f, 0.6f, 0.0f, 0.1f, 0.3f, -0.9f, -0.2f, 0.9f } }; + { 0.0f, 4796.2f }, + { 0.0f, -2845.6f, -642.0f }, + { 0.0f, -115.3f, 245.0f, -538.3f }, + { 0.0f, 283.4f, -188.6f, 180.9f, -329.5f }, + { 0.0f, 47.4f, 196.9f, -119.4f, 16.1f, 100.1f }, + { 0.0f, -20.7f, 33.2f, 58.8f, -66.5f, 7.3f, 62.5f }, + { 0.0f, -54.1f, -19.4f, 5.6f, 24.4f, 3.3f, -27.5f, -2.3f }, + { 0.0f, 10.2f, -18.1f, 13.2f, -14.6f, 16.2f, 5.7f, -9.1f, 2.2f }, + { 0.0f, -21.6f, 10.8f, 11.7f, -6.8f, -6.9f, 7.8f, 1.0f, -3.9f, 8.5f }, + { 0.0f, 3.3f, -0.3f, 4.6f, 4.4f, -7.9f, -0.6f, -4.1f, -2.8f, -1.1f, -8.7f }, + { 0.0f, -0.1f, 2.1f, -0.7f, -1.1f, 0.7f, -0.2f, -2.1f, -1.5f, -2.5f, -2.0f, -2.3f }, + { 0.0f, -1.0f, 0.5f, 1.8f, -2.2f, 0.3f, 0.7f, -0.1f, 0.3f, 0.2f, -0.9f, -0.2f, 0.7f } }; static private final float[][] DELTA_G = new float[][] { { 0.0f }, - { 11.6f, 16.5f }, - { -12.1f, -4.4f, 1.9f }, - { 0.4f, -4.1f, -2.9f, -7.7f }, - { -1.8f, 2.3f, -8.7f, 4.6f, -2.1f }, - { -1.0f, 0.6f, -1.8f, -1.0f, 0.9f, 1.0f }, - { -0.2f, -0.2f, -0.1f, 2.0f, -1.7f, -0.3f, 1.7f }, - { 0.1f, -0.1f, -0.6f, 1.3f, 0.4f, 0.3f, -0.7f, 0.6f }, - { -0.1f, 0.1f, -0.6f, 0.2f, -0.2f, 0.3f, 0.3f, -0.6f, 0.2f }, - { 0.0f, -0.1f, 0.0f, 0.3f, -0.4f, -0.3f, 0.1f, -0.1f, -0.4f, -0.2f }, - { 0.0f, 0.0f, -0.1f, 0.2f, 0.0f, -0.1f, -0.2f, 0.0f, -0.1f, -0.2f, -0.2f }, - { 0.0f, 0.0f, 0.0f, 0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -0.1f, 0.0f }, - { 0.0f, 0.0f, 0.1f, 0.1f, -0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -0.1f, 0.1f } }; + { 10.7f, 17.9f }, + { -8.6f, -3.3f, 2.4f }, + { 3.1f, -6.2f, -0.4f, -10.4f }, + { -0.4f, 0.8f, -9.2f, 4.0f, -4.2f }, + { -0.2f, 0.1f, -1.4f, 0.0f, 1.3f, 3.8f }, + { -0.5f, -0.2f, -0.6f, 2.4f, -1.1f, 0.3f, 1.5f }, + { 0.2f, -0.2f, -0.4f, 1.3f, 0.2f, -0.4f, -0.9f, 0.3f }, + { 0.0f, 0.1f, -0.5f, 0.5f, -0.2f, 0.4f, 0.2f, -0.4f, 0.3f }, + { 0.0f, -0.1f, -0.1f, 0.4f, -0.5f, -0.2f, 0.1f, 0.0f, -0.2f, -0.1f }, + { 0.0f, 0.0f, -0.1f, 0.3f, -0.1f, -0.1f, -0.1f, 0.0f, -0.2f, -0.1f, -0.2f }, + { 0.0f, 0.0f, -0.1f, 0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -0.1f, -0.1f }, + { 0.1f, 0.0f, 0.0f, 0.1f, -0.1f, 0.0f, 0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f } }; static private final float[][] DELTA_H = new float[][] { { 0.0f }, - { 0.0f, -25.9f }, - { 0.0f, -22.5f, -11.8f }, - { 0.0f, 7.3f, -3.9f, -2.6f }, - { 0.0f, 1.1f, 2.7f, 3.9f, -0.8f }, - { 0.0f, 0.4f, 1.8f, 1.2f, 4.0f, -0.6f }, - { 0.0f, -0.2f, -2.1f, -0.4f, -0.6f, 0.5f, 0.9f }, - { 0.0f, 0.7f, 0.3f, -0.1f, -0.1f, -0.8f, -0.3f, 0.3f }, - { 0.0f, -0.1f, 0.2f, 0.4f, 0.4f, 0.1f, -0.1f, 0.4f, 0.3f }, - { 0.0f, 0.0f, -0.2f, 0.0f, -0.1f, 0.1f, 0.0f, -0.2f, 0.3f, 0.2f }, - { 0.0f, 0.1f, -0.1f, 0.0f, -0.1f, -0.1f, 0.0f, -0.1f, -0.2f, 0.0f, -0.1f }, - { 0.0f, 0.0f, 0.1f, 0.0f, 0.1f, 0.0f, 0.1f, 0.0f, -0.1f, -0.1f, 0.0f, -0.1f }, - { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f } }; + { 0.0f, -26.8f }, + { 0.0f, -27.1f, -13.3f }, + { 0.0f, 8.4f, -0.4f, 2.3f }, + { 0.0f, -0.6f, 5.3f, 3.0f, -5.3f }, + { 0.0f, 0.4f, 1.6f, -1.1f, 3.3f, 0.1f }, + { 0.0f, 0.0f, -2.2f, -0.7f, 0.1f, 1.0f, 1.3f }, + { 0.0f, 0.7f, 0.5f, -0.2f, -0.1f, -0.7f, 0.1f, 0.1f }, + { 0.0f, -0.3f, 0.3f, 0.3f, 0.6f, -0.1f, -0.2f, 0.3f, 0.0f }, + { 0.0f, -0.2f, -0.1f, -0.2f, 0.1f, 0.1f, 0.0f, -0.2f, 0.4f, 0.3f }, + { 0.0f, 0.1f, -0.1f, 0.0f, 0.0f, -0.2f, 0.1f, -0.1f, -0.2f, 0.1f, -0.1f }, + { 0.0f, 0.0f, 0.1f, 0.0f, 0.1f, 0.0f, 0.0f, 0.1f, 0.0f, -0.1f, 0.0f, -0.1f }, + { 0.0f, 0.0f, 0.0f, -0.1f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f } }; static private final long BASE_TIME = - new GregorianCalendar(2010, 1, 1).getTimeInMillis(); + new GregorianCalendar(2015, 1, 1).getTimeInMillis(); // The ratio between the Gauss-normalized associated Legendre functions and // the Schmid quasi-normalized ones. Compute these once staticly since they @@ -190,7 +190,7 @@ public class GeomagneticField { // We now compute the magnetic field strength given the geocentric // location. The magnetic field is the derivative of the potential // function defined by the model. See NOAA Technical Report: The US/UK - // World Magnetic Model for 2010-2015 for the derivation. + // World Magnetic Model for 2015-2020 for the derivation. float gcX = 0.0f; // Geocentric northwards component. float gcY = 0.0f; // Geocentric eastwards component. float gcZ = 0.0f; // Geocentric downwards component. diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java index 7049628b3590..b111ad30f27d 100644 --- a/core/java/android/hardware/HardwareBuffer.java +++ b/core/java/android/hardware/HardwareBuffer.java @@ -17,6 +17,7 @@ package android.hardware; import android.annotation.IntDef; +import android.annotation.LongDef; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; @@ -70,7 +71,7 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {USAGE_CPU_READ_RARELY, USAGE_CPU_READ_OFTEN, + @LongDef(flag = true, value = {USAGE_CPU_READ_RARELY, USAGE_CPU_READ_OFTEN, USAGE_CPU_WRITE_RARELY, USAGE_CPU_WRITE_OFTEN, USAGE_GPU_SAMPLED_IMAGE, USAGE_GPU_COLOR_OUTPUT, USAGE_PROTECTED_CONTENT, USAGE_VIDEO_ENCODE, USAGE_GPU_DATA_BUFFER, USAGE_SENSOR_DIRECT_DATA}) diff --git a/core/java/android/hardware/camera2/CameraCaptureSession.java b/core/java/android/hardware/camera2/CameraCaptureSession.java index da771e48d85e..ff69bd89564c 100644 --- a/core/java/android/hardware/camera2/CameraCaptureSession.java +++ b/core/java/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/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 46ad3f0eccc9..3a3048ef1de2 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -1280,11 +1280,11 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <ul> * <li>Processed (but stalling): any non-RAW format with a stallDurations > 0. * Typically {@link android.graphics.ImageFormat#JPEG JPEG format}.</li> - * <li>Raw formats: {@link android.graphics.ImageFormat#RAW_SENSOR RAW_SENSOR}, {@link android.graphics.ImageFormat#RAW10 RAW10}, or {@link android.graphics.ImageFormat#RAW12 RAW12}.</li> - * <li>Processed (but not-stalling): any non-RAW format without a stall duration. - * Typically {@link android.graphics.ImageFormat#YUV_420_888 YUV_420_888}, - * {@link android.graphics.ImageFormat#NV21 NV21}, or - * {@link android.graphics.ImageFormat#YV12 YV12}.</li> + * <li>Raw formats: {@link android.graphics.ImageFormat#RAW_SENSOR RAW_SENSOR}, {@link android.graphics.ImageFormat#RAW10 RAW10}, or + * {@link android.graphics.ImageFormat#RAW12 RAW12}.</li> + * <li>Processed (but not-stalling): any non-RAW format without a stall duration. Typically + * {@link android.graphics.ImageFormat#YUV_420_888 YUV_420_888}, + * {@link android.graphics.ImageFormat#NV21 NV21}, or {@link android.graphics.ImageFormat#YV12 YV12}.</li> * </ul> * <p><b>Range of valid values:</b><br></p> * <p>For processed (and stalling) format streams, >= 1.</p> @@ -1376,8 +1376,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * CPU resources that will consume more power. The image format for this kind of an output stream can * be any non-<code>RAW</code> and supported format provided by {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}.</p> * <p>A processed and stalling format is defined as any non-RAW format with a stallDurations - * > 0. Typically only the {@link android.graphics.ImageFormat#JPEG JPEG format} is a - * stalling format.</p> + * > 0. Typically only the {@link android.graphics.ImageFormat#JPEG JPEG format} is a stalling format.</p> * <p>For full guarantees, query {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration } with a * processed format -- it will return a non-0 value for a stalling stream.</p> * <p>LEGACY devices will support up to 1 processing/stalling stream.</p> @@ -1535,8 +1534,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<int[]>("android.request.availableRequestKeys", int[].class); /** - * <p>A list of all keys that the camera device has available - * to use with {@link android.hardware.camera2.CaptureResult }.</p> + * <p>A list of all keys that the camera device has available to use with {@link android.hardware.camera2.CaptureResult }.</p> * <p>Attempting to get a key from a CaptureResult that is not * listed here will always return a <code>null</code> value. Getting a key from * a CaptureResult that is listed here will generally never return a <code>null</code> @@ -1561,8 +1559,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<int[]>("android.request.availableResultKeys", int[].class); /** - * <p>A list of all keys that the camera device has available - * to use with {@link android.hardware.camera2.CameraCharacteristics }.</p> + * <p>A list of all keys that the camera device has available to use with {@link android.hardware.camera2.CameraCharacteristics }.</p> * <p>This entry follows the same rules as * android.request.availableResultKeys (except that it applies for * CameraCharacteristics instead of CaptureResult). See above for more @@ -1843,8 +1840,6 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} and * android.scaler.availableStallDurations for more details about * calculating the max frame rate.</p> - * <p>(Keep in sync with - * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration })</p> * <p><b>Units</b>: (format, width, height, ns) x n</p> * <p>This key is available on all devices.</p> * @@ -1905,14 +1900,13 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <ul> * <li>{@link android.graphics.ImageFormat#YUV_420_888 }</li> * <li>{@link android.graphics.ImageFormat#RAW10 }</li> + * <li>{@link android.graphics.ImageFormat#RAW12 }</li> * </ul> * <p>All other formats may or may not have an allowed stall duration on * a per-capability basis; refer to {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} * for more details.</p> * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} for more information about * calculating the max frame rate (absent stalls).</p> - * <p>(Keep up to date with - * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration } )</p> * <p><b>Units</b>: (format, width, height, ns) x n</p> * <p>This key is available on all devices.</p> * @@ -2195,9 +2189,9 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * the raw buffers produced by this sensor.</p> * <p>If a camera device supports raw sensor formats, either this or * {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE android.sensor.info.preCorrectionActiveArraySize} is the maximum dimensions for the raw - * output formats listed in {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} (this depends on - * whether or not the image sensor returns buffers containing pixels that are not - * part of the active array region for blacklevel calibration or other purposes).</p> + * output formats listed in {@link android.hardware.camera2.params.StreamConfigurationMap } + * (this depends on whether or not the image sensor returns buffers containing pixels that + * are not part of the active array region for blacklevel calibration or other purposes).</p> * <p>Some parts of the full pixel array may not receive light from the scene, * or be otherwise inactive. The {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE android.sensor.info.preCorrectionActiveArraySize} key * defines the rectangle of active pixels that will be included in processed image @@ -2205,7 +2199,6 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p><b>Units</b>: Pixels</p> * <p>This key is available on all devices.</p> * - * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP * @see CameraCharacteristics#SENSOR_INFO_PHYSICAL_SIZE * @see CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE */ @@ -2838,7 +2831,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p>See the individual level enums for full descriptions of the supported capabilities. The * {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} entry describes the device's capabilities at a * finer-grain level, if needed. In addition, many controls have their available settings or - * ranges defined in individual {@link android.hardware.camera2.CameraCharacteristics } entries.</p> + * ranges defined in individual entries from {@link android.hardware.camera2.CameraCharacteristics }.</p> * <p>Some features are not part of any particular hardware level or capability and must be * queried separately. These include:</p> * <ul> @@ -2973,7 +2966,6 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} and * android.scaler.availableStallDurations for more details about * calculating the max frame rate.</p> - * <p>(Keep in sync with {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration })</p> * <p><b>Units</b>: (format, width, height, ns) x n</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Limited capability</b> - diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 8c8c49fa6373..cb11d0f54a7b 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -587,8 +587,8 @@ public abstract class CameraMetadata<TKey> { * then the list of resolutions for YUV_420_888 from {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes } contains at * least one resolution >= 8 megapixels, with a minimum frame duration of <= 1/20 * s.</p> - * <p>If the device supports the {@link android.graphics.ImageFormat#RAW10 }, {@link android.graphics.ImageFormat#RAW12 }, then those can also be captured at the same rate - * as the maximum-size YUV_420_888 resolution is.</p> + * <p>If the device supports the {@link android.graphics.ImageFormat#RAW10 }, {@link android.graphics.ImageFormat#RAW12 }, then those can also be + * captured at the same rate as the maximum-size YUV_420_888 resolution is.</p> * <p>If the device supports the PRIVATE_REPROCESSING capability, then the same guarantees * as for the YUV_420_888 format also apply to the {@link android.graphics.ImageFormat#PRIVATE } format.</p> * <p>In addition, the {@link CameraCharacteristics#SYNC_MAX_LATENCY android.sync.maxLatency} field is guaranted to have a value between 0 @@ -610,25 +610,22 @@ public abstract class CameraMetadata<TKey> { * following:</p> * <ul> * <li>One input stream is supported, that is, <code>{@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} == 1</code>.</li> - * <li>{@link android.graphics.ImageFormat#YUV_420_888 } is supported as an output/input format, that is, - * YUV_420_888 is included in the lists of formats returned by - * {@link android.hardware.camera2.params.StreamConfigurationMap#getInputFormats } and - * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputFormats }.</li> + * <li>{@link android.graphics.ImageFormat#YUV_420_888 } is supported as an output/input + * format, that is, YUV_420_888 is included in the lists of formats returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputFormats } and {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputFormats }.</li> * <li>{@link android.hardware.camera2.params.StreamConfigurationMap#getValidOutputFormatsForInput } * returns non-empty int[] for each supported input format returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputFormats }.</li> * <li>Each size returned by {@link android.hardware.camera2.params.StreamConfigurationMap#getInputSizes getInputSizes(YUV_420_888)} is also included in {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes getOutputSizes(YUV_420_888)}</li> - * <li>Using {@link android.graphics.ImageFormat#YUV_420_888 } does not cause a frame rate drop - * relative to the sensor's maximum capture rate (at that resolution).</li> + * <li>Using {@link android.graphics.ImageFormat#YUV_420_888 } does not cause a frame rate + * drop relative to the sensor's maximum capture rate (at that resolution).</li> * <li>{@link android.graphics.ImageFormat#YUV_420_888 } will be reprocessable into both * {@link android.graphics.ImageFormat#YUV_420_888 } and {@link android.graphics.ImageFormat#JPEG } formats.</li> * <li>The maximum available resolution for {@link android.graphics.ImageFormat#YUV_420_888 } streams (both input/output) will match the * maximum available resolution of {@link android.graphics.ImageFormat#JPEG } streams.</li> * <li>Static metadata {@link CameraCharacteristics#REPROCESS_MAX_CAPTURE_STALL android.reprocess.maxCaptureStall}.</li> * <li>Only the below controls are effective for reprocessing requests and will be present - * in capture results. The reprocess requests are from the original capture results that - * are associated with the intermediate {@link android.graphics.ImageFormat#YUV_420_888 } - * output buffers. All other controls in the reprocess requests will be ignored by the - * camera device.<ul> + * in capture results. The reprocess requests are from the original capture results + * that are associated with the intermediate {@link android.graphics.ImageFormat#YUV_420_888 } output buffers. All other controls in the + * reprocess requests will be ignored by the camera device.<ul> * <li>android.jpeg.*</li> * <li>{@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode}</li> * <li>{@link CaptureRequest#EDGE_MODE android.edge.mode}</li> @@ -654,13 +651,13 @@ public abstract class CameraMetadata<TKey> { * <p>The camera device can produce depth measurements from its field of view.</p> * <p>This capability requires the camera device to support the following:</p> * <ul> - * <li>{@link android.graphics.ImageFormat#DEPTH16 } is supported as an output format.</li> - * <li>{@link android.graphics.ImageFormat#DEPTH_POINT_CLOUD } is optionally supported as an - * output format.</li> - * <li>This camera device, and all camera devices with the same {@link CameraCharacteristics#LENS_FACING android.lens.facing}, - * will list the following calibration entries in both - * {@link android.hardware.camera2.CameraCharacteristics } and - * {@link android.hardware.camera2.CaptureResult }:<ul> + * <li>{@link android.graphics.ImageFormat#DEPTH16 } is supported as + * an output format.</li> + * <li>{@link android.graphics.ImageFormat#DEPTH_POINT_CLOUD } is + * optionally supported as an output format.</li> + * <li>This camera device, and all camera devices with the same {@link CameraCharacteristics#LENS_FACING android.lens.facing}, will + * list the following calibration metadata entries in both {@link android.hardware.camera2.CameraCharacteristics } + * and {@link android.hardware.camera2.CaptureResult }:<ul> * <li>{@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}</li> * <li>{@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}</li> * <li>{@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}</li> @@ -674,8 +671,7 @@ public abstract class CameraMetadata<TKey> { * </ul> * <p>Generally, depth output operates at a slower frame rate than standard color capture, * so the DEPTH16 and DEPTH_POINT_CLOUD formats will commonly have a stall duration that - * should be accounted for (see - * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }). + * should be accounted for (see {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }). * On a device that supports both depth and color-based output, to enable smooth preview, * using a repeating burst is recommended, where a depth-output target is only included * once every N frames, where N is the ratio between preview output rate and depth output @@ -692,23 +688,19 @@ public abstract class CameraMetadata<TKey> { public static final int REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT = 8; /** - * <p>The device supports constrained high speed video recording (frame rate >=120fps) - * use case. The camera device will support high speed capture session created by - * {@link android.hardware.camera2.CameraDevice#createConstrainedHighSpeedCaptureSession }, which - * only accepts high speed request lists created by - * {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList }.</p> - * <p>A camera device can still support high speed video streaming by advertising the high speed - * FPS ranges in {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES android.control.aeAvailableTargetFpsRanges}. For this case, all normal - * capture request per frame control and synchronization requirements will apply to - * the high speed fps ranges, the same as all other fps ranges. This capability describes - * the capability of a specialized operating mode with many limitations (see below), which - * is only targeted at high speed video recording.</p> - * <p>The supported high speed video sizes and fps ranges are specified in - * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoFpsRanges }. - * To get desired output frame rates, the application is only allowed to select video size - * and FPS range combinations provided by - * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoSizes }. - * The fps range can be controlled via {@link CaptureRequest#CONTROL_AE_TARGET_FPS_RANGE android.control.aeTargetFpsRange}.</p> + * <p>The device supports constrained high speed video recording (frame rate >=120fps) use + * case. The camera device will support high speed capture session created by {@link android.hardware.camera2.CameraDevice#createConstrainedHighSpeedCaptureSession }, which + * only accepts high speed request lists created by {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList }.</p> + * <p>A camera device can still support high speed video streaming by advertising the high + * speed FPS ranges in {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES android.control.aeAvailableTargetFpsRanges}. For this case, all + * normal capture request per frame control and synchronization requirements will apply + * to the high speed fps ranges, the same as all other fps ranges. This capability + * describes the capability of a specialized operating mode with many limitations (see + * below), which is only targeted at high speed video recording.</p> + * <p>The supported high speed video sizes and fps ranges are specified in {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoFpsRanges }. + * To get desired output frame rates, the application is only allowed to select video + * size and FPS range combinations provided by {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoSizes }. The + * fps range can be controlled via {@link CaptureRequest#CONTROL_AE_TARGET_FPS_RANGE android.control.aeTargetFpsRange}.</p> * <p>In this capability, the camera device will override aeMode, awbMode, and afMode to * ON, AUTO, and CONTINUOUS_VIDEO, respectively. All post-processing block mode * controls will be overridden to be FAST. Therefore, no manual control of capture @@ -743,19 +735,16 @@ public abstract class CameraMetadata<TKey> { * frame rate. If the destination surface is from preview window, the actual preview frame * rate will be bounded by the screen refresh rate.</p> * <p>The camera device will only support up to 2 high speed simultaneous output surfaces - * (preview and recording surfaces) - * in this mode. Above controls will be effective only if all of below conditions are true:</p> + * (preview and recording surfaces) in this mode. Above controls will be effective only + * if all of below conditions are true:</p> * <ul> * <li>The application creates a camera capture session with no more than 2 surfaces via * {@link android.hardware.camera2.CameraDevice#createConstrainedHighSpeedCaptureSession }. The - * targeted surfaces must be preview surface (either from - * {@link android.view.SurfaceView } or {@link android.graphics.SurfaceTexture }) or - * recording surface(either from {@link android.media.MediaRecorder#getSurface } or - * {@link android.media.MediaCodec#createInputSurface }).</li> + * targeted surfaces must be preview surface (either from {@link android.view.SurfaceView } or {@link android.graphics.SurfaceTexture }) or recording + * surface(either from {@link android.media.MediaRecorder#getSurface } or {@link android.media.MediaCodec#createInputSurface }).</li> * <li>The stream sizes are selected from the sizes reported by * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoSizes }.</li> - * <li>The FPS ranges are selected from - * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoFpsRanges }.</li> + * <li>The FPS ranges are selected from {@link android.hardware.camera2.params.StreamConfigurationMap#getHighSpeedVideoFpsRanges }.</li> * </ul> * <p>When above conditions are NOT satistied, * {@link android.hardware.camera2.CameraDevice#createConstrainedHighSpeedCaptureSession } @@ -1038,8 +1027,7 @@ public abstract class CameraMetadata<TKey> { /** * <p>This camera device is running in backward compatibility mode.</p> - * <p>Only the stream configurations listed in the <code>LEGACY</code> table in the {@link android.hardware.camera2.CameraDevice#createCaptureSession createCaptureSession} - * documentation are supported.</p> + * <p>Only the stream configurations listed in the <code>LEGACY</code> table in the {@link android.hardware.camera2.CameraDevice#createCaptureSession createCaptureSession} documentation are supported.</p> * <p>A <code>LEGACY</code> device does not support per-frame control, manual sensor control, manual * post-processing, arbitrary cropping regions, and has relaxed performance constraints. * No additional capabilities beyond <code>BACKWARD_COMPATIBLE</code> will ever be listed by a @@ -1061,8 +1049,7 @@ public abstract class CameraMetadata<TKey> { * <p>This camera device is capable of YUV reprocessing and RAW data capture, in addition to * FULL-level capabilities.</p> * <p>The stream configurations listed in the <code>LEVEL_3</code>, <code>RAW</code>, <code>FULL</code>, <code>LEGACY</code> and - * <code>LIMITED</code> tables in the {@link android.hardware.camera2.CameraDevice#createCaptureSession createCaptureSession} - * documentation are guaranteed to be supported.</p> + * <code>LIMITED</code> tables in the {@link android.hardware.camera2.CameraDevice#createCaptureSession createCaptureSession} documentation are guaranteed to be supported.</p> * <p>The following additional capabilities are guaranteed to be supported:</p> * <ul> * <li><code>YUV_REPROCESSING</code> capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains @@ -2155,12 +2142,13 @@ public abstract class CameraMetadata<TKey> { public static final int EDGE_MODE_HIGH_QUALITY = 2; /** - * <p>Edge enhancement is applied at different levels for different output streams, - * based on resolution. Streams at maximum recording resolution (see {@link android.hardware.camera2.CameraDevice#createCaptureSession }) or below have - * edge enhancement applied, while higher-resolution streams have no edge enhancement - * applied. The level of edge enhancement for low-resolution streams is tuned so that - * frame rate is not impacted, and the quality is equal to or better than FAST (since it - * is only applied to lower-resolution outputs, quality may improve from FAST).</p> + * <p>Edge enhancement is applied at different + * levels for different output streams, based on resolution. Streams at maximum recording + * resolution (see {@link android.hardware.camera2.CameraDevice#createCaptureSession }) + * or below have edge enhancement applied, while higher-resolution streams have no edge + * enhancement applied. The level of edge enhancement for low-resolution streams is tuned + * so that frame rate is not impacted, and the quality is equal to or better than FAST + * (since it is only applied to lower-resolution outputs, quality may improve from FAST).</p> * <p>This mode is intended to be used by applications operating in a zero-shutter-lag mode * with YUV or PRIVATE reprocessing, where the application continuously captures * high-resolution intermediate buffers into a circular buffer, from which a final image is @@ -2287,12 +2275,12 @@ public abstract class CameraMetadata<TKey> { /** * <p>Noise reduction is applied at different levels for different output streams, - * based on resolution. Streams at maximum recording resolution (see {@link android.hardware.camera2.CameraDevice#createCaptureSession }) or below have noise - * reduction applied, while higher-resolution streams have MINIMAL (if supported) or no - * noise reduction applied (if MINIMAL is not supported.) The degree of noise reduction - * for low-resolution streams is tuned so that frame rate is not impacted, and the quality - * is equal to or better than FAST (since it is only applied to lower-resolution outputs, - * quality may improve from FAST).</p> + * based on resolution. Streams at maximum recording resolution (see {@link android.hardware.camera2.CameraDevice#createCaptureSession }) + * or below have noise reduction applied, while higher-resolution streams have MINIMAL (if + * supported) or no noise reduction applied (if MINIMAL is not supported.) The degree of + * noise reduction for low-resolution streams is tuned so that frame rate is not impacted, + * and the quality is equal to or better than FAST (since it is only applied to + * lower-resolution outputs, quality may improve from FAST).</p> * <p>This mode is intended to be used by applications operating in a zero-shutter-lag mode * with YUV or PRIVATE reprocessing, where the application continuously captures * high-resolution intermediate buffers into a circular buffer, from which a final image is @@ -2737,6 +2725,22 @@ public abstract class CameraMetadata<TKey> { public static final int CONTROL_AWB_STATE_LOCKED = 3; // + // Enumeration values for CaptureResult#CONTROL_AF_SCENE_CHANGE + // + + /** + * <p>Scene change is not detected within the AF region(s).</p> + * @see CaptureResult#CONTROL_AF_SCENE_CHANGE + */ + public static final int CONTROL_AF_SCENE_CHANGE_NOT_DETECTED = 0; + + /** + * <p>Scene change is detected within the AF region(s).</p> + * @see CaptureResult#CONTROL_AF_SCENE_CHANGE + */ + public static final int CONTROL_AF_SCENE_CHANGE_DETECTED = 1; + + // // Enumeration values for CaptureResult#FLASH_STATE // diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index c41fc0207d92..0262ecb54f0d 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -680,7 +680,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * FAST or HIGH_QUALITY will yield a picture with the same white point * as what was produced by the camera device in the earlier frame.</p> * <p>The expected processing pipeline is as follows:</p> - * <p><img alt="White balance processing pipeline" src="../../../../images/camera2/metadata/android.colorCorrection.mode/processing_pipeline.png" /></p> + * <p><img alt="White balance processing pipeline" src="/reference/images/camera2/metadata/android.colorCorrection.mode/processing_pipeline.png" /></p> * <p>The white balance is encoded by two values, a 4-channel white-balance * gain vector (applied in the Bayer domain), and a 3x3 color transform * matrix (applied after demosaic).</p> @@ -1470,10 +1470,10 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <p>When set to AUTO, the individual algorithm controls in * android.control.* are in effect, such as {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode}.</p> * <p>When set to USE_SCENE_MODE, the individual controls in - * android.control.* are mostly disabled, and the camera device implements - * one of the scene mode settings (such as ACTION, SUNSET, or PARTY) - * as it wishes. The camera device scene mode 3A settings are provided by - * {@link android.hardware.camera2.CaptureResult capture results}.</p> + * android.control.* are mostly disabled, and the camera device + * implements one of the scene mode settings (such as ACTION, + * SUNSET, or PARTY) as it wishes. The camera device scene mode + * 3A settings are provided by {@link android.hardware.camera2.CaptureResult capture results}.</p> * <p>When set to OFF_KEEP_STATE, it is similar to OFF mode, the only difference * is that this frame will not be used by camera device background 3A statistics * update, as if this frame is never captured. This mode can be used in the scenario @@ -2268,45 +2268,35 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * can run concurrently to the rest of the camera pipeline, but * cannot process more than 1 capture at a time.</li> * </ul> - * <p>The necessary information for the application, given the model above, - * is provided via the {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} field using + * <p>The necessary information for the application, given the model above, is provided via * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration }. - * These are used to determine the maximum frame rate / minimum frame - * duration that is possible for a given stream configuration.</p> + * These are used to determine the maximum frame rate / minimum frame duration that is + * possible for a given stream configuration.</p> * <p>Specifically, the application can use the following rules to * determine the minimum frame duration it can request from the camera * device:</p> * <ol> - * <li>Let the set of currently configured input/output streams - * be called <code>S</code>.</li> - * <li>Find the minimum frame durations for each stream in <code>S</code>, by looking - * it up in {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} using {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration } - * (with its respective size/format). Let this set of frame durations be - * called <code>F</code>.</li> - * <li>For any given request <code>R</code>, the minimum frame duration allowed - * for <code>R</code> is the maximum out of all values in <code>F</code>. Let the streams - * used in <code>R</code> be called <code>S_r</code>.</li> + * <li>Let the set of currently configured input/output streams be called <code>S</code>.</li> + * <li>Find the minimum frame durations for each stream in <code>S</code>, by looking it up in {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration } + * (with its respective size/format). Let this set of frame durations be called <code>F</code>.</li> + * <li>For any given request <code>R</code>, the minimum frame duration allowed for <code>R</code> is the maximum + * out of all values in <code>F</code>. Let the streams used in <code>R</code> be called <code>S_r</code>.</li> * </ol> * <p>If none of the streams in <code>S_r</code> have a stall time (listed in {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration } - * using its respective size/format), then the frame duration in <code>F</code> - * determines the steady state frame rate that the application will get - * if it uses <code>R</code> as a repeating request. Let this special kind of - * request be called <code>Rsimple</code>.</p> - * <p>A repeating request <code>Rsimple</code> can be <em>occasionally</em> interleaved - * by a single capture of a new request <code>Rstall</code> (which has at least - * one in-use stream with a non-0 stall time) and if <code>Rstall</code> has the - * same minimum frame duration this will not cause a frame rate loss - * if all buffers from the previous <code>Rstall</code> have already been - * delivered.</p> - * <p>For more details about stalling, see - * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }.</p> + * using its respective size/format), then the frame duration in <code>F</code> determines the steady + * state frame rate that the application will get if it uses <code>R</code> as a repeating request. Let + * this special kind of request be called <code>Rsimple</code>.</p> + * <p>A repeating request <code>Rsimple</code> can be <em>occasionally</em> interleaved by a single capture of a + * new request <code>Rstall</code> (which has at least one in-use stream with a non-0 stall time) and if + * <code>Rstall</code> has the same minimum frame duration this will not cause a frame rate loss if all + * buffers from the previous <code>Rstall</code> have already been delivered.</p> + * <p>For more details about stalling, see {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }.</p> * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to * OFF; otherwise the auto-exposure algorithm will override this value.</p> * <p><b>Units</b>: Nanoseconds</p> * <p><b>Range of valid values:</b><br> - * See {@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}, - * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}. The duration - * is capped to <code>max(duration, exposureTime + overhead)</code>.</p> + * See {@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}, {@link android.hardware.camera2.params.StreamConfigurationMap }. + * The duration is capped to <code>max(duration, exposureTime + overhead)</code>.</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the @@ -2315,7 +2305,6 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * @see CaptureRequest#CONTROL_AE_MODE * @see CaptureRequest#CONTROL_MODE * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL - * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP * @see CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION */ @PublicKey @@ -2584,11 +2573,11 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <p>Linear mapping:</p> * <pre><code>android.tonemap.curveRed = [ 0, 0, 1.0, 1.0 ] * </code></pre> - * <p><img alt="Linear mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> + * <p><img alt="Linear mapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> * <p>Invert mapping:</p> * <pre><code>android.tonemap.curveRed = [ 0, 1.0, 1.0, 0 ] * </code></pre> - * <p><img alt="Inverting mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> + * <p><img alt="Inverting mapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> * <p>Gamma 1/2.2 mapping, with 16 control points:</p> * <pre><code>android.tonemap.curveRed = [ * 0.0000, 0.0000, 0.0667, 0.2920, 0.1333, 0.4002, 0.2000, 0.4812, @@ -2596,7 +2585,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * 0.5333, 0.7515, 0.6000, 0.7928, 0.6667, 0.8317, 0.7333, 0.8685, * 0.8000, 0.9035, 0.8667, 0.9370, 0.9333, 0.9691, 1.0000, 1.0000 ] * </code></pre> - * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> + * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> * <p>Standard sRGB gamma mapping, per IEC 61966-2-1:1999, with 16 control points:</p> * <pre><code>android.tonemap.curveRed = [ * 0.0000, 0.0000, 0.0667, 0.2864, 0.1333, 0.4007, 0.2000, 0.4845, @@ -2604,7 +2593,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * 0.5333, 0.7569, 0.6000, 0.7977, 0.6667, 0.8360, 0.7333, 0.8721, * 0.8000, 0.9063, 0.8667, 0.9389, 0.9333, 0.9701, 1.0000, 1.0000 ] * </code></pre> - * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * <p><img alt="sRGB tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> * <p><b>Range of valid values:</b><br> * 0-1 on both input and output coordinates, normalized * as a floating-point value such that 0 == black and 1 == white.</p> @@ -2646,11 +2635,11 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <p>Linear mapping:</p> * <pre><code>curveRed = [ (0, 0), (1.0, 1.0) ] * </code></pre> - * <p><img alt="Linear mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> + * <p><img alt="Linear mapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> * <p>Invert mapping:</p> * <pre><code>curveRed = [ (0, 1.0), (1.0, 0) ] * </code></pre> - * <p><img alt="Inverting mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> + * <p><img alt="Inverting mapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> * <p>Gamma 1/2.2 mapping, with 16 control points:</p> * <pre><code>curveRed = [ * (0.0000, 0.0000), (0.0667, 0.2920), (0.1333, 0.4002), (0.2000, 0.4812), @@ -2658,7 +2647,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * (0.5333, 0.7515), (0.6000, 0.7928), (0.6667, 0.8317), (0.7333, 0.8685), * (0.8000, 0.9035), (0.8667, 0.9370), (0.9333, 0.9691), (1.0000, 1.0000) ] * </code></pre> - * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> + * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> * <p>Standard sRGB gamma mapping, per IEC 61966-2-1:1999, with 16 control points:</p> * <pre><code>curveRed = [ * (0.0000, 0.0000), (0.0667, 0.2864), (0.1333, 0.4007), (0.2000, 0.4845), @@ -2666,7 +2655,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * (0.5333, 0.7569), (0.6000, 0.7977), (0.6667, 0.8360), (0.7333, 0.8721), * (0.8000, 0.9063), (0.8667, 0.9389), (0.9333, 0.9701), (1.0000, 1.0000) ] * </code></pre> - * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * <p><img alt="sRGB tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the @@ -2756,9 +2745,9 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * PRESET_CURVE</p> * <p>The tonemap curve will be defined by specified standard.</p> * <p>sRGB (approximated by 16 control points):</p> - * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * <p><img alt="sRGB tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> * <p>Rec. 709 (approximated by 16 control points):</p> - * <p><img alt="Rec. 709 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/rec709_tonemap.png" /></p> + * <p><img alt="Rec. 709 tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/rec709_tonemap.png" /></p> * <p>Note that above figures show a 16 control points approximation of preset * curves. Camera devices may apply a different approximation to the curve.</p> * <p><b>Possible values:</b> diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 6d80c20a84af..6d7b06fc609f 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -390,7 +390,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * FAST or HIGH_QUALITY will yield a picture with the same white point * as what was produced by the camera device in the earlier frame.</p> * <p>The expected processing pipeline is as follows:</p> - * <p><img alt="White balance processing pipeline" src="../../../../images/camera2/metadata/android.colorCorrection.mode/processing_pipeline.png" /></p> + * <p><img alt="White balance processing pipeline" src="/reference/images/camera2/metadata/android.colorCorrection.mode/processing_pipeline.png" /></p> * <p>The white balance is encoded by two values, a 4-channel white-balance * gain vector (applied in the Bayer domain), and a 3x3 color transform * matrix (applied after demosaic).</p> @@ -1975,10 +1975,10 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p>When set to AUTO, the individual algorithm controls in * android.control.* are in effect, such as {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode}.</p> * <p>When set to USE_SCENE_MODE, the individual controls in - * android.control.* are mostly disabled, and the camera device implements - * one of the scene mode settings (such as ACTION, SUNSET, or PARTY) - * as it wishes. The camera device scene mode 3A settings are provided by - * {@link android.hardware.camera2.CaptureResult capture results}.</p> + * android.control.* are mostly disabled, and the camera device + * implements one of the scene mode settings (such as ACTION, + * SUNSET, or PARTY) as it wishes. The camera device scene mode + * 3A settings are provided by {@link android.hardware.camera2.CaptureResult capture results}.</p> * <p>When set to OFF_KEEP_STATE, it is similar to OFF mode, the only difference * is that this frame will not be used by camera device background 3A statistics * update, as if this frame is never captured. This mode can be used in the scenario @@ -2185,6 +2185,30 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { new Key<Boolean>("android.control.enableZsl", boolean.class); /** + * <p>Whether a significant scene change is detected within the currently-set AF + * region(s).</p> + * <p>When the camera focus routine detects a change in the scene it is looking at, + * such as a large shift in camera viewpoint, significant motion in the scene, or a + * significant illumination change, this value will be set to DETECTED for a single capture + * result. Otherwise the value will be NOT_DETECTED. The threshold for detection is similar + * to what would trigger a new passive focus scan to begin in CONTINUOUS autofocus modes.</p> + * <p>afSceneChange may be DETECTED only if afMode is AF_MODE_CONTINUOUS_VIDEO or + * AF_MODE_CONTINUOUS_PICTURE. In other AF modes, afSceneChange must be NOT_DETECTED.</p> + * <p>This key will be available if the camera device advertises this key via {@link android.hardware.camera2.CameraCharacteristics#getAvailableCaptureResultKeys }.</p> + * <p><b>Possible values:</b> + * <ul> + * <li>{@link #CONTROL_AF_SCENE_CHANGE_NOT_DETECTED NOT_DETECTED}</li> + * <li>{@link #CONTROL_AF_SCENE_CHANGE_DETECTED DETECTED}</li> + * </ul></p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * @see #CONTROL_AF_SCENE_CHANGE_NOT_DETECTED + * @see #CONTROL_AF_SCENE_CHANGE_DETECTED + */ + @PublicKey + public static final Key<Integer> CONTROL_AF_SCENE_CHANGE = + new Key<Integer>("android.control.afSceneChange", int.class); + + /** * <p>Operation mode for edge * enhancement.</p> * <p>Edge enhancement improves sharpness and details in the captured image. OFF means @@ -3108,45 +3132,35 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * can run concurrently to the rest of the camera pipeline, but * cannot process more than 1 capture at a time.</li> * </ul> - * <p>The necessary information for the application, given the model above, - * is provided via the {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} field using + * <p>The necessary information for the application, given the model above, is provided via * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration }. - * These are used to determine the maximum frame rate / minimum frame - * duration that is possible for a given stream configuration.</p> + * These are used to determine the maximum frame rate / minimum frame duration that is + * possible for a given stream configuration.</p> * <p>Specifically, the application can use the following rules to * determine the minimum frame duration it can request from the camera * device:</p> * <ol> - * <li>Let the set of currently configured input/output streams - * be called <code>S</code>.</li> - * <li>Find the minimum frame durations for each stream in <code>S</code>, by looking - * it up in {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} using {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration } - * (with its respective size/format). Let this set of frame durations be - * called <code>F</code>.</li> - * <li>For any given request <code>R</code>, the minimum frame duration allowed - * for <code>R</code> is the maximum out of all values in <code>F</code>. Let the streams - * used in <code>R</code> be called <code>S_r</code>.</li> + * <li>Let the set of currently configured input/output streams be called <code>S</code>.</li> + * <li>Find the minimum frame durations for each stream in <code>S</code>, by looking it up in {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration } + * (with its respective size/format). Let this set of frame durations be called <code>F</code>.</li> + * <li>For any given request <code>R</code>, the minimum frame duration allowed for <code>R</code> is the maximum + * out of all values in <code>F</code>. Let the streams used in <code>R</code> be called <code>S_r</code>.</li> * </ol> * <p>If none of the streams in <code>S_r</code> have a stall time (listed in {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration } - * using its respective size/format), then the frame duration in <code>F</code> - * determines the steady state frame rate that the application will get - * if it uses <code>R</code> as a repeating request. Let this special kind of - * request be called <code>Rsimple</code>.</p> - * <p>A repeating request <code>Rsimple</code> can be <em>occasionally</em> interleaved - * by a single capture of a new request <code>Rstall</code> (which has at least - * one in-use stream with a non-0 stall time) and if <code>Rstall</code> has the - * same minimum frame duration this will not cause a frame rate loss - * if all buffers from the previous <code>Rstall</code> have already been - * delivered.</p> - * <p>For more details about stalling, see - * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }.</p> + * using its respective size/format), then the frame duration in <code>F</code> determines the steady + * state frame rate that the application will get if it uses <code>R</code> as a repeating request. Let + * this special kind of request be called <code>Rsimple</code>.</p> + * <p>A repeating request <code>Rsimple</code> can be <em>occasionally</em> interleaved by a single capture of a + * new request <code>Rstall</code> (which has at least one in-use stream with a non-0 stall time) and if + * <code>Rstall</code> has the same minimum frame duration this will not cause a frame rate loss if all + * buffers from the previous <code>Rstall</code> have already been delivered.</p> + * <p>For more details about stalling, see {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputStallDuration }.</p> * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to * OFF; otherwise the auto-exposure algorithm will override this value.</p> * <p><b>Units</b>: Nanoseconds</p> * <p><b>Range of valid values:</b><br> - * See {@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}, - * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}. The duration - * is capped to <code>max(duration, exposureTime + overhead)</code>.</p> + * See {@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}, {@link android.hardware.camera2.params.StreamConfigurationMap }. + * The duration is capped to <code>max(duration, exposureTime + overhead)</code>.</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the @@ -3155,7 +3169,6 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * @see CaptureRequest#CONTROL_AE_MODE * @see CaptureRequest#CONTROL_MODE * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL - * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP * @see CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION */ @PublicKey @@ -3408,9 +3421,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * layout key (see {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT android.sensor.info.colorFilterArrangement}), i.e. the * nth value given corresponds to the black level offset for the nth * color channel listed in the CFA.</p> - * <p>This key will be available if {@link CameraCharacteristics#SENSOR_OPTICAL_BLACK_REGIONS android.sensor.opticalBlackRegions} is - * available or the camera device advertises this key via - * {@link android.hardware.camera2.CameraCharacteristics#getAvailableCaptureResultKeys }.</p> + * <p>This key will be available if {@link CameraCharacteristics#SENSOR_OPTICAL_BLACK_REGIONS android.sensor.opticalBlackRegions} is available or the + * camera device advertises this key via {@link android.hardware.camera2.CameraCharacteristics#getAvailableCaptureResultKeys }.</p> * <p><b>Range of valid values:</b><br> * >= 0 for each.</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> @@ -3640,13 +3652,13 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * </code></pre> * <p>The low-resolution scaling map images for each channel are * (displayed using nearest-neighbor interpolation):</p> - * <p><img alt="Red lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/red_shading.png" /> - * <img alt="Green (even rows) lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/green_e_shading.png" /> - * <img alt="Green (odd rows) lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/green_o_shading.png" /> - * <img alt="Blue lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/blue_shading.png" /></p> + * <p><img alt="Red lens shading map" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/red_shading.png" /> + * <img alt="Green (even rows) lens shading map" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/green_e_shading.png" /> + * <img alt="Green (odd rows) lens shading map" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/green_o_shading.png" /> + * <img alt="Blue lens shading map" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/blue_shading.png" /></p> * <p>As a visualization only, inverting the full-color map to recover an * image of a gray wall (using bicubic interpolation for visual quality) as captured by the sensor gives:</p> - * <p><img alt="Image of a uniform white wall (inverse shading map)" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/inv_shading.png" /></p> + * <p><img alt="Image of a uniform white wall (inverse shading map)" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/inv_shading.png" /></p> * <p><b>Range of valid values:</b><br> * Each gain factor is >= 1</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> @@ -3707,14 +3719,14 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * </code></pre> * <p>The low-resolution scaling map images for each channel are * (displayed using nearest-neighbor interpolation):</p> - * <p><img alt="Red lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/red_shading.png" /> - * <img alt="Green (even rows) lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/green_e_shading.png" /> - * <img alt="Green (odd rows) lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/green_o_shading.png" /> - * <img alt="Blue lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/blue_shading.png" /></p> + * <p><img alt="Red lens shading map" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/red_shading.png" /> + * <img alt="Green (even rows) lens shading map" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/green_e_shading.png" /> + * <img alt="Green (odd rows) lens shading map" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/green_o_shading.png" /> + * <img alt="Blue lens shading map" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/blue_shading.png" /></p> * <p>As a visualization only, inverting the full-color map to recover an * image of a gray wall (using bicubic interpolation for visual quality) * as captured by the sensor gives:</p> - * <p><img alt="Image of a uniform white wall (inverse shading map)" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/inv_shading.png" /></p> + * <p><img alt="Image of a uniform white wall (inverse shading map)" src="/reference/images/camera2/metadata/android.statistics.lensShadingMap/inv_shading.png" /></p> * <p>Note that the RAW image data might be subject to lens shading * correction not reported on this map. Query * {@link CameraCharacteristics#SENSOR_INFO_LENS_SHADING_APPLIED android.sensor.info.lensShadingApplied} to see if RAW image data has subject @@ -3947,11 +3959,11 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p>Linear mapping:</p> * <pre><code>android.tonemap.curveRed = [ 0, 0, 1.0, 1.0 ] * </code></pre> - * <p><img alt="Linear mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> + * <p><img alt="Linear mapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> * <p>Invert mapping:</p> * <pre><code>android.tonemap.curveRed = [ 0, 1.0, 1.0, 0 ] * </code></pre> - * <p><img alt="Inverting mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> + * <p><img alt="Inverting mapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> * <p>Gamma 1/2.2 mapping, with 16 control points:</p> * <pre><code>android.tonemap.curveRed = [ * 0.0000, 0.0000, 0.0667, 0.2920, 0.1333, 0.4002, 0.2000, 0.4812, @@ -3959,7 +3971,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * 0.5333, 0.7515, 0.6000, 0.7928, 0.6667, 0.8317, 0.7333, 0.8685, * 0.8000, 0.9035, 0.8667, 0.9370, 0.9333, 0.9691, 1.0000, 1.0000 ] * </code></pre> - * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> + * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> * <p>Standard sRGB gamma mapping, per IEC 61966-2-1:1999, with 16 control points:</p> * <pre><code>android.tonemap.curveRed = [ * 0.0000, 0.0000, 0.0667, 0.2864, 0.1333, 0.4007, 0.2000, 0.4845, @@ -3967,7 +3979,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * 0.5333, 0.7569, 0.6000, 0.7977, 0.6667, 0.8360, 0.7333, 0.8721, * 0.8000, 0.9063, 0.8667, 0.9389, 0.9333, 0.9701, 1.0000, 1.0000 ] * </code></pre> - * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * <p><img alt="sRGB tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> * <p><b>Range of valid values:</b><br> * 0-1 on both input and output coordinates, normalized * as a floating-point value such that 0 == black and 1 == white.</p> @@ -4009,11 +4021,11 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <p>Linear mapping:</p> * <pre><code>curveRed = [ (0, 0), (1.0, 1.0) ] * </code></pre> - * <p><img alt="Linear mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> + * <p><img alt="Linear mapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> * <p>Invert mapping:</p> * <pre><code>curveRed = [ (0, 1.0), (1.0, 0) ] * </code></pre> - * <p><img alt="Inverting mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> + * <p><img alt="Inverting mapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> * <p>Gamma 1/2.2 mapping, with 16 control points:</p> * <pre><code>curveRed = [ * (0.0000, 0.0000), (0.0667, 0.2920), (0.1333, 0.4002), (0.2000, 0.4812), @@ -4021,7 +4033,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * (0.5333, 0.7515), (0.6000, 0.7928), (0.6667, 0.8317), (0.7333, 0.8685), * (0.8000, 0.9035), (0.8667, 0.9370), (0.9333, 0.9691), (1.0000, 1.0000) ] * </code></pre> - * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> + * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> * <p>Standard sRGB gamma mapping, per IEC 61966-2-1:1999, with 16 control points:</p> * <pre><code>curveRed = [ * (0.0000, 0.0000), (0.0667, 0.2864), (0.1333, 0.4007), (0.2000, 0.4845), @@ -4029,7 +4041,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * (0.5333, 0.7569), (0.6000, 0.7977), (0.6667, 0.8360), (0.7333, 0.8721), * (0.8000, 0.9063), (0.8667, 0.9389), (0.9333, 0.9701), (1.0000, 1.0000) ] * </code></pre> - * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * <p><img alt="sRGB tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the @@ -4119,9 +4131,9 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * PRESET_CURVE</p> * <p>The tonemap curve will be defined by specified standard.</p> * <p>sRGB (approximated by 16 control points):</p> - * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * <p><img alt="sRGB tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> * <p>Rec. 709 (approximated by 16 control points):</p> - * <p><img alt="Rec. 709 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/rec709_tonemap.png" /></p> + * <p><img alt="Rec. 709 tonemapping curve" src="/reference/images/camera2/metadata/android.tonemap.curveRed/rec709_tonemap.png" /></p> * <p>Note that above figures show a 16 control points approximation of preset * curves. Camera devices may apply a different approximation to the curve.</p> * <p><b>Possible values:</b> diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java index c7654c9e74e1..374789c6cf05 100644 --- a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java +++ b/core/java/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/core/java/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java index fec7fd97764c..8c4dbfa58d05 100644 --- a/core/java/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java +++ b/core/java/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/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index bfeb14dedb5c..6787d84b57d3 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/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/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java index 27087a2e4881..0978ff87b38b 100644 --- a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java +++ b/core/java/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/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java index 49d4096e3f3e..119cca8d4715 100644 --- a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java +++ b/core/java/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/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java index 2b317d679d1c..7409671f9b9f 100644 --- a/core/java/android/hardware/camera2/params/OutputConfiguration.java +++ b/core/java/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/core/java/android/hardware/display/BrightnessChangeEvent.aidl b/core/java/android/hardware/display/BrightnessChangeEvent.aidl new file mode 100644 index 000000000000..942e0db2d43c --- /dev/null +++ b/core/java/android/hardware/display/BrightnessChangeEvent.aidl @@ -0,0 +1,19 @@ +/* + * 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.display; + +parcelable BrightnessChangeEvent; diff --git a/core/java/android/hardware/display/BrightnessChangeEvent.java b/core/java/android/hardware/display/BrightnessChangeEvent.java new file mode 100644 index 000000000000..3003607e5f72 --- /dev/null +++ b/core/java/android/hardware/display/BrightnessChangeEvent.java @@ -0,0 +1,121 @@ +/* + * 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. + * + * {@see DisplayManager.getBrightnessEvents()} + * 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. + * This will be null if the caller of {@see DisplayManager.getBrightnessEvents()} + * does not have access to usage stats {@see UsageStatsManager} */ + 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() { + } + + /** @hide */ + public BrightnessChangeEvent(BrightnessChangeEvent other) { + this.brightness = other.brightness; + this.timeStamp = other.timeStamp; + this.packageName = other.packageName; + this.userId = other.userId; + this.luxValues = other.luxValues; + this.luxTimestamps = other.luxTimestamps; + this.batteryLevel = other.batteryLevel; + this.nightMode = other.nightMode; + this.colorTemperature = other.colorTemperature; + this.lastBrightness = other.lastBrightness; + } + + 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/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index b2af44ecf0fd..97ca231bf68c 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -16,10 +16,13 @@ package android.hardware.display; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.app.KeyguardManager; import android.content.Context; import android.graphics.Point; import android.media.projection.MediaProjection; @@ -27,9 +30,9 @@ import android.os.Handler; import android.util.SparseArray; import android.view.Display; import android.view.Surface; -import android.view.WindowManagerPolicy; import java.util.ArrayList; +import java.util.List; /** * Manages the properties of attached displays. @@ -253,8 +256,8 @@ public final class DisplayManager { * </p> * * @see #createVirtualDisplay - * @see WindowManagerPolicy#isKeyguardSecure(int) - * @see WindowManagerPolicy#isKeyguardTrustedLw() + * @see KeyguardManager#isDeviceSecure() + * @see KeyguardManager#isDeviceLocked() * @hide */ // TODO: Update name and documentation and un-hide the flag. Don't change the value before that. @@ -615,6 +618,22 @@ public final class DisplayManager { } /** + * Fetch {@link BrightnessChangeEvent}s. + * @hide until we make it a system api. + */ + @RequiresPermission(Manifest.permission.BRIGHTNESS_SLIDER_USAGE) + public List<BrightnessChangeEvent> getBrightnessEvents() { + return mGlobal.getBrightnessEvents(mContext.getOpPackageName()); + } + + /** + * @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/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index a8a4eb67f580..c3f82f56dad8 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/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,34 @@ public final class DisplayManagerGlobal { } } + /** + * Retrieves brightness change events. + */ + public List<BrightnessChangeEvent> getBrightnessEvents(String callingPackage) { + try { + ParceledListSlice<BrightnessChangeEvent> events = + mDm.getBrightnessEvents(callingPackage); + 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/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index e845359a35c4..cd551bd42e0f 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -174,6 +174,11 @@ public abstract class DisplayManagerInternal { public abstract boolean isUidPresentOnDisplay(int uid, int displayId); /** + * Persist brightness slider events. + */ + public abstract void persistBrightnessSliderEvents(); + + /** * Describes the requested power state of the display. * * This object is intended to describe the general characteristics of the diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl index 505388419c8c..f2ed9e7571d3 100644 --- a/core/java/android/hardware/display/IDisplayManager.aidl +++ b/core/java/android/hardware/display/IDisplayManager.aidl @@ -16,6 +16,7 @@ package android.hardware.display; +import android.content.pm.ParceledListSlice; import android.graphics.Point; import android.hardware.display.IDisplayManagerCallback; import android.hardware.display.IVirtualDisplayCallback; @@ -81,4 +82,11 @@ interface IDisplayManager { // Get a stable metric for the device's display size. No permissions required. Point getStableDisplaySize(); + + // Requires BRIGHTNESS_SLIDER_USAGE permission. + ParceledListSlice getBrightnessEvents(String callingPackage); + + // STOPSHIP remove when adaptive brightness code is updated to accept curves. + // Requires BRIGHTNESS_SLIDER_USAGE permission. + void setBrightness(int brightness); } diff --git a/core/java/android/hardware/location/ContextHubClient.java b/core/java/android/hardware/location/ContextHubClient.java new file mode 100644 index 000000000000..52527ed67ae4 --- /dev/null +++ b/core/java/android/hardware/location/ContextHubClient.java @@ -0,0 +1,123 @@ +/* + * 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.location; + +import android.annotation.RequiresPermission; +import android.os.RemoteException; + +import dalvik.system.CloseGuard; + +import java.io.Closeable; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A class describing a client of the Context Hub Service. + * + * Clients can send messages to nanoapps at a Context Hub through this object. The APIs supported + * by this object are thread-safe and can be used without external synchronization. + * + * @hide + */ +public class ContextHubClient implements Closeable { + /* + * The proxy to the client interface at the service. + */ + private final IContextHubClient mClientProxy; + + /* + * The callback interface associated with this client. + */ + private final IContextHubClientCallback mCallbackInterface; + + /* + * The Context Hub that this client is attached to. + */ + private final ContextHubInfo mAttachedHub; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final AtomicBoolean mIsClosed = new AtomicBoolean(false); + + /* package */ ContextHubClient( + IContextHubClient clientProxy, IContextHubClientCallback callback, + ContextHubInfo hubInfo) { + mClientProxy = clientProxy; + mCallbackInterface = callback; + mAttachedHub = hubInfo; + mCloseGuard.open("close"); + } + + /** + * Returns the hub that this client is attached to. + * + * @return the ContextHubInfo of the attached hub + */ + public ContextHubInfo getAttachedHub() { + return mAttachedHub; + } + + /** + * Closes the connection for this client and the Context Hub Service. + * + * When this function is invoked, the messaging associated with this client is invalidated. + * All futures messages targeted for this client are dropped at the service. + */ + public void close() { + if (!mIsClosed.getAndSet(true)) { + mCloseGuard.close(); + try { + mClientProxy.close(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Sends a message to a nanoapp through the Context Hub Service. + * + * This function returns TRANSACTION_SUCCESS if the message has reached the HAL, but + * does not guarantee delivery of the message to the target nanoapp. + * + * @param message the message object to send + * + * @return the result of sending the message defined as in ContextHubTransaction.Result + * + * @see NanoAppMessage + * @see ContextHubTransaction.Result + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + @ContextHubTransaction.Result + public int sendMessageToNanoApp(NanoAppMessage message) { + try { + return mClientProxy.sendMessageToNanoApp(message); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } finally { + super.finalize(); + } + } +} diff --git a/core/java/android/hardware/location/ContextHubClientCallback.java b/core/java/android/hardware/location/ContextHubClientCallback.java new file mode 100644 index 000000000000..ab19d547025d --- /dev/null +++ b/core/java/android/hardware/location/ContextHubClientCallback.java @@ -0,0 +1,85 @@ +/* + * 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.location; + +/** + * A class for {@link android.hardware.location.ContextHubClient ContextHubClient} to + * receive messages and life-cycle events from nanoapps in the Context Hub at which the client is + * attached to. + * + * This callback is registered through the + * {@link android.hardware.location.ContextHubManager#createClient() creation} of + * {@link android.hardware.location.ContextHubClient ContextHubClient}. Callbacks are + * invoked in the following ways: + * 1) Messages from nanoapps delivered through onMessageFromNanoApp may either be broadcasted + * or targeted to a specific client. + * 2) Nanoapp or Context Hub events (the remaining callbacks) are broadcasted to all clients, and + * the client can choose to ignore the event by filtering through the parameters. + * + * @hide + */ +public class ContextHubClientCallback { + /** + * Callback invoked when receiving a message from a nanoapp. + * + * The message contents of this callback may either be broadcasted or targeted to the + * client receiving the invocation. + * + * @param message the message sent by the nanoapp + */ + public void onMessageFromNanoApp(NanoAppMessage message) {} + + /** + * Callback invoked when the attached Context Hub has reset. + */ + public void onHubReset() {} + + /** + * Callback invoked when a nanoapp aborts at the attached Context Hub. + * + * @param nanoAppId the ID of the nanoapp that had aborted + * @param abortCode the reason for nanoapp's abort, specific to each nanoapp + */ + public void onNanoAppAborted(long nanoAppId, int abortCode) {} + + /** + * Callback invoked when a nanoapp is loaded at the attached Context Hub. + * + * @param nanoAppId the ID of the nanoapp that had been loaded + */ + public void onNanoAppLoaded(long nanoAppId) {} + + /** + * Callback invoked when a nanoapp is unloaded from the attached Context Hub. + * + * @param nanoAppId the ID of the nanoapp that had been unloaded + */ + public void onNanoAppUnloaded(long nanoAppId) {} + + /** + * Callback invoked when a nanoapp is enabled at the attached Context Hub. + * + * @param nanoAppId the ID of the nanoapp that had been enabled + */ + public void onNanoAppEnabled(long nanoAppId) {} + + /** + * Callback invoked when a nanoapp is disabled at the attached Context Hub. + * + * @param nanoAppId the ID of the nanoapp that had been disabled + */ + public void onNanoAppDisabled(long nanoAppId) {} +} diff --git a/core/java/android/hardware/location/ContextHubInfo.java b/core/java/android/hardware/location/ContextHubInfo.java index aaf6c57b9abe..e1137aa51348 100644 --- a/core/java/android/hardware/location/ContextHubInfo.java +++ b/core/java/android/hardware/location/ContextHubInfo.java @@ -16,6 +16,7 @@ package android.hardware.location; import android.annotation.SystemApi; +import android.hardware.contexthub.V1_0.ContextHub; import android.os.Parcel; import android.os.Parcelable; @@ -31,22 +32,52 @@ public class ContextHubInfo { private String mVendor; private String mToolchain; private int mPlatformVersion; - private int mStaticSwVersion; private int mToolchainVersion; private float mPeakMips; private float mStoppedPowerDrawMw; private float mSleepPowerDrawMw; private float mPeakPowerDrawMw; private int mMaxPacketLengthBytes; + private byte mChreApiMajorVersion; + private byte mChreApiMinorVersion; + private short mChrePatchVersion; + private long mChrePlatformId; private int[] mSupportedSensors; private MemoryRegion[] mMemoryRegions; + /* + * TODO(b/67734082): Deprecate this constructor and mark private fields as final. + */ public ContextHubInfo() { } /** + * @hide + */ + public ContextHubInfo(ContextHub contextHub) { + mId = contextHub.hubId; + mName = contextHub.name; + mVendor = contextHub.vendor; + mToolchain = contextHub.toolchain; + mPlatformVersion = contextHub.platformVersion; + mToolchainVersion = contextHub.toolchainVersion; + mPeakMips = contextHub.peakMips; + mStoppedPowerDrawMw = contextHub.stoppedPowerDrawMw; + mSleepPowerDrawMw = contextHub.sleepPowerDrawMw; + mPeakPowerDrawMw = contextHub.peakPowerDrawMw; + mMaxPacketLengthBytes = contextHub.maxSupportedMsgLen; + mChrePlatformId = contextHub.chrePlatformId; + mChreApiMajorVersion = contextHub.chreApiMajorVersion; + mChreApiMinorVersion = contextHub.chreApiMinorVersion; + mChrePatchVersion = contextHub.chrePatchVersion; + + mSupportedSensors = new int[0]; + mMemoryRegions = new MemoryRegion[0]; + } + + /** * returns the maximum number of bytes that can be sent per message to the hub * * @return int - maximum bytes that can be transmitted in a @@ -57,17 +88,6 @@ public class ContextHubInfo { } /** - * set the context hub unique identifer - * - * @param bytes - Maximum number of bytes per message - * - * @hide - */ - public void setMaxPacketLenBytes(int bytes) { - mMaxPacketLengthBytes = bytes; - } - - /** * get the context hub unique identifer * * @return int - unique system wide identifier @@ -77,17 +97,6 @@ public class ContextHubInfo { } /** - * set the context hub unique identifer - * - * @param id - unique system wide identifier for the hub - * - * @hide - */ - public void setId(int id) { - mId = id; - } - - /** * get a string as a hub name * * @return String - a name for the hub @@ -97,17 +106,6 @@ public class ContextHubInfo { } /** - * set a string as the hub name - * - * @param name - the name for the hub - * - * @hide - */ - public void setName(String name) { - mName = name; - } - - /** * get a string as the vendor name * * @return String - a name for the vendor @@ -117,17 +115,6 @@ public class ContextHubInfo { } /** - * set a string as the vendor name - * - * @param vendor - a name for the vendor - * - * @hide - */ - public void setVendor(String vendor) { - mVendor = vendor; - } - - /** * get tool chain string * * @return String - description of the tool chain @@ -137,17 +124,6 @@ public class ContextHubInfo { } /** - * set tool chain string - * - * @param toolchain - description of the tool chain - * - * @hide - */ - public void setToolchain(String toolchain) { - mToolchain = toolchain; - } - - /** * get platform version * * @return int - platform version number @@ -157,34 +133,12 @@ public class ContextHubInfo { } /** - * set platform version - * - * @param platformVersion - platform version number - * - * @hide - */ - public void setPlatformVersion(int platformVersion) { - mPlatformVersion = platformVersion; - } - - /** * get static platform version number * * @return int - platform version number */ public int getStaticSwVersion() { - return mStaticSwVersion; - } - - /** - * set platform software version - * - * @param staticSwVersion - platform static s/w version number - * - * @hide - */ - public void setStaticSwVersion(int staticSwVersion) { - mStaticSwVersion = staticSwVersion; + return (mChreApiMajorVersion << 24) | (mChreApiMinorVersion << 16) | (mChrePatchVersion); } /** @@ -197,17 +151,6 @@ public class ContextHubInfo { } /** - * set the tool chain version number - * - * @param toolchainVersion - tool chain version number - * - * @hide - */ - public void setToolchainVersion(int toolchainVersion) { - mToolchainVersion = toolchainVersion; - } - - /** * get the peak processing mips the hub can support * * @return float - peak MIPS that this hub can deliver @@ -217,17 +160,6 @@ public class ContextHubInfo { } /** - * set the peak mips that this hub can support - * - * @param peakMips - peak mips this hub can deliver - * - * @hide - */ - public void setPeakMips(float peakMips) { - mPeakMips = peakMips; - } - - /** * get the stopped power draw in milliwatts * This assumes that the hub enter a stopped state - which is * different from the sleep state. Latencies on exiting the @@ -241,17 +173,6 @@ public class ContextHubInfo { } /** - * Set the power consumed by the hub in stopped state - * - * @param stoppedPowerDrawMw - stopped power in milli watts - * - * @hide - */ - public void setStoppedPowerDrawMw(float stoppedPowerDrawMw) { - mStoppedPowerDrawMw = stoppedPowerDrawMw; - } - - /** * get the power draw of the hub in sleep mode. This assumes * that the hub supports a sleep mode in which the power draw is * lower than the power consumed when the hub is actively @@ -267,17 +188,6 @@ public class ContextHubInfo { } /** - * Set the sleep power draw in milliwatts - * - * @param sleepPowerDrawMw - sleep power draw in milliwatts. - * - * @hide - */ - public void setSleepPowerDrawMw(float sleepPowerDrawMw) { - mSleepPowerDrawMw = sleepPowerDrawMw; - } - - /** * get the peak powe draw of the hub. This is the power consumed * by the hub at maximum load. * @@ -288,18 +198,6 @@ public class ContextHubInfo { } /** - * set the peak power draw of the hub - * - * @param peakPowerDrawMw - peak power draw of the hub in - * milliwatts. - * - * @hide - */ - public void setPeakPowerDrawMw(float peakPowerDrawMw) { - mPeakPowerDrawMw = peakPowerDrawMw; - } - - /** * get the sensors supported by this hub * * @return int[] - all the supported sensors on this hub @@ -322,46 +220,65 @@ public class ContextHubInfo { } /** - * set the supported sensors on this hub - * - * @param supportedSensors - supported sensors on this hub + * @return the CHRE platform ID as defined in chre/version.h * + * TODO(b/67734082): Expose as public API * @hide */ - public void setSupportedSensors(int[] supportedSensors) { - mSupportedSensors = Arrays.copyOf(supportedSensors, supportedSensors.length); + public long getChrePlatformId() { + return mChrePlatformId; } /** - * set memory regions for this hub + * @return the CHRE API's major version as defined in chre/version.h * - * @param memoryRegions - memory regions information + * TODO(b/67734082): Expose as public API + * @hide + */ + public byte getChreApiMajorVersion() { + return mChreApiMajorVersion; + } + + /** + * @return the CHRE API's minor version as defined in chre/version.h * - * @see MemoryRegion + * TODO(b/67734082): Expose as public API + * @hide + */ + public byte getChreApiMinorVersion() { + return mChreApiMinorVersion; + } + + /** + * @return the CHRE patch version as defined in chre/version.h * + * TODO(b/67734082): Expose as public API * @hide */ - public void setMemoryRegions(MemoryRegion[] memoryRegions) { - mMemoryRegions = Arrays.copyOf(memoryRegions, memoryRegions.length); + public short getChrePatchVersion() { + return mChrePatchVersion; } @Override public String toString() { - String retVal = ""; - retVal += "Id : " + mId; - retVal += ", Name : " + mName; - retVal += "\n\tVendor : " + mVendor; - retVal += ", ToolChain : " + mToolchain; - retVal += "\n\tPlatformVersion : " + mPlatformVersion; - retVal += ", StaticSwVersion : " + mStaticSwVersion; - retVal += "\n\tPeakMips : " + mPeakMips; - retVal += ", StoppedPowerDraw : " + mStoppedPowerDrawMw + " mW"; - retVal += ", PeakPowerDraw : " + mPeakPowerDrawMw + " mW"; - retVal += ", MaxPacketLength : " + mMaxPacketLengthBytes + " Bytes"; - retVal += "\n\tSupported sensors : " + Arrays.toString(mSupportedSensors); - retVal += "\n\tMemory Regions : " + Arrays.toString(mMemoryRegions); - - return retVal; + String retVal = ""; + retVal += "Id : " + mId; + retVal += ", Name : " + mName; + retVal += "\n\tVendor : " + mVendor; + retVal += ", Toolchain : " + mToolchain; + retVal += ", Toolchain version: 0x" + Integer.toHexString(mToolchainVersion); + retVal += "\n\tPlatformVersion : 0x" + Integer.toHexString(mPlatformVersion); + retVal += ", SwVersion : " + + mChreApiMajorVersion + "." + mChreApiMinorVersion + "." + mChrePatchVersion; + retVal += ", CHRE platform ID: 0x" + Long.toHexString(mChrePlatformId); + retVal += "\n\tPeakMips : " + mPeakMips; + retVal += ", StoppedPowerDraw : " + mStoppedPowerDrawMw + " mW"; + retVal += ", PeakPowerDraw : " + mPeakPowerDrawMw + " mW"; + retVal += ", MaxPacketLength : " + mMaxPacketLengthBytes + " Bytes"; + retVal += "\n\tSupported sensors : " + Arrays.toString(mSupportedSensors); + retVal += "\n\tMemory Regions : " + Arrays.toString(mMemoryRegions); + + return retVal; } private ContextHubInfo(Parcel in) { @@ -371,12 +288,15 @@ public class ContextHubInfo { mToolchain = in.readString(); mPlatformVersion = in.readInt(); mToolchainVersion = in.readInt(); - mStaticSwVersion = in.readInt(); mPeakMips = in.readFloat(); mStoppedPowerDrawMw = in.readFloat(); mSleepPowerDrawMw = in.readFloat(); mPeakPowerDrawMw = in.readFloat(); mMaxPacketLengthBytes = in.readInt(); + mChrePlatformId = in.readLong(); + mChreApiMajorVersion = in.readByte(); + mChreApiMinorVersion = in.readByte(); + mChrePatchVersion = (short) in.readInt(); int numSupportedSensors = in.readInt(); mSupportedSensors = new int[numSupportedSensors]; @@ -395,12 +315,15 @@ public class ContextHubInfo { out.writeString(mToolchain); out.writeInt(mPlatformVersion); out.writeInt(mToolchainVersion); - out.writeInt(mStaticSwVersion); out.writeFloat(mPeakMips); out.writeFloat(mStoppedPowerDrawMw); out.writeFloat(mSleepPowerDrawMw); out.writeFloat(mPeakPowerDrawMw); out.writeInt(mMaxPacketLengthBytes); + out.writeLong(mChrePlatformId); + out.writeByte(mChreApiMajorVersion); + out.writeByte(mChreApiMinorVersion); + out.writeInt(mChrePatchVersion); out.writeInt(mSupportedSensors.length); out.writeIntArray(mSupportedSensors); diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java index 60500463d82e..1d66dc6d939f 100644 --- a/core/java/android/hardware/location/ContextHubManager.java +++ b/core/java/android/hardware/location/ContextHubManager.java @@ -15,6 +15,7 @@ */ package android.hardware.location; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; @@ -27,6 +28,8 @@ import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.util.Log; +import java.util.List; + /** * A class that exposes the Context hubs on a device to applications. * @@ -38,7 +41,6 @@ import android.util.Log; @SystemApi @SystemService(Context.CONTEXTHUB_SERVICE) public final class ContextHubManager { - private static final String TAG = "ContextHubManager"; private final Looper mMainLooper; @@ -256,6 +258,183 @@ public final class ContextHubManager { } /** + * Returns the list of context hubs in the system. + * + * @return the list of context hub informations + * + * @see ContextHubInfo + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public List<ContextHubInfo> getContextHubs() { + 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. + * + * After the nanoapp binary is successfully loaded at the specified hub, the nanoapp will be in + * the enabled state. + * + * @param hubInfo the hub to load the nanoapp on + * @param appBinary The app binary to load + * + * @return the ContextHubTransaction of the request + * + * @see NanoAppBinary + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public ContextHubTransaction<Void> loadNanoApp( + ContextHubInfo hubInfo, NanoAppBinary appBinary) { + ContextHubTransaction<Void> transaction = + new ContextHubTransaction<>(ContextHubTransaction.TYPE_LOAD_NANOAPP); + IContextHubTransactionCallback callback = createTransactionCallback(transaction); + + try { + mService.loadNanoAppOnHub(hubInfo.getId(), callback, appBinary); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return transaction; + } + + /** + * Unloads a nanoapp at the specified Context Hub. + * + * @param hubInfo the hub to unload the nanoapp from + * @param nanoAppId the app to unload + * + * @return the ContextHubTransaction of the request + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public ContextHubTransaction<Void> unloadNanoApp(ContextHubInfo hubInfo, long nanoAppId) { + ContextHubTransaction<Void> transaction = + new ContextHubTransaction<>(ContextHubTransaction.TYPE_UNLOAD_NANOAPP); + IContextHubTransactionCallback callback = createTransactionCallback(transaction); + + try { + mService.unloadNanoAppFromHub(hubInfo.getId(), callback, nanoAppId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return transaction; + } + + /** + * Enables a nanoapp at the specified Context Hub. + * + * @param hubInfo the hub to enable the nanoapp on + * @param nanoAppId the app to enable + * + * @return the ContextHubTransaction of the request + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public ContextHubTransaction<Void> enableNanoApp(ContextHubInfo hubInfo, long nanoAppId) { + throw new UnsupportedOperationException("TODO: Implement this"); + } + + /** + * Disables a nanoapp at the specified Context Hub. + * + * @param hubInfo the hub to disable the nanoapp on + * @param nanoAppId the app to disable + * + * @return the ContextHubTransaction of the request + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public ContextHubTransaction<Void> disableNanoApp(ContextHubInfo hubInfo, long nanoAppId) { + throw new UnsupportedOperationException("TODO: Implement this"); + } + + /** + * Requests a query for nanoapps loaded at the specified Context Hub. + * + * @param hubInfo the hub to query a list of nanoapps from + * + * @return the ContextHubTransaction of the request + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) + public ContextHubTransaction<List<NanoAppState>> queryNanoApps(ContextHubInfo hubInfo) { + ContextHubTransaction<List<NanoAppState>> transaction = + new ContextHubTransaction<>(ContextHubTransaction.TYPE_QUERY_NANOAPPS); + IContextHubTransactionCallback callback = createQueryCallback(transaction); + + try { + mService.queryNanoApps(hubInfo.getId(), callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return transaction; + } + + /** * Set a callback to receive messages from the context hub * * @param callback Callback object @@ -307,6 +486,95 @@ public final class ContextHubManager { } /** + * Creates an interface to the ContextHubClient to send down to the service. + * + * @param callback the callback to invoke at the client process + * @param handler the handler to post callbacks for this client + * + * @return the callback interface + */ + private IContextHubClientCallback createClientCallback( + ContextHubClientCallback callback, Handler handler) { + return new IContextHubClientCallback.Stub() { + @Override + public void onMessageFromNanoApp(NanoAppMessage message) { + handler.post(() -> callback.onMessageFromNanoApp(message)); + } + + @Override + public void onHubReset() { + handler.post(() -> callback.onHubReset()); + } + + @Override + public void onNanoAppAborted(long nanoAppId, int abortCode) { + handler.post(() -> callback.onNanoAppAborted(nanoAppId, abortCode)); + } + + @Override + public void onNanoAppLoaded(long nanoAppId) { + handler.post(() -> callback.onNanoAppLoaded(nanoAppId)); + } + + @Override + public void onNanoAppUnloaded(long nanoAppId) { + handler.post(() -> callback.onNanoAppUnloaded(nanoAppId)); + } + + @Override + public void onNanoAppEnabled(long nanoAppId) { + handler.post(() -> callback.onNanoAppEnabled(nanoAppId)); + } + + @Override + public void onNanoAppDisabled(long nanoAppId) { + handler.post(() -> callback.onNanoAppDisabled(nanoAppId)); + } + }; + } + + /** + * Creates and registers a client and its callback with the Context Hub Service. + * + * A client is registered with the Context Hub Service for a specified Context Hub. When the + * registration succeeds, the client can send messages to nanoapps through the returned + * {@link ContextHubClient} object, and receive notifications through the provided callback. + * + * @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 main thread's Looper + * @return the registered client object + * + * @throws IllegalArgumentException if hubInfo does not represent a valid hub + * @throws IllegalStateException if there were too many registered clients at the service + * @throws NullPointerException if callback or hubInfo is null + * + * @hide + * @see ContextHubClientCallback + */ + public ContextHubClient createClient( + ContextHubClientCallback callback, ContextHubInfo hubInfo, @Nullable Handler handler) { + if (callback == null) { + throw new NullPointerException("Callback cannot be null"); + } + if (hubInfo == null) { + throw new NullPointerException("Hub info cannot be null"); + } + + Handler realHandler = (handler == null) ? new Handler(mMainLooper) : handler; + IContextHubClientCallback clientInterface = createClientCallback(callback, realHandler); + + IContextHubClient client; + try { + client = mService.createClient(clientInterface, hubInfo.getId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return new ContextHubClient(client, clientInterface, hubInfo); + } + + /** * Unregister a callback for receive messages from the context hub. * * @see Callback diff --git a/core/java/android/hardware/location/ContextHubTransaction.java b/core/java/android/hardware/location/ContextHubTransaction.java new file mode 100644 index 000000000000..b808de3a11d6 --- /dev/null +++ b/core/java/android/hardware/location/ContextHubTransaction.java @@ -0,0 +1,364 @@ +/* + * 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.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. + * + * This object is generated as a result of an asynchronous request sent to the Context Hub + * 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 #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. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_LOAD_NANOAPP, + TYPE_UNLOAD_NANOAPP, + TYPE_ENABLE_NANOAPP, + TYPE_DISABLE_NANOAPP, + TYPE_QUERY_NANOAPPS}) + public @interface Type {} + public static final int TYPE_LOAD_NANOAPP = 0; + public static final int TYPE_UNLOAD_NANOAPP = 1; + public static final int TYPE_ENABLE_NANOAPP = 2; + public static final int TYPE_DISABLE_NANOAPP = 3; + public static final int TYPE_QUERY_NANOAPPS = 4; + + /** + * Constants describing the result of a transaction or request through the Context Hub Service. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TRANSACTION_SUCCESS, + TRANSACTION_FAILED_UNKNOWN, + TRANSACTION_FAILED_BAD_PARAMS, + TRANSACTION_FAILED_UNINITIALIZED, + TRANSACTION_FAILED_PENDING, + TRANSACTION_FAILED_AT_HUB, + TRANSACTION_FAILED_TIMEOUT, + TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE, + TRANSACTION_FAILED_HAL_UNAVAILABLE}) + public @interface Result {} + public static final int TRANSACTION_SUCCESS = 0; + /** + * Generic failure mode. + */ + public static final int TRANSACTION_FAILED_UNKNOWN = 1; + /** + * Failure mode when the request parameters were not valid. + */ + public static final int TRANSACTION_FAILED_BAD_PARAMS = 2; + /** + * Failure mode when the Context Hub is not initialized. + */ + public static final int TRANSACTION_FAILED_UNINITIALIZED = 3; + /** + * Failure mode when there are too many transactions pending. + */ + public static final int TRANSACTION_FAILED_PENDING = 4; + /** + * Failure mode when the request went through, but failed asynchronously at the hub. + */ + public static final int TRANSACTION_FAILED_AT_HUB = 5; + /** + * 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; + /** + * Failure mode when the Context Hub HAL was not available. + */ + public static final int TRANSACTION_FAILED_HAL_UNAVAILABLE = 8; + + /** + * A class describing the response for a ContextHubTransaction. + * + * @param <R> the type of the contents in the response + */ + public static class Response<R> { + /* + * The result of the transaction. + */ + @ContextHubTransaction.Result + private int mResult; + + /* + * The contents of the response from the Context Hub. + */ + private R mContents; + + Response(@ContextHubTransaction.Result int result, R contents) { + mResult = result; + mContents = contents; + } + + @ContextHubTransaction.Result + public int getResult() { + return mResult; + } + + public R getContents() { + return mContents; + } + } + + /** + * An interface describing the callback to be invoked when a transaction completes. + * + * @param <C> the type of the contents in the transaction response + */ + @FunctionalInterface + public interface Callback<C> { + /** + * The callback to invoke when the transaction completes. + * + * @param transaction the transaction that this callback was attached to. + * @param response the response of the transaction. + */ + void onComplete( + ContextHubTransaction<C> transaction, ContextHubTransaction.Response<C> response); + } + + /* + * The type of the transaction. + */ + @Type + private int mTransactionType; + + /* + * The response of the transaction. + */ + private ContextHubTransaction.Response<T> mResponse; + + /* + * The handler to invoke the aynsc response supplied by onComplete. + */ + private Handler mHandler = null; + + /* + * The callback to invoke when the transaction completes. + */ + private ContextHubTransaction.Callback<T> mCallback = null; + + /* + * 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; + } + + /** + * Converts a transaction type to a human-readable string + * + * @param type the type of a transaction + * @param upperCase {@code true} if upper case the first letter, {@code false} otherwise + * @return a string describing the transaction + */ + public static String typeToString(@Type int type, boolean upperCase) { + switch (type) { + case ContextHubTransaction.TYPE_LOAD_NANOAPP: + return upperCase ? "Load" : "load"; + case ContextHubTransaction.TYPE_UNLOAD_NANOAPP: + return upperCase ? "Unload" : "unload"; + case ContextHubTransaction.TYPE_ENABLE_NANOAPP: + return upperCase ? "Enable" : "enable"; + case ContextHubTransaction.TYPE_DISABLE_NANOAPP: + return upperCase ? "Disable" : "disable"; + case ContextHubTransaction.TYPE_QUERY_NANOAPPS: + return upperCase ? "Query" : "query"; + default: + return upperCase ? "Unknown" : "unknown"; + } + } + + /** + * @return the type of the transaction + */ + @Type + public int getType() { + return mTransactionType; + } + + /** + * Waits to receive the asynchronous transaction result. + * + * This function blocks until the Context Hub Service has received a response + * for the transaction represented by this object by the Context Hub, or a + * specified timeout period has elapsed. + * + * 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) throws InterruptedException, TimeoutException { + boolean success = mDoneSignal.await(timeout, unit); + + if (!success) { + throw new TimeoutException("Timed out while waiting for transaction"); + } + + return mResponse; + } + + /** + * Sets a callback to be invoked when the transaction completes. + * + * This function provides an asynchronous approach to retrieve the result of the + * transaction. When the transaction response has been provided by the Context Hub, + * the given callback will be posted by the provided handler. + * + * If the transaction has already completed at the time of invocation, the callback + * 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 #setCallbackOnComplete(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 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"); + } + } + } + } + + /** + * 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/core/java/android/hardware/location/IContextHubClient.aidl b/core/java/android/hardware/location/IContextHubClient.aidl new file mode 100644 index 000000000000..d81126a0ac54 --- /dev/null +++ b/core/java/android/hardware/location/IContextHubClient.aidl @@ -0,0 +1,31 @@ +/* + * 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.location; + +import android.hardware.location.NanoAppMessage; + +/** + * @hide + */ +interface IContextHubClient { + + // Sends a message to a nanoapp + int sendMessageToNanoApp(in NanoAppMessage message); + + // Closes the connection with the Context Hub + void close(); +} diff --git a/core/java/android/hardware/location/IContextHubClientCallback.aidl b/core/java/android/hardware/location/IContextHubClientCallback.aidl new file mode 100644 index 000000000000..1c76bcbe18ce --- /dev/null +++ b/core/java/android/hardware/location/IContextHubClientCallback.aidl @@ -0,0 +1,49 @@ +/* + * 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.location; + +import android.hardware.location.NanoAppMessage; + +/** + * An interface used by the Context Hub Service to invoke callbacks for lifecycle notifications of a + * Context Hub and nanoapps, as well as for nanoapp messaging. + * + * @hide + */ +oneway interface IContextHubClientCallback { + + // Callback invoked when receiving a message from a nanoapp. + void onMessageFromNanoApp(in NanoAppMessage message); + + // Callback invoked when the attached Context Hub has reset. + void onHubReset(); + + // Callback invoked when a nanoapp aborts at the attached Context Hub. + void onNanoAppAborted(long nanoAppId, int abortCode); + + // Callback invoked when a nanoapp is loaded at the attached Context Hub. + void onNanoAppLoaded(long nanoAppId); + + // Callback invoked when a nanoapp is unloaded from the attached Context Hub. + void onNanoAppUnloaded(long nanoAppId); + + // Callback invoked when a nanoapp is enabled at the attached Context Hub. + void onNanoAppEnabled(long nanoAppId); + + // Callback invoked when a nanoapp is disabled at the attached Context Hub. + void onNanoAppDisabled(long nanoAppId); +} diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl index ff8c1d07ce2c..628ebc7d4579 100644 --- a/core/java/android/hardware/location/IContextHubService.aidl +++ b/core/java/android/hardware/location/IContextHubService.aidl @@ -17,39 +17,59 @@ package android.hardware.location; // Declare any non-default types here with import statements -import android.hardware.location.ContextHubMessage; import android.hardware.location.ContextHubInfo; +import android.hardware.location.ContextHubMessage; import android.hardware.location.NanoApp; -import android.hardware.location.NanoAppInstanceInfo; +import android.hardware.location.NanoAppBinary; import android.hardware.location.NanoAppFilter; +import android.hardware.location.NanoAppInstanceInfo; import android.hardware.location.IContextHubCallback; +import android.hardware.location.IContextHubClient; +import android.hardware.location.IContextHubClientCallback; +import android.hardware.location.IContextHubTransactionCallback; /** * @hide */ interface IContextHubService { - // register a callback to receive messages + // Registers a callback to receive messages int registerCallback(in IContextHubCallback callback); // Gets a list of available context hub handles int[] getContextHubHandles(); - // Get the properties of a hub + // Gets the properties of a hub ContextHubInfo getContextHubInfo(int contextHubHandle); - // Load a nanoapp on a specified context hub + // Loads a nanoapp at the specified hub (old API) int loadNanoApp(int hubHandle, in NanoApp app); - // Unload a nanoapp instance + // Unloads a nanoapp given its instance ID (old API) int unloadNanoApp(int nanoAppInstanceHandle); - // get information about a nanoAppInstance + // Gets the NanoAppInstanceInfo of a nanoapp give its instance ID NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppInstanceHandle); - // find all nanoApp instances matching some filter + // Finds all nanoApp instances matching some filter int[] findNanoAppOnHub(int hubHandle, in NanoAppFilter filter); - // send a message to a nanoApp + // Sends a message to a nanoApp int sendMessage(int hubHandle, int nanoAppHandle, in ContextHubMessage msg); + + // Creates a client to send and receive messages + IContextHubClient createClient(in IContextHubClientCallback client, int contextHubId); + + // Loads a nanoapp at the specified hub (new API) + void loadNanoAppOnHub( + int contextHubId, in IContextHubTransactionCallback transactionCallback, + in NanoAppBinary nanoAppBinary); + + // Unloads a nanoapp on a specified context hub (new API) + void unloadNanoAppFromHub( + int contextHubId, in IContextHubTransactionCallback transactionCallback, + long nanoAppId); + + // Queries for a list of nanoapps + void queryNanoApps(int contextHubId, in IContextHubTransactionCallback transactionCallback); } diff --git a/core/java/android/hardware/location/IContextHubTransactionCallback.aidl b/core/java/android/hardware/location/IContextHubTransactionCallback.aidl new file mode 100644 index 000000000000..5419cd7e60d8 --- /dev/null +++ b/core/java/android/hardware/location/IContextHubTransactionCallback.aidl @@ -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.hardware.location; + +import android.hardware.location.NanoAppState; + +/** + * An interface used by the Context Hub Service to invoke callbacks notifying the complete of a + * transaction. The callbacks are unique for each type of transaction, and the service is + * responsible for invoking the correct callback. + * + * @hide + */ +oneway interface IContextHubTransactionCallback { + + // Callback to be invoked when a query request completes + void onQueryResponse(int result, in List<NanoAppState> nanoappList); + + // Callback to be invoked when a non-query request completes + void onTransactionComplete(int result); +} diff --git a/core/java/android/hardware/location/NanoAppBinary.aidl b/core/java/android/hardware/location/NanoAppBinary.aidl new file mode 100644 index 000000000000..ff50c93e1e44 --- /dev/null +++ b/core/java/android/hardware/location/NanoAppBinary.aidl @@ -0,0 +1,22 @@ +/* + * 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.location; + +/** + * @hide + */ +parcelable NanoAppBinary; diff --git a/core/java/android/hardware/location/NanoAppBinary.java b/core/java/android/hardware/location/NanoAppBinary.java new file mode 100644 index 000000000000..934e9e48c01a --- /dev/null +++ b/core/java/android/hardware/location/NanoAppBinary.java @@ -0,0 +1,250 @@ +/* + * 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.location; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * @hide + */ +public final class NanoAppBinary implements Parcelable { + private static final String TAG = "NanoAppBinary"; + + /* + * The contents of the app binary. + */ + private byte[] mNanoAppBinary; + + /* + * Contents of the nanoapp binary header. + * + * Only valid if mHasValidHeader is true. + * See nano_app_binary_t in context_hub.h for details. + */ + private int mHeaderVersion; + private int mMagic; + private long mNanoAppId; + private int mNanoAppVersion; + private int mFlags; + private long mHwHubType; + private byte mTargetChreApiMajorVersion; + private byte mTargetChreApiMinorVersion; + + private boolean mHasValidHeader = false; + + /* + * The header version used to parse the binary in parseBinaryHeader(). + */ + private static final int EXPECTED_HEADER_VERSION = 1; + + /* + * 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)); + + /* + * Byte order established in context_hub.h + */ + 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(); + } + + /* + * Parses the binary header and populates its field using mNanoAppBinary. + */ + private void parseBinaryHeader() { + ByteBuffer buf = ByteBuffer.wrap(mNanoAppBinary).order(HEADER_ORDER); + + mHasValidHeader = false; + try { + mHeaderVersion = buf.getInt(); + if (mHeaderVersion != EXPECTED_HEADER_VERSION) { + Log.e(TAG, "Unexpected header version " + mHeaderVersion + " while parsing header" + + " (expected " + EXPECTED_HEADER_VERSION + ")"); + return; + } + + mMagic = buf.getInt(); + mNanoAppId = buf.getLong(); + mNanoAppVersion = buf.getInt(); + mFlags = buf.getInt(); + mHwHubType = buf.getLong(); + mTargetChreApiMajorVersion = buf.get(); + mTargetChreApiMinorVersion = buf.get(); + } catch (BufferUnderflowException e) { + Log.e(TAG, "Not enough contents in nanoapp header"); + return; + } + + if (mMagic != EXPECTED_MAGIC_VALUE) { + Log.e(TAG, "Unexpected magic value " + String.format("0x%08X", mMagic) + + "while parsing header (expected " + + String.format("0x%08X", EXPECTED_MAGIC_VALUE) + ")"); + } else { + mHasValidHeader = true; + } + } + + /** + * @return the app binary byte array + */ + 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() { + return mHasValidHeader; + } + + /** + * @return the header version + */ + public int getHeaderVersion() { + return mHeaderVersion; + } + + /** + * @return the app ID parsed from the nanoapp header + */ + public long getNanoAppId() { + return mNanoAppId; + } + + /** + * @return the app version parsed from the nanoapp header + */ + public int getNanoAppVersion() { + return mNanoAppVersion; + } + + /** + * @return the compile target hub type parsed from the nanoapp header + */ + public long getHwHubType() { + return mHwHubType; + } + + /** + * @return the target CHRE API major version parsed from the nanoapp header + */ + public byte getTargetChreApiMajorVersion() { + return mTargetChreApiMajorVersion; + } + + /** + * @return the target CHRE API minor version parsed from the nanoapp header + */ + public byte getTargetChreApiMinorVersion() { + 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]; + in.readByteArray(mNanoAppBinary); + + parseBinaryHeader(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mNanoAppBinary.length); + out.writeByteArray(mNanoAppBinary); + } + + public static final Creator<NanoAppBinary> CREATOR = + new Creator<NanoAppBinary>() { + @Override + public NanoAppBinary createFromParcel(Parcel in) { + return new NanoAppBinary(in); + } + + @Override + public NanoAppBinary[] newArray(int size) { + return new NanoAppBinary[size]; + } + }; +} diff --git a/core/java/android/hardware/location/NanoAppFilter.java b/core/java/android/hardware/location/NanoAppFilter.java index bf35a3d6fbd6..5ccf546a55ad 100644 --- a/core/java/android/hardware/location/NanoAppFilter.java +++ b/core/java/android/hardware/location/NanoAppFilter.java @@ -20,7 +20,6 @@ package android.hardware.location; import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; /** * @hide @@ -130,6 +129,14 @@ public class NanoAppFilter { (versionsMatch(mVersionRestrictionMask, mAppVersion, info.getAppVersion())); } + @Override + public String toString() { + return "nanoAppId: 0x" + Long.toHexString(mAppId) + + ", nanoAppVersion: 0x" + Integer.toHexString(mAppVersion) + + ", versionMask: " + mVersionRestrictionMask + + ", vendorMask: " + mAppIdVendorMask; + } + public static final Parcelable.Creator<NanoAppFilter> CREATOR = new Parcelable.Creator<NanoAppFilter>() { public NanoAppFilter createFromParcel(Parcel in) { diff --git a/core/java/android/hardware/location/NanoAppMessage.aidl b/core/java/android/hardware/location/NanoAppMessage.aidl new file mode 100644 index 000000000000..f1f4ff69b7af --- /dev/null +++ b/core/java/android/hardware/location/NanoAppMessage.aidl @@ -0,0 +1,22 @@ +/* + * 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.location; + +/** + * @hide + */ +parcelable NanoAppMessage; diff --git a/core/java/android/hardware/location/NanoAppMessage.java b/core/java/android/hardware/location/NanoAppMessage.java new file mode 100644 index 000000000000..202867490fb9 --- /dev/null +++ b/core/java/android/hardware/location/NanoAppMessage.java @@ -0,0 +1,143 @@ +/* + * 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.location; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A class describing messages send to or from nanoapps through the Context Hub Service. + * + * The basis of the class is in the IContextHub.hal ContextHubMsg definition. + * + * @hide + */ +public final class NanoAppMessage implements Parcelable { + private long mNanoAppId; + private int mMessageType; + private byte[] mMessageBody; + private boolean mIsBroadcasted; + + private NanoAppMessage( + long nanoAppId, int messageType, byte[] messageBody, boolean broadcasted) { + mNanoAppId = nanoAppId; + mMessageType = messageType; + mMessageBody = messageBody; + mIsBroadcasted = broadcasted; + } + + /** + * Creates a NanoAppMessage object to send to a nanoapp. + * + * This factory method can be used to generate a NanoAppMessage object to be used in + * the ContextHubClient.sendMessageToNanoApp API. + * + * @param targetNanoAppId the ID of the nanoapp to send the message to + * @param messageType the nanoapp-dependent message type + * @param messageBody the byte array message contents + * + * @return the NanoAppMessage object + */ + public static NanoAppMessage createMessageToNanoApp( + long targetNanoAppId, int messageType, byte[] messageBody) { + return new NanoAppMessage( + targetNanoAppId, messageType, messageBody, false /* broadcasted */); + } + + /** + * Creates a NanoAppMessage object sent from a nanoapp. + * + * This factory method is intended only to be used by the Context Hub Service when delivering + * messages from a nanoapp to clients. + * + * @param sourceNanoAppId the ID of the nanoapp that the message was sent from + * @param messageType the nanoapp-dependent message type + * @param messageBody the byte array message contents + * @param broadcasted {@code true} if the message was broadcasted, {@code false} otherwise + * + * @return the NanoAppMessage object + */ + public static NanoAppMessage createMessageFromNanoApp( + long sourceNanoAppId, int messageType, byte[] messageBody, boolean broadcasted) { + return new NanoAppMessage(sourceNanoAppId, messageType, messageBody, broadcasted); + } + + /** + * @return the ID of the source or destination nanoapp + */ + public long getNanoAppId() { + return mNanoAppId; + } + + /** + * @return the type of the message that is nanoapp-dependent + */ + public int getMessageType() { + return mMessageType; + } + + /** + * @return the byte array contents of the message + */ + public byte[] getMessageBody() { + return mMessageBody; + } + + /** + * @return {@code true} if the message is broadcasted, {@code false} otherwise + */ + public boolean isBroadcastMessage() { + return mIsBroadcasted; + } + + private NanoAppMessage(Parcel in) { + mNanoAppId = in.readLong(); + mIsBroadcasted = (in.readInt() == 1); + mMessageType = in.readInt(); + + int msgSize = in.readInt(); + mMessageBody = new byte[msgSize]; + in.readByteArray(mMessageBody); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mNanoAppId); + out.writeInt(mIsBroadcasted ? 1 : 0); + out.writeInt(mMessageType); + + out.writeInt(mMessageBody.length); + out.writeByteArray(mMessageBody); + } + + public static final Creator<NanoAppMessage> CREATOR = + new Creator<NanoAppMessage>() { + @Override + public NanoAppMessage createFromParcel(Parcel in) { + return new NanoAppMessage(in); + } + + @Override + public NanoAppMessage[] newArray(int size) { + return new NanoAppMessage[size]; + } + }; +} diff --git a/core/java/android/hardware/location/NanoAppState.aidl b/core/java/android/hardware/location/NanoAppState.aidl new file mode 100644 index 000000000000..f80f4356ba55 --- /dev/null +++ b/core/java/android/hardware/location/NanoAppState.aidl @@ -0,0 +1,22 @@ +/* + * 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.location; + +/** + * @hide + */ +parcelable NanoAppState; diff --git a/core/java/android/hardware/location/NanoAppState.java b/core/java/android/hardware/location/NanoAppState.java new file mode 100644 index 000000000000..644031b034d5 --- /dev/null +++ b/core/java/android/hardware/location/NanoAppState.java @@ -0,0 +1,88 @@ +/* + * 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.location; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A class describing the nanoapp state information resulting from a query to a Context Hub. + * + * @hide + */ +public final class NanoAppState implements Parcelable { + private long mNanoAppId; + private int mNanoAppVersion; + private boolean mIsEnabled; + + public NanoAppState(long nanoAppId, int appVersion, boolean enabled) { + mNanoAppId = nanoAppId; + mNanoAppVersion = appVersion; + mIsEnabled = enabled; + } + + /** + * @return the NanoAppInfo for this app + */ + public long getNanoAppId() { + return mNanoAppId; + } + + /** + * @return the app version + */ + public long getNanoAppVersion() { + return mNanoAppVersion; + } + + /** + * @return {@code true} if the app is enabled at the Context Hub, {@code false} otherwise + */ + public boolean isEnabled() { + return mIsEnabled; + } + + private NanoAppState(Parcel in) { + mNanoAppId = in.readLong(); + mNanoAppVersion = in.readInt(); + mIsEnabled = (in.readInt() == 1); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mNanoAppId); + out.writeInt(mNanoAppVersion); + out.writeInt(mIsEnabled ? 1 : 0); + } + + public static final Creator<NanoAppState> CREATOR = + new Creator<NanoAppState>() { + @Override + public NanoAppState createFromParcel(Parcel in) { + return new NanoAppState(in); + } + + @Override + public NanoAppState[] newArray(int size) { + return new NanoAppState[size]; + } + }; +} diff --git a/core/java/android/hardware/radio/ITuner.aidl b/core/java/android/hardware/radio/ITuner.aidl index 3aaeb5061de3..18287fae1b9b 100644 --- a/core/java/android/hardware/radio/ITuner.aidl +++ b/core/java/android/hardware/radio/ITuner.aidl @@ -94,5 +94,17 @@ interface ITuner { */ void setAnalogForced(boolean isForced); + /** + * @param parameters Vendor-specific key-value pairs, must be Map<String, String> + * @return Vendor-specific key-value pairs, must be Map<String, String> + */ + Map setParameters(in Map parameters); + + /** + * @param keys Parameter keys to fetch + * @return Vendor-specific key-value pairs, must be Map<String, String> + */ + Map getParameters(in List<String> keys); + boolean isAntennaConnected(); } diff --git a/core/java/android/hardware/radio/ITunerCallback.aidl b/core/java/android/hardware/radio/ITunerCallback.aidl index 6ed171bbb8a9..775e25c7e7cf 100644 --- a/core/java/android/hardware/radio/ITunerCallback.aidl +++ b/core/java/android/hardware/radio/ITunerCallback.aidl @@ -30,4 +30,9 @@ oneway interface ITunerCallback { void onBackgroundScanAvailabilityChange(boolean isAvailable); void onBackgroundScanComplete(); void onProgramListChanged(); + + /** + * @param parameters Vendor-specific key-value pairs, must be Map<String, String> + */ + void onParametersUpdated(in Map parameters); } diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java index 6e8991aa4a4a..e93fd5f1b86b 100644 --- a/core/java/android/hardware/radio/RadioTuner.java +++ b/core/java/android/hardware/radio/RadioTuner.java @@ -309,6 +309,58 @@ public abstract class RadioTuner { public abstract void setAnalogForced(boolean isForced); /** + * Generic method for setting vendor-specific parameter values. + * The framework does not interpret the parameters, they are passed + * in an opaque manner between a vendor application and HAL. + * + * Framework does not make any assumptions on the keys or values, other than + * ones stated in VendorKeyValue documentation (a requirement of key + * prefixes). + * + * For each pair in the result map, the key will be one of the keys + * contained in the input (possibly with wildcards expanded), and the value + * will be a vendor-specific result status (such as "OK" or an error code). + * The implementation may choose to return an empty map, or only return + * a status for a subset of the provided inputs, at its discretion. + * + * Application and HAL must not use keys with unknown prefix. In particular, + * it must not place a key-value pair in results vector for unknown key from + * parameters vector - instead, an unknown key should simply be ignored. + * In other words, results vector may contain a subset of parameter keys + * (however, the framework doesn't enforce a strict subset - the only + * formal requirement is vendor domain prefix for keys). + * + * @param parameters Vendor-specific key-value pairs. + * @return Operation completion status for parameters being set. + * @hide FutureFeature + */ + public abstract @NonNull Map<String, String> + setParameters(@NonNull Map<String, String> parameters); + + /** + * Generic method for retrieving vendor-specific parameter values. + * The framework does not interpret the parameters, they are passed + * in an opaque manner between a vendor application and HAL. + * + * Framework does not cache set/get requests, so it's possible for + * getParameter to return a different value than previous setParameter call. + * + * The syntax and semantics of keys are up to the vendor (as long as prefix + * rules are obeyed). For instance, vendors may include some form of + * wildcard support. In such case, result vector may be of different size + * than requested keys vector. However, wildcards are not recognized by + * framework and they are passed as-is to the HAL implementation. + * + * Unknown keys must be ignored and not placed into results vector. + * + * @param keys Parameter keys to fetch. + * @return Vendor-specific key-value pairs. + * @hide FutureFeature + */ + public abstract @NonNull Map<String, String> + getParameters(@NonNull List<String> keys); + + /** * Get current antenna connection state for current configuration. * Only valid if a configuration has been applied. * @return {@code true} if the antenna is connected, {@code false} otherwise. @@ -429,6 +481,22 @@ public abstract class RadioTuner { * Use {@link RadioTuner#getProgramList(String)} to get an actual list. */ public void onProgramListChanged() {} + + /** + * Generic callback for passing updates to vendor-specific parameter values. + * The framework does not interpret the parameters, they are passed + * in an opaque manner between a vendor application and HAL. + * + * It's up to the HAL implementation if and how to implement this callback, + * as long as it obeys the prefix rule. In particular, only selected keys + * may be notified this way. However, setParameters must not trigger + * this callback, while an internal event can change parameters + * asynchronously. + * + * @param parameters Vendor-specific key-value pairs. + * @hide FutureFeature + */ + public void onParametersUpdated(@NonNull Map<String, String> parameters) {} } } diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java index b62196902570..864d17c2de9f 100644 --- a/core/java/android/hardware/radio/TunerAdapter.java +++ b/core/java/android/hardware/radio/TunerAdapter.java @@ -24,6 +24,7 @@ import android.util.Log; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Implements the RadioTuner interface by forwarding calls to radio service. @@ -251,6 +252,24 @@ class TunerAdapter extends RadioTuner { } @Override + public @NonNull Map<String, String> setParameters(@NonNull Map<String, String> parameters) { + try { + return mTuner.setParameters(Objects.requireNonNull(parameters)); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override + public @NonNull Map<String, String> getParameters(@NonNull List<String> keys) { + try { + return mTuner.getParameters(Objects.requireNonNull(keys)); + } catch (RemoteException e) { + throw new RuntimeException("service died", e); + } + } + + @Override public boolean isAntennaConnected() { try { return mTuner.isAntennaConnected(); diff --git a/core/java/android/hardware/radio/TunerCallbackAdapter.java b/core/java/android/hardware/radio/TunerCallbackAdapter.java index ffd5b30fa15c..a01f658e80f6 100644 --- a/core/java/android/hardware/radio/TunerCallbackAdapter.java +++ b/core/java/android/hardware/radio/TunerCallbackAdapter.java @@ -22,6 +22,8 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; +import java.util.Map; + /** * Implements the ITunerCallback interface by forwarding calls to RadioTuner.Callback. */ @@ -94,4 +96,9 @@ class TunerCallbackAdapter extends ITunerCallback.Stub { public void onProgramListChanged() { mHandler.post(() -> mCallback.onProgramListChanged()); } + + @Override + public void onParametersUpdated(Map parameters) { + mHandler.post(() -> mCallback.onParametersUpdated(parameters)); + } } diff --git a/core/java/android/hardware/sidekick/SidekickInternal.java b/core/java/android/hardware/sidekick/SidekickInternal.java new file mode 100644 index 000000000000..fe3550b24cae --- /dev/null +++ b/core/java/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/core/java/android/hardware/usb/AccessoryFilter.java b/core/java/android/hardware/usb/AccessoryFilter.java new file mode 100644 index 000000000000..d9b7c5be7ddd --- /dev/null +++ b/core/java/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/core/java/android/hardware/usb/DeviceFilter.java b/core/java/android/hardware/usb/DeviceFilter.java new file mode 100644 index 000000000000..439c629758b0 --- /dev/null +++ b/core/java/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/core/java/android/hardware/usb/IUsbManager.aidl b/core/java/android/hardware/usb/IUsbManager.aidl index 025d46d12567..151e62de7b70 100644 --- a/core/java/android/hardware/usb/IUsbManager.aidl +++ b/core/java/android/hardware/usb/IUsbManager.aidl @@ -34,7 +34,7 @@ interface IUsbManager /* Returns a file descriptor for communicating with the USB device. * The native fd can be passed to usb_device_new() in libusbhost. */ - ParcelFileDescriptor openDevice(String deviceName); + ParcelFileDescriptor openDevice(String deviceName, String packageName); /* Returns the currently attached USB accessory */ UsbAccessory getCurrentAccessory(); @@ -55,7 +55,7 @@ interface IUsbManager void setAccessoryPackage(in UsbAccessory accessory, String packageName, int userId); /* Returns true if the caller has permission to access the device. */ - boolean hasDevicePermission(in UsbDevice device); + boolean hasDevicePermission(in UsbDevice device, String packageName); /* Returns true if the caller has permission to access the accessory. */ boolean hasAccessoryPermission(in UsbAccessory accessory); diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java index 1fa606cf60da..bdb90bcca4f8 100644 --- a/core/java/android/hardware/usb/UsbManager.java +++ b/core/java/android/hardware/usb/UsbManager.java @@ -17,10 +17,13 @@ package android.hardware.usb; +import android.Manifest; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SdkConstant; -import android.annotation.SystemService; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SystemApi; +import android.annotation.SystemService; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -341,7 +344,7 @@ public class UsbManager { public UsbDeviceConnection openDevice(UsbDevice device) { try { String deviceName = device.getDeviceName(); - ParcelFileDescriptor pfd = mService.openDevice(deviceName); + ParcelFileDescriptor pfd = mService.openDevice(deviceName, mContext.getPackageName()); if (pfd != null) { UsbDeviceConnection connection = new UsbDeviceConnection(device); boolean result = connection.open(deviceName, pfd, mContext); @@ -397,6 +400,9 @@ public class UsbManager { * Permission might have been granted temporarily via * {@link #requestPermission(UsbDevice, PendingIntent)} or * by the user choosing the caller as the default application for the device. + * Permission for USB devices of class {@link UsbConstants#USB_CLASS_VIDEO} for clients that + * target SDK {@link android.os.Build.VERSION_CODES#P} and above can be granted only if they + * have additionally the {@link android.Manifest.permission#CAMERA} permission. * * @param device to check permissions for * @return true if caller has permission @@ -406,7 +412,7 @@ public class UsbManager { return false; } try { - return mService.hasDevicePermission(device); + return mService.hasDevicePermission(device, mContext.getPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -447,6 +453,10 @@ public class UsbManager { * permission was granted by the user * </ul> * + * Permission for USB devices of class {@link UsbConstants#USB_CLASS_VIDEO} for clients that + * target SDK {@link android.os.Build.VERSION_CODES#P} and above can be granted only if they + * have additionally the {@link android.Manifest.permission#CAMERA} permission. + * * @param device to request permissions for * @param pi PendingIntent for returning result */ @@ -519,6 +529,8 @@ public class UsbManager { * * {@hide} */ + @SystemApi + @RequiresPermission(Manifest.permission.MANAGE_USB) public void grantPermission(UsbDevice device, String packageName) { try { int uid = mContext.getPackageManager() diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java index 29177b6b47cf..185215a5ce75 100644 --- a/core/java/android/inputmethodservice/AbstractInputMethodService.java +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -16,6 +16,7 @@ package android.inputmethodservice; +import android.annotation.MainThread; import android.annotation.NonNull; import android.app.Service; import android.content.Intent; @@ -62,6 +63,7 @@ public abstract class AbstractInputMethodService extends Service * back to {@link AbstractInputMethodService#onCreateInputMethodSessionInterface() * AbstractInputMethodService.onCreateInputMethodSessionInterface()}. */ + @MainThread public void createSession(SessionCallback callback) { callback.sessionCreated(onCreateInputMethodSessionInterface()); } @@ -71,6 +73,7 @@ public abstract class AbstractInputMethodService extends Service * {@link AbstractInputMethodSessionImpl#revokeSelf() * AbstractInputMethodSessionImpl.setEnabled()} method. */ + @MainThread public void setSessionEnabled(InputMethodSession session, boolean enabled) { ((AbstractInputMethodSessionImpl)session).setEnabled(enabled); } @@ -80,6 +83,7 @@ public abstract class AbstractInputMethodService extends Service * {@link AbstractInputMethodSessionImpl#revokeSelf() * AbstractInputMethodSessionImpl.revokeSelf()} method. */ + @MainThread public void revokeSession(InputMethodSession session) { ((AbstractInputMethodSessionImpl)session).revokeSelf(); } diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index 765aff96c704..2c7e51a1db25 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -16,14 +16,8 @@ package android.inputmethodservice; -import com.android.internal.os.HandlerCaller; -import com.android.internal.os.SomeArgs; -import com.android.internal.view.IInputContext; -import com.android.internal.view.IInputMethod; -import com.android.internal.view.IInputMethodSession; -import com.android.internal.view.IInputSessionCallback; -import com.android.internal.view.InputConnectionWrapper; - +import android.annotation.BinderThread; +import android.annotation.MainThread; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; @@ -41,11 +35,20 @@ import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodSession; import android.view.inputmethod.InputMethodSubtype; +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethod; +import com.android.internal.view.IInputMethodSession; +import com.android.internal.view.IInputSessionCallback; +import com.android.internal.view.InputConnectionWrapper; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; /** * Implements the internal IInputMethod interface to convert incoming calls @@ -67,17 +70,27 @@ class IInputMethodWrapper extends IInputMethod.Stub private static final int DO_SHOW_SOFT_INPUT = 60; private static final int DO_HIDE_SOFT_INPUT = 70; private static final int DO_CHANGE_INPUTMETHOD_SUBTYPE = 80; - + final WeakReference<AbstractInputMethodService> mTarget; final Context mContext; final HandlerCaller mCaller; final WeakReference<InputMethod> mInputMethod; final int mTargetSdkVersion; - - static class Notifier { - boolean notified; - } - + + /** + * This is not {@null} only between {@link #bindInput(InputBinding)} and {@link #unbindInput()} + * so that {@link InputConnectionWrapper} can query if {@link #unbindInput()} has already been + * called or not, mainly to avoid unnecessary blocking operations. + * + * <p>This field must be set and cleared only from the binder thread(s), where the system + * guarantees that {@link #bindInput(InputBinding)}, + * {@link #startInput(IBinder, IInputContext, int, EditorInfo, boolean)}, and + * {@link #unbindInput()} are called with the same order as the original calls + * in {@link com.android.server.InputMethodManagerService}. See {@link IBinder#FLAG_ONEWAY} + * for detailed semantics.</p> + */ + AtomicBoolean mIsUnbindIssued = null; + // NOTE: we should have a cache of these. static final class InputMethodSessionCallbackWrapper implements InputMethod.SessionCallback { final Context mContext; @@ -108,20 +121,16 @@ class IInputMethodWrapper extends IInputMethod.Stub } } } - - public IInputMethodWrapper(AbstractInputMethodService context, - InputMethod inputMethod) { - mTarget = new WeakReference<AbstractInputMethodService>(context); + + public IInputMethodWrapper(AbstractInputMethodService context, InputMethod inputMethod) { + mTarget = new WeakReference<>(context); mContext = context.getApplicationContext(); mCaller = new HandlerCaller(mContext, null, this, true /*asyncHandler*/); - mInputMethod = new WeakReference<InputMethod>(inputMethod); + mInputMethod = new WeakReference<>(inputMethod); mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion; } - public InputMethod getInternalInputMethod() { - return mInputMethod.get(); - } - + @MainThread @Override public void executeMessage(Message msg) { InputMethod inputMethod = mInputMethod.get(); @@ -169,8 +178,10 @@ class IInputMethodWrapper extends IInputMethod.Stub final IBinder startInputToken = (IBinder) args.arg1; final IInputContext inputContext = (IInputContext) args.arg2; final EditorInfo info = (EditorInfo) args.arg3; + final AtomicBoolean isUnbindIssued = (AtomicBoolean) args.arg4; final InputConnection ic = inputContext != null - ? new InputConnectionWrapper(mTarget, inputContext, missingMethods) : null; + ? new InputConnectionWrapper( + mTarget, inputContext, missingMethods, isUnbindIssued) : null; info.makeCompatible(mTargetSdkVersion); inputMethod.dispatchStartInputWithToken(ic, info, restarting /* restarting */, startInputToken); @@ -205,6 +216,7 @@ class IInputMethodWrapper extends IInputMethod.Stub Log.w(TAG, "Unhandled message code: " + msg.what); } + @BinderThread @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { AbstractInputMethodService target = mTarget.get(); @@ -232,40 +244,63 @@ class IInputMethodWrapper extends IInputMethod.Stub } } + @BinderThread @Override public void attachToken(IBinder token) { mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_ATTACH_TOKEN, token)); } + @BinderThread @Override public void bindInput(InputBinding binding) { + if (mIsUnbindIssued != null) { + Log.e(TAG, "bindInput must be paired with unbindInput."); + } + mIsUnbindIssued = new AtomicBoolean(); // This IInputContext is guaranteed to implement all the methods. final int missingMethodFlags = 0; InputConnection ic = new InputConnectionWrapper(mTarget, - IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags); + IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags, + mIsUnbindIssued); InputBinding nu = new InputBinding(ic, binding); mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu)); } + @BinderThread @Override public void unbindInput() { + if (mIsUnbindIssued != null) { + // Signal the flag then forget it. + mIsUnbindIssued.set(true); + mIsUnbindIssued = null; + } else { + Log.e(TAG, "unbindInput must be paired with bindInput."); + } mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_UNSET_INPUT_CONTEXT)); } + @BinderThread @Override public void startInput(IBinder startInputToken, IInputContext inputContext, @InputConnectionInspector.MissingMethodFlags final int missingMethods, EditorInfo attribute, boolean restarting) { - mCaller.executeOrSendMessage(mCaller.obtainMessageIIOOO(DO_START_INPUT, - missingMethods, restarting ? 1 : 0, startInputToken, inputContext, attribute)); + if (mIsUnbindIssued == null) { + Log.e(TAG, "startInput must be called after bindInput."); + mIsUnbindIssued = new AtomicBoolean(); + } + mCaller.executeOrSendMessage(mCaller.obtainMessageIIOOOO(DO_START_INPUT, + missingMethods, restarting ? 1 : 0, startInputToken, inputContext, attribute, + mIsUnbindIssued)); } + @BinderThread @Override public void createSession(InputChannel channel, IInputSessionCallback callback) { mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_SESSION, channel, callback)); } + @BinderThread @Override public void setSessionEnabled(IInputMethodSession session, boolean enabled) { try { @@ -282,6 +317,7 @@ class IInputMethodWrapper extends IInputMethod.Stub } } + @BinderThread @Override public void revokeSession(IInputMethodSession session) { try { @@ -297,18 +333,21 @@ class IInputMethodWrapper extends IInputMethod.Stub } } + @BinderThread @Override public void showSoftInput(int flags, ResultReceiver resultReceiver) { mCaller.executeOrSendMessage(mCaller.obtainMessageIO(DO_SHOW_SOFT_INPUT, flags, resultReceiver)); } + @BinderThread @Override public void hideSoftInput(int flags, ResultReceiver resultReceiver) { mCaller.executeOrSendMessage(mCaller.obtainMessageIO(DO_HIDE_SOFT_INPUT, flags, resultReceiver)); } + @BinderThread @Override public void changeInputMethodSubtype(InputMethodSubtype subtype) { mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_CHANGE_INPUTMETHOD_SUBTYPE, diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 7a20943e2a4b..02b1c658b2ec 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -382,20 +382,24 @@ public class InputMethodService extends AbstractInputMethodService { */ public class InputMethodImpl extends AbstractInputMethodImpl { /** - * Take care of attaching the given window token provided by the system. + * {@inheritDoc} */ + @MainThread + @Override public void attachToken(IBinder token) { if (mToken == null) { mToken = token; mWindow.setToken(token); } } - + /** - * Handle a new input binding, calling - * {@link InputMethodService#onBindInput InputMethodService.onBindInput()} - * when done. + * {@inheritDoc} + * + * <p>Calls {@link InputMethodService#onBindInput()} when done.</p> */ + @MainThread + @Override public void bindInput(InputBinding binding) { mInputBinding = binding; mInputConnection = binding.getConnection(); @@ -409,8 +413,12 @@ public class InputMethodService extends AbstractInputMethodService { } /** - * Clear the current input binding. + * {@inheritDoc} + * + * <p>Calls {@link InputMethodService#onUnbindInput()} when done.</p> */ + @MainThread + @Override public void unbindInput() { if (DEBUG) Log.v(TAG, "unbindInput(): binding=" + mInputBinding + " ic=" + mInputConnection); @@ -419,11 +427,21 @@ public class InputMethodService extends AbstractInputMethodService { mInputConnection = null; } + /** + * {@inheritDoc} + */ + @MainThread + @Override public void startInput(InputConnection ic, EditorInfo attribute) { if (DEBUG) Log.v(TAG, "startInput(): editor=" + attribute); doStartInput(ic, attribute, false); } + /** + * {@inheritDoc} + */ + @MainThread + @Override public void restartInput(InputConnection ic, EditorInfo attribute) { if (DEBUG) Log.v(TAG, "restartInput(): editor=" + attribute); doStartInput(ic, attribute, true); @@ -433,6 +451,7 @@ public class InputMethodService extends AbstractInputMethodService { * {@inheritDoc} * @hide */ + @MainThread @Override public void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo, boolean restarting, @@ -447,8 +466,10 @@ public class InputMethodService extends AbstractInputMethodService { } /** - * Handle a request by the system to hide the soft input area. + * {@inheritDoc} */ + @MainThread + @Override public void hideSoftInput(int flags, ResultReceiver resultReceiver) { if (DEBUG) Log.v(TAG, "hideSoftInput()"); boolean wasVis = isInputViewShown(); @@ -465,8 +486,10 @@ public class InputMethodService extends AbstractInputMethodService { } /** - * Handle a request by the system to show the soft input area. + * {@inheritDoc} */ + @MainThread + @Override public void showSoftInput(int flags, ResultReceiver resultReceiver) { if (DEBUG) Log.v(TAG, "showSoftInput()"); boolean wasVis = isInputViewShown(); @@ -495,6 +518,11 @@ public class InputMethodService extends AbstractInputMethodService { } } + /** + * {@inheritDoc} + */ + @MainThread + @Override public void changeInputMethodSubtype(InputMethodSubtype subtype) { onCurrentInputMethodSubtypeChanged(subtype); } @@ -1036,7 +1064,89 @@ public class InputMethodService extends AbstractInputMethodService { } return mInputConnection; } - + + /** + * Force switch to a new input method component. This can only be called + * from an application or a service which has a token of the currently active input method. + * @param id The unique identifier for the new input method to be switched to. + */ + public void setInputMethod(String id) { + mImm.setInputMethodInternal(mToken, id); + } + + /** + * Force switch to a new input method and subtype. This can only be called + * from an application or a service which has a token of the currently active input method. + * @param id The unique identifier for the new input method to be switched to. + * @param subtype The new subtype of the new input method to be switched to. + */ + public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype) { + mImm.setInputMethodAndSubtypeInternal(mToken, id, subtype); + } + + /** + * Close/hide the input method's soft input area, so the user no longer + * sees it or can interact with it. This can only be called + * from the currently active input method, as validated by the given token. + * + * @param flags Provides additional operating flags. Currently may be + * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY}, + * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set. + */ + public void hideSoftInputFromInputMethod(int flags) { + mImm.hideSoftInputFromInputMethodInternal(mToken, flags); + } + + /** + * Show the input method's soft input area, so the user + * sees the input method window and can interact with it. + * This can only be called from the currently active input method, + * as validated by the given token. + * + * @param flags Provides additional operating flags. Currently may be + * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT} or + * {@link InputMethodManager#SHOW_FORCED} bit set. + */ + public void showSoftInputFromInputMethod(int flags) { + mImm.showSoftInputFromInputMethodInternal(mToken, flags); + } + + /** + * Force switch to the last used input method and subtype. If the last input method didn't have + * any subtypes, the framework will simply switch to the last input method with no subtype + * specified. + * @return true if the current input method and subtype was successfully switched to the last + * used input method and subtype. + */ + public boolean switchToLastInputMethod() { + return mImm.switchToLastInputMethodInternal(mToken); + } + + /** + * Force switch to the next input method and subtype. If there is no IME enabled except + * current IME and subtype, do nothing. + * @param onlyCurrentIme if true, the framework will find the next subtype which + * belongs to the current IME + * @return true if the current input method and subtype was successfully switched to the next + * input method and subtype. + */ + public boolean switchToNextInputMethod(boolean onlyCurrentIme) { + return mImm.switchToNextInputMethodInternal(mToken, onlyCurrentIme); + } + + /** + * Returns true if the current IME needs to offer the users ways to switch to a next input + * method (e.g. a globe key.). + * When an IME sets supportsSwitchingToNextInputMethod and this method returns true, + * the IME has to offer ways to to invoke {@link #switchToNextInputMethod} accordingly. + * <p> Note that the system determines the most appropriate next input method + * and subtype in order to provide the consistent user experience in switching + * between IMEs and subtypes. + */ + public boolean shouldOfferSwitchingToNextInputMethod() { + return mImm.shouldOfferSwitchingToNextInputMethodInternal(mToken); + } + public boolean getCurrentInputStarted() { return mInputStarted; } diff --git a/core/java/android/net/LocalServerSocket.java b/core/java/android/net/LocalServerSocket.java index 3fcde33071e9..d1f49d2082f5 100644 --- a/core/java/android/net/LocalServerSocket.java +++ b/core/java/android/net/LocalServerSocket.java @@ -16,14 +16,15 @@ package android.net; -import java.io.IOException; +import java.io.Closeable; import java.io.FileDescriptor; +import java.io.IOException; /** * Non-standard class for creating an inbound UNIX-domain socket * in the Linux abstract namespace. */ -public class LocalServerSocket { +public class LocalServerSocket implements Closeable { private final LocalSocketImpl impl; private final LocalSocketAddress localAddress; @@ -106,7 +107,7 @@ public class LocalServerSocket { * * @throws IOException */ - public void close() throws IOException + @Override public void close() throws IOException { impl.close(); } diff --git a/core/java/android/net/MacAddress.java b/core/java/android/net/MacAddress.java index 5620a627df7f..34588614fcc8 100644 --- a/core/java/android/net/MacAddress.java +++ b/core/java/android/net/MacAddress.java @@ -33,8 +33,6 @@ import java.util.Random; * * This class only supports 48 bits long addresses and does not support 64 bits long addresses. * Instances of this class are immutable. - * - * @hide */ public final class MacAddress implements Parcelable { diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java index 7c897de9b5e9..f468e5d2f92b 100644 --- a/core/java/android/net/NetworkCapabilities.java +++ b/core/java/android/net/NetworkCapabilities.java @@ -895,9 +895,7 @@ public final class NetworkCapabilities implements Parcelable { // Ignore NOT_METERED being added or removed as it is effectively dynamic. http://b/63326103 // TODO: properly support NOT_METERED as a mutable and requestable capability. - // Ignore DUN being added or removed. http://b/65257223. - final long mask = ~MUTABLE_CAPABILITIES - & ~(1 << NET_CAPABILITY_NOT_METERED) & ~(1 << NET_CAPABILITY_DUN); + final long mask = ~MUTABLE_CAPABILITIES & ~(1 << NET_CAPABILITY_NOT_METERED); long oldImmutableCapabilities = this.mNetworkCapabilities & mask; long newImmutableCapabilities = that.mNetworkCapabilities & mask; if (oldImmutableCapabilities != newImmutableCapabilities) { diff --git a/core/java/android/net/NetworkScoreManager.java b/core/java/android/net/NetworkScoreManager.java index 54e1899e9a00..50dd468aa905 100644 --- a/core/java/android/net/NetworkScoreManager.java +++ b/core/java/android/net/NetworkScoreManager.java @@ -226,6 +226,8 @@ public class NetworkScoreManager { * the {@link #ACTION_CHANGE_ACTIVE} intent. * @return the full package name of the current active scorer, or null if there is no active * scorer. + * @throws SecurityException if the caller doesn't hold either {@link permission#SCORE_NETWORKS} + * or {@link permission#REQUEST_NETWORK_SCORES} permissions. */ @RequiresPermission(anyOf = {android.Manifest.permission.SCORE_NETWORKS, android.Manifest.permission.REQUEST_NETWORK_SCORES}) @@ -240,6 +242,8 @@ public class NetworkScoreManager { /** * Returns metadata about the active scorer or <code>null</code> if there is no active scorer. * + * @throws SecurityException if the caller does not hold the + * {@link permission#REQUEST_NETWORK_SCORES} permission. * @hide */ @Nullable @@ -256,8 +260,11 @@ public class NetworkScoreManager { * Returns the list of available scorer apps. The list will be empty if there are * no valid scorers. * + * @throws SecurityException if the caller does not hold the + * {@link permission#REQUEST_NETWORK_SCORES} permission. * @hide */ + @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES) public List<NetworkScorerAppData> getAllValidScorers() { try { return mService.getAllValidScorers(); @@ -296,9 +303,11 @@ public class NetworkScoreManager { * from one scorer cannot be compared to those from another scorer. * * @return whether the clear was successful. - * @throws SecurityException if the caller is not the active scorer or privileged. + * @throws SecurityException if the caller is not the active scorer or if the caller doesn't + * hold the {@link permission#REQUEST_NETWORK_SCORES} permission. */ - @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES) + @RequiresPermission(anyOf = {android.Manifest.permission.SCORE_NETWORKS, + android.Manifest.permission.REQUEST_NETWORK_SCORES}) public boolean clearScores() throws SecurityException { try { return mService.clearScores(); @@ -314,12 +323,13 @@ public class NetworkScoreManager { * the {@link #ACTION_CHANGE_ACTIVE} broadcast, or using a custom configuration activity. * * @return true if the operation succeeded, or false if the new package is not a valid scorer. - * @throws SecurityException if the caller is not a system process or does not hold the - * {@link permission#SCORE_NETWORKS} permission + * @throws SecurityException if the caller doesn't hold either {@link permission#SCORE_NETWORKS} + * or {@link permission#REQUEST_NETWORK_SCORES} permissions. * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.SCORE_NETWORKS) + @RequiresPermission(anyOf = {android.Manifest.permission.SCORE_NETWORKS, + android.Manifest.permission.REQUEST_NETWORK_SCORES}) public boolean setActiveScorer(String packageName) throws SecurityException { try { return mService.setActiveScorer(packageName); @@ -333,9 +343,11 @@ public class NetworkScoreManager { * * <p>May only be called by the current scorer app, or the system. * - * @throws SecurityException if the caller is neither the active scorer nor the system. + * @throws SecurityException if the caller is not the active scorer or if the caller doesn't + * hold the {@link permission#REQUEST_NETWORK_SCORES} permission. */ - @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES) + @RequiresPermission(anyOf = {android.Manifest.permission.SCORE_NETWORKS, + android.Manifest.permission.REQUEST_NETWORK_SCORES}) public void disableScoring() throws SecurityException { try { mService.disableScoring(); @@ -352,6 +364,7 @@ public class NetworkScoreManager { * {@link permission#REQUEST_NETWORK_SCORES} permission. * @hide */ + @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES) public boolean requestScores(NetworkKey[] networks) throws SecurityException { try { return mService.requestScores(networks); @@ -371,6 +384,7 @@ public class NetworkScoreManager { * @deprecated equivalent to registering for cache updates with CACHE_FILTER_NONE. * @hide */ + @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES) @Deprecated // migrate to registerNetworkScoreCache(int, INetworkScoreCache, int) public void registerNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) { registerNetworkScoreCache(networkType, scoreCache, CACHE_FILTER_NONE); @@ -387,6 +401,7 @@ public class NetworkScoreManager { * @throws IllegalArgumentException if a score cache is already registered for this type. * @hide */ + @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES) public void registerNetworkScoreCache(int networkType, INetworkScoreCache scoreCache, @CacheUpdateFilter int filterType) { try { @@ -406,6 +421,7 @@ public class NetworkScoreManager { * @throws IllegalArgumentException if a score cache is already registered for this type. * @hide */ + @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES) public void unregisterNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) { try { mService.unregisterNetworkScoreCache(networkType, scoreCache); @@ -419,8 +435,11 @@ public class NetworkScoreManager { * * @param callingUid the UID to check * @return true if the provided UID is the active scorer, false otherwise. + * @throws SecurityException if the caller does not hold the + * {@link permission#REQUEST_NETWORK_SCORES} permission. * @hide */ + @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES) public boolean isCallerActiveScorer(int callingUid) { try { return mService.isCallerActiveScorer(callingUid); diff --git a/core/java/android/net/NetworkWatchlistManager.java b/core/java/android/net/NetworkWatchlistManager.java new file mode 100644 index 000000000000..42e43c8aea1e --- /dev/null +++ b/core/java/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/core/java/android/net/PskKeyManager.java b/core/java/android/net/PskKeyManager.java deleted file mode 100644 index f5167eafab62..000000000000 --- a/core/java/android/net/PskKeyManager.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2014 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 com.android.org.conscrypt.PSKKeyManager; -import java.net.Socket; -import javax.crypto.SecretKey; -import javax.net.ssl.SSLEngine; - -/** - * Provider of key material for pre-shared key (PSK) key exchange used in TLS-PSK cipher suites. - * - * <h3>Overview of TLS-PSK</h3> - * - * <p>TLS-PSK is a set of TLS/SSL cipher suites which rely on a symmetric pre-shared key (PSK) to - * secure the TLS/SSL connection and mutually authenticate its peers. These cipher suites may be - * a more natural fit compared to conventional public key based cipher suites in some scenarios - * where communication between peers is bootstrapped via a separate step (for example, a pairing - * step) and requires both peers to authenticate each other. In such scenarios a symmetric key (PSK) - * can be exchanged during the bootstrapping step, removing the need to generate and exchange public - * key pairs and X.509 certificates.</p> - * - * <p>When a TLS-PSK cipher suite is used, both peers have to use the same key for the TLS/SSL - * handshake to succeed. Thus, both peers are implicitly authenticated by a successful handshake. - * This removes the need to use a {@code TrustManager} in conjunction with this {@code KeyManager}. - * </p> - * - * <h3>Supporting multiple keys</h3> - * - * <p>A peer may have multiple keys to choose from. To help choose the right key, during the - * handshake the server can provide a <em>PSK identity hint</em> to the client, and the client can - * provide a <em>PSK identity</em> to the server. The contents of these two pieces of information - * are specific to application-level protocols.</p> - * - * <p><em>NOTE: Both the PSK identity hint and the PSK identity are transmitted in cleartext. - * Moreover, these data are received and processed prior to peer having been authenticated. Thus, - * they must not contain or leak key material or other sensitive information, and should be - * treated (e.g., parsed) with caution, as untrusted data.</em></p> - * - * <p>The high-level flow leading to peers choosing a key during TLS/SSL handshake is as follows: - * <ol> - * <li>Server receives a handshake request from client. - * <li>Server replies, optionally providing a PSK identity hint to client.</li> - * <li>Client chooses the key.</li> - * <li>Client provides a PSK identity of the chosen key to server.</li> - * <li>Server chooses the key.</li> - * </ol></p> - * - * <p>In the flow above, either peer can signal that they do not have a suitable key, in which case - * the the handshake will be aborted immediately. This may enable a network attacker who does not - * know the key to learn which PSK identity hints or PSK identities are supported. If this is a - * concern then a randomly generated key should be used in the scenario where no key is available. - * This will lead to the handshake aborting later, due to key mismatch -- same as in the scenario - * where a key is available -- making it appear to the attacker that all PSK identity hints and PSK - * identities are supported.</p> - * - * <h3>Maximum sizes</h3> - * - * <p>The maximum supported sizes are as follows: - * <ul> - * <li>256 bytes for keys (see {@link #MAX_KEY_LENGTH_BYTES}),</li> - * <li>128 bytes for PSK identity and PSK identity hint (in modified UTF-8 representation) (see - * {@link #MAX_IDENTITY_LENGTH_BYTES} and {@link #MAX_IDENTITY_HINT_LENGTH_BYTES}).</li> - * </ul></p> - * - * <h3>Subclassing</h3> - * Subclasses should normally provide their own implementation of {@code getKey} because the default - * implementation returns no key, which aborts the handshake. - * - * <h3>Known issues</h3> - * The implementation of {@code ECDHE_PSK} cipher suites in API Level 21 contains a bug which breaks - * compatibility with other implementations. {@code ECDHE_PSK} cipher suites are enabled by default - * on platforms with API Level 21 when an {@code SSLContext} is initialized with a - * {@code PskKeyManager}. A workaround is to disable {@code ECDHE_PSK} cipher suites on platforms - * with API Level 21. - * - * <h3>Example</h3> - * The following example illustrates how to create an {@code SSLContext} which enables the use of - * TLS-PSK in {@code SSLSocket}, {@code SSLServerSocket} and {@code SSLEngine} instances obtained - * from it. - * <pre> {@code - * PskKeyManager pskKeyManager = ...; - * - * SSLContext sslContext = SSLContext.getInstance("TLS"); - * sslContext.init( - * new KeyManager[] { pskKeyManager }, - * new TrustManager[0], // No TrustManagers needed for TLS-PSK - * null // Use the default source of entropy - * ); - * - * SSLSocket sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket(...); - * }</pre> - * - * @removed This class is removed because it does not work with TLS 1.3. - */ -public abstract class PskKeyManager implements PSKKeyManager { - // IMPLEMENTATION DETAILS: This class exists only because the default implemenetation of the - // TLS/SSL JSSE provider (currently Conscrypt) cannot depend on Android framework classes. - // As a result, this framework class simply extends the PSKKeyManager interface from Conscrypt - // without adding any new methods or fields. Moreover, for technical reasons (Conscrypt classes - // are "hidden") this class replaces the Javadoc of Conscrypt's PSKKeyManager. - - /** - * Maximum supported length (in bytes) for PSK identity hint (in modified UTF-8 representation). - */ - public static final int MAX_IDENTITY_HINT_LENGTH_BYTES = - PSKKeyManager.MAX_IDENTITY_HINT_LENGTH_BYTES; - - /** Maximum supported length (in bytes) for PSK identity (in modified UTF-8 representation). */ - public static final int MAX_IDENTITY_LENGTH_BYTES = PSKKeyManager.MAX_IDENTITY_LENGTH_BYTES; - - /** Maximum supported length (in bytes) for PSK. */ - public static final int MAX_KEY_LENGTH_BYTES = PSKKeyManager.MAX_KEY_LENGTH_BYTES; - - /** - * Gets the PSK identity hint to report to the client to help agree on the PSK for the provided - * socket. - * - * <p> - * The default implementation returns {@code null}. - * - * @return PSK identity hint to be provided to the client or {@code null} to provide no hint. - */ - @Override - public String chooseServerKeyIdentityHint(Socket socket) { - return null; - } - - /** - * Gets the PSK identity hint to report to the client to help agree on the PSK for the provided - * engine. - * - * <p> - * The default implementation returns {@code null}. - * - * @return PSK identity hint to be provided to the client or {@code null} to provide no hint. - */ - @Override - public String chooseServerKeyIdentityHint(SSLEngine engine) { - return null; - } - - /** - * Gets the PSK identity to report to the server to help agree on the PSK for the provided - * socket. - * - * <p> - * The default implementation returns an empty string. - * - * @param identityHint identity hint provided by the server or {@code null} if none provided. - * - * @return PSK identity to provide to the server. {@code null} is permitted but will be - * converted into an empty string. - */ - @Override - public String chooseClientKeyIdentity(String identityHint, Socket socket) { - return ""; - } - - /** - * Gets the PSK identity to report to the server to help agree on the PSK for the provided - * engine. - * - * <p> - * The default implementation returns an empty string. - * - * @param identityHint identity hint provided by the server or {@code null} if none provided. - * - * @return PSK identity to provide to the server. {@code null} is permitted but will be - * converted into an empty string. - */ - @Override - public String chooseClientKeyIdentity(String identityHint, SSLEngine engine) { - return ""; - } - - /** - * Gets the PSK to use for the provided socket. - * - * <p> - * The default implementation returns {@code null}. - * - * @param identityHint identity hint provided by the server to help select the key or - * {@code null} if none provided. - * @param identity identity provided by the client to help select the key. - * - * @return key or {@code null} to signal to peer that no suitable key is available and to abort - * the handshake. - */ - @Override - public SecretKey getKey(String identityHint, String identity, Socket socket) { - return null; - } - - /** - * Gets the PSK to use for the provided engine. - * - * <p> - * The default implementation returns {@code null}. - * - * @param identityHint identity hint provided by the server to help select the key or - * {@code null} if none provided. - * @param identity identity provided by the client to help select the key. - * - * @return key or {@code null} to signal to peer that no suitable key is available and to abort - * the handshake. - */ - @Override - public SecretKey getKey(String identityHint, String identity, SSLEngine engine) { - return null; - } -} diff --git a/core/java/android/net/ScoredNetwork.java b/core/java/android/net/ScoredNetwork.java index 666da0a455fa..e38d227bb4e6 100644 --- a/core/java/android/net/ScoredNetwork.java +++ b/core/java/android/net/ScoredNetwork.java @@ -16,18 +16,14 @@ package android.net; -import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.SystemApi; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import java.lang.Math; -import java.lang.UnsupportedOperationException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.Objects; +import java.util.Set; /** * A network identifier along with a score for the quality of that network. @@ -195,7 +191,28 @@ public class ScoredNetwork implements Parcelable { return Objects.equals(networkKey, that.networkKey) && Objects.equals(rssiCurve, that.rssiCurve) && Objects.equals(meteredHint, that.meteredHint) - && Objects.equals(attributes, that.attributes); + && bundleEquals(attributes, that.attributes); + } + + private boolean bundleEquals(Bundle bundle1, Bundle bundle2) { + if (bundle1 == bundle2) { + return true; + } + if (bundle1 == null || bundle2 == null) { + return false; + } + if (bundle1.size() != bundle2.size()) { + return false; + } + Set<String> keys = bundle1.keySet(); + for (String key : keys) { + Object value1 = bundle1.get(key); + Object value2 = bundle2.get(key); + if (!Objects.equals(value1, value2)) { + return false; + } + } + return true; } @Override diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index c339856f4388..954e59c2c424 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -17,6 +17,7 @@ package android.net; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.DownloadManager; import android.app.backup.BackupManager; @@ -30,6 +31,8 @@ import com.android.server.NetworkManagementSocketTagger; import dalvik.system.SocketTagger; +import java.io.FileDescriptor; +import java.io.IOException; import java.net.DatagramSocket; import java.net.Socket; import java.net.SocketException; @@ -264,14 +267,25 @@ public class TrafficStats { } /** + * Set specific UID to use when accounting {@link Socket} traffic + * originating from the current thread as the calling UID. Designed for use + * when another application is performing operations on your behalf. + * <p> + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + */ + public static void setThreadStatsUidSelf() { + setThreadStatsUid(android.os.Process.myUid()); + } + + /** * Clear any active UID set to account {@link Socket} traffic originating * from the current thread. * * @see #setThreadStatsUid(int) - * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) + @SuppressLint("Doclava125") public static void clearThreadStatsUid() { NetworkManagementSocketTagger.setThreadSocketStatsUid(-1); } @@ -316,6 +330,27 @@ public class TrafficStats { } /** + * Tag the given {@link FileDescriptor} socket with any statistics + * parameters active for the current thread. Subsequent calls always replace + * any existing parameters. When finished, call + * {@link #untagFileDescriptor(FileDescriptor)} to remove statistics + * parameters. + * + * @see #setThreadStatsTag(int) + */ + public static void tagFileDescriptor(FileDescriptor fd) throws IOException { + SocketTagger.get().tag(fd); + } + + /** + * Remove any statistics parameters from the given {@link FileDescriptor} + * socket. + */ + public static void untagFileDescriptor(FileDescriptor fd) throws IOException { + SocketTagger.get().untag(fd); + } + + /** * Start profiling data usage for current UID. Only one profiling session * can be active at a time. * diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index f715f507f6c9..843bdb50dcab 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -18,7 +18,9 @@ package android.os; import android.annotation.SystemService; import android.content.Context; +import android.content.Intent; import android.hardware.health.V1_0.Constants; + import com.android.internal.app.IBatteryStats; /** @@ -33,39 +35,47 @@ 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}: + * Boolean field indicating whether the battery is currently considered to be + * low, that is whether a {@link Intent#ACTION_BATTERY_LOW} broadcast + * has been sent. + */ + public static final String EXTRA_BATTERY_LOW = "battery_low"; + /** * 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 +83,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 +226,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 +234,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 +242,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 +292,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/core/java/android/os/BatteryProperty.java b/core/java/android/os/BatteryProperty.java index 84119bdc126d..b7e7b17729f9 100644 --- a/core/java/android/os/BatteryProperty.java +++ b/core/java/android/os/BatteryProperty.java @@ -43,6 +43,13 @@ public class BatteryProperty implements Parcelable { return mValueLong; } + /** + * @hide + */ + public void setLong(long val) { + mValueLong = val; + } + /* * Parcel read/write code must be kept in sync with * frameworks/native/services/batteryservice/BatteryProperty.cpp diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 5785619dcb9f..2e9eeb16a887 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -19,6 +19,7 @@ package android.os; import android.app.job.JobParameters; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.service.batterystats.BatteryStatsServiceDumpProto; import android.telephony.SignalStrength; import android.text.format.DateFormat; import android.util.ArrayMap; @@ -29,11 +30,13 @@ import android.util.Printer; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; import android.view.Display; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; +import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -190,7 +193,7 @@ public abstract class BatteryStats implements Parcelable { public static final int STATS_SINCE_UNPLUGGED = 2; // NOTE: Update this list if you add/change any stats above. - // These characters are supposed to represent "total", "last", "current", + // These characters are supposed to represent "total", "last", "current", // and "unplugged". They were shortened for efficiency sake. private static final String[] STAT_NAMES = { "l", "c", "u" }; @@ -219,8 +222,11 @@ public abstract class BatteryStats implements Parcelable { * - Resource power manager (rpm) states [but screenOffRpm is disabled from working properly] * New in version 27: * - Always On Display (screen doze mode) time and power + * New in version 28: + * - Light/Deep Doze power + * - WiFi Multicast Wakelock statistics (count & duration) */ - static final String CHECKIN_VERSION = "27"; + static final int CHECKIN_VERSION = 28; /** * Old version, we hit 9 and ran out of room, need to remove. @@ -308,6 +314,8 @@ public abstract class BatteryStats implements Parcelable { private static final String CAMERA_DATA = "cam"; private static final String VIDEO_DATA = "vid"; private static final String AUDIO_DATA = "aud"; + private static final String WIFI_MULTICAST_TOTAL_DATA = "wmct"; + private static final String WIFI_MULTICAST_DATA = "wmc"; public static final String RESULT_RECEIVER_CONTROLLER_KEY = "controller_activity"; @@ -444,8 +452,7 @@ public abstract class BatteryStats implements Parcelable { /** * Returns the max duration if it is being tracked. - * Not all Timer subclasses track the max, total, current durations. - + * Not all Timer subclasses track the max, total, and current durations. */ public long getMaxDurationMsLocked(long elapsedRealtimeMs) { return -1; @@ -453,14 +460,14 @@ public abstract class BatteryStats implements Parcelable { /** * Returns the current time the timer has been active, if it is being tracked. - * Not all Timer subclasses track the max, total, current durations. + * Not all Timer subclasses track the max, total, and current durations. */ public long getCurrentDurationMsLocked(long elapsedRealtimeMs) { return -1; } /** - * Returns the current time the timer has been active, if it is being tracked. + * Returns the total time the timer has been active, if it is being tracked. * * Returns the total cumulative duration (i.e. sum of past durations) that this timer has * been on since reset. @@ -468,7 +475,7 @@ public abstract class BatteryStats implements Parcelable { * depending on the Timer, getTotalTimeLocked may represent the total 'blamed' or 'pooled' * time, rather than the actual time. By contrast, getTotalDurationMsLocked always gives * the actual total time. - * Not all Timer subclasses track the max, total, current durations. + * Not all Timer subclasses track the max, total, and current durations. */ public long getTotalDurationMsLocked(long elapsedRealtimeMs) { return -1; @@ -512,6 +519,13 @@ public abstract class BatteryStats implements Parcelable { public abstract ArrayMap<String, ? extends Wakelock> getWakelockStats(); /** + * Returns the WiFi Multicast Wakelock statistics. + * + * @return a Timer Object for the per uid Multicast statistics. + */ + public abstract Timer getMulticastWakelockStats(); + + /** * Returns a mapping containing sync statistics. * * @return a Map from Strings to Timer objects. @@ -597,9 +611,17 @@ public abstract class BatteryStats implements Parcelable { public abstract long getFullWifiLockTime(long elapsedRealtimeUs, int which); public abstract long getWifiScanTime(long elapsedRealtimeUs, int which); public abstract int getWifiScanCount(int which); + /** + * Returns the timer keeping track of wifi scans. + */ + public abstract Timer getWifiScanTimer(); public abstract int getWifiScanBackgroundCount(int which); public abstract long getWifiScanActualTime(long elapsedRealtimeUs); public abstract long getWifiScanBackgroundTime(long elapsedRealtimeUs); + /** + * Returns the timer keeping track of background wifi scans. + */ + public abstract Timer getWifiScanBackgroundTimer(); public abstract long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which); public abstract int getWifiBatchedScanCount(int csphBin, int which); public abstract long getWifiMulticastTime(long elapsedRealtimeUs, int which); @@ -1158,7 +1180,7 @@ public abstract class BatteryStats implements Parcelable { public static final class PackageChange { public String mPackageName; public boolean mUpdate; - public int mVersionCode; + public long mVersionCode; } public static final class DailyItem { @@ -1908,6 +1930,13 @@ public abstract class BatteryStats implements Parcelable { long elapsedRealtimeUs, int which); /** + * Returns the {@link Timer} object that tracks the given screen brightness. + * + * {@hide} + */ + public abstract Timer getScreenBrightnessTimer(int brightnessBin); + + /** * Returns the time in microseconds that power save mode has been enabled while the device was * running on battery. * @@ -1969,7 +1998,7 @@ public abstract class BatteryStats implements Parcelable { public abstract long getDeviceIdlingTime(int mode, long elapsedRealtimeUs, int which); /** - * Returns the number of times that the devie has started idling. + * Returns the number of times that the device has started idling. * * {@hide} */ @@ -2016,6 +2045,14 @@ public abstract class BatteryStats implements Parcelable { long elapsedRealtimeUs, int which); /** + * Returns the {@link Timer} object that tracks how much the phone has been trying to + * acquire a signal. + * + * {@hide} + */ + public abstract Timer getPhoneSignalScanningTimer(); + + /** * Returns the number of times the phone has entered the given signal strength. * * {@hide} @@ -2023,6 +2060,12 @@ public abstract class BatteryStats implements Parcelable { public abstract int getPhoneSignalStrengthCount(int strengthBin, int which); /** + * Return the {@link Timer} object used to track the given signal strength's duration and + * counts. + */ + protected abstract Timer getPhoneSignalStrengthTimer(int strengthBin); + + /** * Returns the time in microseconds that the mobile network has been active * (in a high power state). * @@ -2105,6 +2148,11 @@ public abstract class BatteryStats implements Parcelable { */ public abstract int getPhoneDataConnectionCount(int dataType, int which); + /** + * Returns the {@link Timer} object that tracks the phone's data connection type stats. + */ + public abstract Timer getPhoneDataConnectionTimer(int dataType); + public static final int WIFI_SUPPL_STATE_INVALID = 0; public static final int WIFI_SUPPL_STATE_DISCONNECTED = 1; public static final int WIFI_SUPPL_STATE_INTERFACE_DISABLED = 2; @@ -2264,6 +2312,13 @@ public abstract class BatteryStats implements Parcelable { public abstract int getWifiStateCount(int wifiState, int which); /** + * Returns the {@link Timer} object that tracks the given WiFi state. + * + * {@hide} + */ + public abstract Timer getWifiStateTimer(int wifiState); + + /** * Returns the time in microseconds that the wifi supplicant has been * in a given state. * @@ -2279,6 +2334,13 @@ public abstract class BatteryStats implements Parcelable { */ public abstract int getWifiSupplStateCount(int state, int which); + /** + * Returns the {@link Timer} object that tracks the given wifi supplicant state. + * + * {@hide} + */ + public abstract Timer getWifiSupplStateTimer(int state); + public static final int NUM_WIFI_SIGNAL_STRENGTH_BINS = 5; /** @@ -2298,6 +2360,13 @@ public abstract class BatteryStats implements Parcelable { public abstract int getWifiSignalStrengthCount(int strengthBin, int which); /** + * Returns the {@link Timer} object that tracks the given WIFI signal strength. + * + * {@hide} + */ + public abstract Timer getWifiSignalStrengthTimer(int strengthBin); + + /** * Returns the time in microseconds that the flashlight has been on while the device was * running on battery. * @@ -2484,13 +2553,13 @@ public abstract class BatteryStats implements Parcelable { public abstract int getDischargeAmountScreenOffSinceCharge(); /** - * Get the amount the battery has discharged while the screen was doze, + * Get the amount the battery has discharged while the screen was dozing, * since the last time power was unplugged. */ public abstract int getDischargeAmountScreenDoze(); /** - * Get the amount the battery has discharged while the screen was doze, + * Get the amount the battery has discharged while the screen was dozing, * since the last time the device was charged. */ public abstract int getDischargeAmountScreenDozeSinceCharge(); @@ -2623,20 +2692,32 @@ public abstract class BatteryStats implements Parcelable { * micro-Ampere-hours. This will be non-zero only if the device's battery has * a coulomb counter. */ - public abstract long getMahDischargeScreenOff(int which); + public abstract long getUahDischargeScreenOff(int which); /** * Return the amount of battery discharge while the screen was in doze mode, measured in * micro-Ampere-hours. This will be non-zero only if the device's battery has * a coulomb counter. */ - public abstract long getMahDischargeScreenDoze(int which); + public abstract long getUahDischargeScreenDoze(int which); /** * Return the amount of battery discharge measured in micro-Ampere-hours. This will be * non-zero only if the device's battery has a coulomb counter. */ - public abstract long getMahDischarge(int which); + public abstract long getUahDischarge(int which); + + /** + * @return the amount of battery discharge while the device is in light idle mode, measured in + * micro-Ampere-hours. + */ + public abstract long getUahDischargeLightDoze(int which); + + /** + * @return the amount of battery discharge while the device is in deep idle mode, measured in + * micro-Ampere-hours. + */ + public abstract long getUahDischargeDeepDoze(int which); /** * Returns the estimated real battery capacity, which may be less than the capacity @@ -2774,6 +2855,10 @@ public abstract class BatteryStats implements Parcelable { } } + private static long roundUsToMs(long timeUs) { + return (timeUs + 500) / 1000; + } + private static long computeWakeLock(Timer timer, long elapsedRealtimeUs, int which) { if (timer != null) { // Convert from microseconds to milliseconds with rounding @@ -2978,16 +3063,54 @@ public abstract class BatteryStats implements Parcelable { Timer timer, long rawRealtime, int which) { if (timer != null) { // Convert from microseconds to milliseconds with rounding - final long totalTime = (timer.getTotalTimeLocked(rawRealtime, which) + 500) - / 1000; + final long totalTime = roundUsToMs(timer.getTotalTimeLocked(rawRealtime, which)); final int count = timer.getCountLocked(which); - if (totalTime != 0) { + if (totalTime != 0 || count != 0) { dumpLine(pw, uid, category, type, totalTime, count); } } } /** + * Dump a given timer stat to the proto stream. + * + * @param proto the ProtoOutputStream to log to + * @param fieldId type of data, the field to save to (e.g. AggregatedBatteryStats.WAKELOCK) + * @param timer a {@link Timer} to dump stats for + * @param rawRealtimeUs the current elapsed realtime of the system in microseconds + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT + */ + private static void dumpTimer(ProtoOutputStream proto, long fieldId, + Timer timer, long rawRealtimeUs, int which) { + if (timer == null) { + return; + } + // Convert from microseconds to milliseconds with rounding + final long timeMs = roundUsToMs(timer.getTotalTimeLocked(rawRealtimeUs, which)); + final int count = timer.getCountLocked(which); + final long maxDurationMs = timer.getMaxDurationMsLocked(rawRealtimeUs / 1000); + final long curDurationMs = timer.getCurrentDurationMsLocked(rawRealtimeUs / 1000); + final long totalDurationMs = timer.getTotalDurationMsLocked(rawRealtimeUs / 1000); + if (timeMs != 0 || count != 0 || maxDurationMs != -1 || curDurationMs != -1 + || totalDurationMs != -1) { + final long token = proto.start(fieldId); + proto.write(TimerProto.DURATION_MS, timeMs); + proto.write(TimerProto.COUNT, count); + // These values will be -1 for timers that don't implement the functionality. + if (maxDurationMs != -1) { + proto.write(TimerProto.MAX_DURATION_MS, maxDurationMs); + } + if (curDurationMs != -1) { + proto.write(TimerProto.CURRENT_DURATION_MS, curDurationMs); + } + if (totalDurationMs != -1) { + proto.write(TimerProto.TOTAL_DURATION_MS, totalDurationMs); + } + proto.end(token); + } + } + + /** * Checks if the ControllerActivityCounter has any data worth dumping. */ private static boolean controllerActivityHasData(ControllerActivityCounter counter, int which) { @@ -3039,6 +3162,38 @@ public abstract class BatteryStats implements Parcelable { pw.println(); } + /** + * Dumps the ControllerActivityCounter if it has any data worth dumping. + */ + private static void dumpControllerActivityProto(ProtoOutputStream proto, long fieldId, + ControllerActivityCounter counter, + int which) { + if (!controllerActivityHasData(counter, which)) { + return; + } + + final long cToken = proto.start(fieldId); + + proto.write(ControllerActivityProto.IDLE_DURATION_MS, + counter.getIdleTimeCounter().getCountLocked(which)); + proto.write(ControllerActivityProto.RX_DURATION_MS, + counter.getRxTimeCounter().getCountLocked(which)); + proto.write(ControllerActivityProto.POWER_MAH, + counter.getPowerCounter().getCountLocked(which) / (1000 * 60 * 60)); + + long tToken; + LongCounter[] txCounters = counter.getTxTimeCounters(); + for (int i = 0; i < txCounters.length; ++i) { + LongCounter c = txCounters[i]; + tToken = proto.start(ControllerActivityProto.TX); + proto.write(ControllerActivityProto.TxLevel.LEVEL, i); + proto.write(ControllerActivityProto.TxLevel.DURATION_MS, c.getCountLocked(which)); + proto.end(tToken); + } + + proto.end(cToken); + } + private final void printControllerActivityIfInteresting(PrintWriter pw, StringBuilder sb, String prefix, String controllerName, ControllerActivityCounter counter, @@ -3164,13 +3319,13 @@ public abstract class BatteryStats implements Parcelable { /** * Checkin server version of dump to produce more compact, computer-readable log. * - * NOTE: all times are expressed in 'ms'. + * NOTE: all times are expressed in microseconds, unless specified otherwise. */ public final void dumpCheckinLocked(Context context, PrintWriter pw, int which, int reqUid, boolean wifiOnly) { final long rawUptime = SystemClock.uptimeMillis() * 1000; - final long rawRealtime = SystemClock.elapsedRealtime() * 1000; - final long rawRealtimeMs = (rawRealtime + 500) / 1000; + final long rawRealtimeMs = SystemClock.elapsedRealtime(); + final long rawRealtime = rawRealtimeMs * 1000; final long batteryUptime = getBatteryUptime(rawUptime); final long whichBatteryUptime = computeBatteryUptime(rawUptime, which); final long whichBatteryRealtime = computeBatteryRealtime(rawRealtime, which); @@ -3193,9 +3348,11 @@ public abstract class BatteryStats implements Parcelable { rawRealtime, which); final int connChanges = getNumConnectivityChange(which); final long phoneOnTime = getPhoneOnTime(rawRealtime, which); - final long dischargeCount = getMahDischarge(which); - final long dischargeScreenOffCount = getMahDischargeScreenOff(which); - final long dischargeScreenDozeCount = getMahDischargeScreenDoze(which); + final long dischargeCount = getUahDischarge(which); + final long dischargeScreenOffCount = getUahDischargeScreenOff(which); + final long dischargeScreenDozeCount = getUahDischargeScreenDoze(which); + final long dischargeLightDozeCount = getUahDischargeLightDoze(which); + final long dischargeDeepDozeCount = getUahDischargeDeepDoze(which); final StringBuilder sb = new StringBuilder(128); @@ -3216,13 +3373,16 @@ public abstract class BatteryStats implements Parcelable { screenDozeTime / 1000); - // Calculate wakelock times across all uids. + // Calculate both wakelock and wifi multicast wakelock times across all uids. long fullWakeLockTimeTotal = 0; long partialWakeLockTimeTotal = 0; + long multicastWakeLockTimeTotalMicros = 0; + int multicastWakeLockCountTotal = 0; for (int iu = 0; iu < NU; iu++) { final Uid u = uidStats.valueAt(iu); + // First calculating the wakelock stats final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelocks = u.getWakelockStats(); for (int iw=wakelocks.size()-1; iw>=0; iw--) { @@ -3240,6 +3400,13 @@ public abstract class BatteryStats implements Parcelable { rawRealtime, which); } } + + // Now calculating the wifi multicast wakelock stats + final Timer mcTimer = u.getMulticastWakelockStats(); + if (mcTimer != null) { + multicastWakeLockTimeTotalMicros += mcTimer.getTotalTimeLocked(rawRealtime, which); + multicastWakeLockCountTotal += mcTimer.getCountLocked(which); + } } // Dump network stats @@ -3355,6 +3522,11 @@ public abstract class BatteryStats implements Parcelable { } dumpLine(pw, 0 /* uid */, category, WIFI_SIGNAL_STRENGTH_COUNT_DATA, args); + // Dump Multicast total stats + dumpLine(pw, 0 /* uid */, category, WIFI_MULTICAST_TOTAL_DATA, + multicastWakeLockTimeTotalMicros / 1000, + multicastWakeLockCountTotal); + if (which == STATS_SINCE_UNPLUGGED) { dumpLine(pw, 0 /* uid */, category, BATTERY_LEVEL_DATA, getDischargeStartLevel(), getDischargeCurrentLevel()); @@ -3366,14 +3538,16 @@ public abstract class BatteryStats implements Parcelable { getDischargeStartLevel()-getDischargeCurrentLevel(), getDischargeAmountScreenOn(), getDischargeAmountScreenOff(), dischargeCount / 1000, dischargeScreenOffCount / 1000, - getDischargeAmountScreenDoze(), dischargeScreenDozeCount / 1000); + getDischargeAmountScreenDoze(), dischargeScreenDozeCount / 1000, + dischargeLightDozeCount / 1000, dischargeDeepDozeCount / 1000); } else { dumpLine(pw, 0 /* uid */, category, BATTERY_DISCHARGE_DATA, getLowDischargeAmountSinceCharge(), getHighDischargeAmountSinceCharge(), getDischargeAmountScreenOnSinceCharge(), getDischargeAmountScreenOffSinceCharge(), dischargeCount / 1000, dischargeScreenOffCount / 1000, - getDischargeAmountScreenDozeSinceCharge(), dischargeScreenDozeCount / 1000); + getDischargeAmountScreenDozeSinceCharge(), dischargeScreenDozeCount / 1000, + dischargeLightDozeCount / 1000, dischargeDeepDozeCount / 1000); } if (reqUid < 0) { @@ -3433,9 +3607,9 @@ public abstract class BatteryStats implements Parcelable { BatteryStatsHelper.makemAh(helper.getComputedPower()), BatteryStatsHelper.makemAh(helper.getMinDrainedPower()), BatteryStatsHelper.makemAh(helper.getMaxDrainedPower())); + int uid = 0; for (int i=0; i<sippers.size(); i++) { final BatterySipper bs = sippers.get(i); - int uid = 0; String label; switch (bs.drainType) { case IDLE: @@ -3476,6 +3650,9 @@ public abstract class BatteryStats implements Parcelable { case CAMERA: label = "camera"; break; + case MEMORY: + label = "memory"; + break; default: label = "???"; } @@ -3496,6 +3673,7 @@ public abstract class BatteryStats implements Parcelable { dumpLine(pw, 0 /* uid */, category, GLOBAL_CPU_FREQ_DATA, sb.toString()); } + // Dump stats per UID. for (int iu = 0; iu < NU; iu++) { final int uid = uidStats.keyAt(iu); if (reqUid >= 0 && uid != reqUid) { @@ -3659,7 +3837,7 @@ public abstract class BatteryStats implements Parcelable { linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), rawRealtime, "w", which, linePrefix); - // Only log if we had at lease one wakelock... + // Only log if we had at least one wakelock... if (sb.length() > 0) { String name = wakelocks.keyAt(iw); if (name.indexOf(',') >= 0) { @@ -3675,6 +3853,18 @@ public abstract class BatteryStats implements Parcelable { } } + // WiFi Multicast Wakelock Statistics + final Timer mcTimer = u.getMulticastWakelockStats(); + if (mcTimer != null) { + final long totalMcWakelockTimeMs = + mcTimer.getTotalTimeLocked(rawRealtime, which) / 1000 ; + final int countMcWakelock = mcTimer.getCountLocked(which); + if(totalMcWakelockTimeMs > 0) { + dumpLine(pw, uid, category, WIFI_MULTICAST_DATA, + totalMcWakelockTimeMs, countMcWakelock); + } + } + final ArrayMap<String, ? extends Timer> syncs = u.getSyncStats(); for (int isy=syncs.size()-1; isy>=0; isy--) { final Timer timer = syncs.valueAt(isy); @@ -3993,7 +4183,7 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } - final long dischargeCount = getMahDischarge(which); + final long dischargeCount = getUahDischarge(which); if (dischargeCount >= 0) { sb.setLength(0); sb.append(prefix); @@ -4003,7 +4193,7 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } - final long dischargeScreenOffCount = getMahDischargeScreenOff(which); + final long dischargeScreenOffCount = getUahDischargeScreenOff(which); if (dischargeScreenOffCount >= 0) { sb.setLength(0); sb.append(prefix); @@ -4013,7 +4203,7 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } - final long dischargeScreenDozeCount = getMahDischargeScreenDoze(which); + final long dischargeScreenDozeCount = getUahDischargeScreenDoze(which); if (dischargeScreenDozeCount >= 0) { sb.setLength(0); sb.append(prefix); @@ -4034,6 +4224,26 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } + final long dischargeLightDozeCount = getUahDischargeLightDoze(which); + if (dischargeLightDozeCount >= 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Device light doze discharge: "); + sb.append(BatteryStatsHelper.makemAh(dischargeLightDozeCount / 1000.0)); + sb.append(" mAh"); + pw.println(sb.toString()); + } + + final long dischargeDeepDozeCount = getUahDischargeDeepDoze(which); + if (dischargeDeepDozeCount >= 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Device deep doze discharge: "); + sb.append(BatteryStatsHelper.makemAh(dischargeDeepDozeCount / 1000.0)); + sb.append(" mAh"); + pw.println(sb.toString()); + } + pw.print(" Start clock time: "); pw.println(DateFormat.format("yyyy-MM-dd-HH-mm-ss", getStartClockTime()).toString()); @@ -4154,15 +4364,18 @@ public abstract class BatteryStats implements Parcelable { pw.print(" Connectivity changes: "); pw.println(connChanges); } - // Calculate wakelock times across all uids. + // Calculate both wakelock and wifi multicast wakelock times across all uids. long fullWakeLockTimeTotalMicros = 0; long partialWakeLockTimeTotalMicros = 0; + long multicastWakeLockTimeTotalMicros = 0; + int multicastWakeLockCountTotal = 0; final ArrayList<TimerEntry> timers = new ArrayList<>(); for (int iu = 0; iu < NU; iu++) { final Uid u = uidStats.valueAt(iu); + // First calculate wakelock statistics final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelocks = u.getWakelockStats(); for (int iw=wakelocks.size()-1; iw>=0; iw--) { @@ -4190,6 +4403,13 @@ public abstract class BatteryStats implements Parcelable { } } } + + // Next calculate wifi multicast wakelock statistics + final Timer mcTimer = u.getMulticastWakelockStats(); + if (mcTimer != null) { + multicastWakeLockTimeTotalMicros += mcTimer.getTotalTimeLocked(rawRealtime, which); + multicastWakeLockCountTotal += mcTimer.getCountLocked(which); + } } final long mobileRxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); @@ -4219,6 +4439,20 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } + if (multicastWakeLockTimeTotalMicros != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Total WiFi Multicast wakelock Count: "); + sb.append(multicastWakeLockCountTotal); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Total WiFi Multicast wakelock time: "); + formatTimeMsNoSpace(sb, (multicastWakeLockTimeTotalMicros + 500) / 1000); + pw.println(sb.toString()); + } + pw.println(""); pw.print(prefix); sb.setLength(0); @@ -5136,6 +5370,24 @@ public abstract class BatteryStats implements Parcelable { } } + // Calculate multicast wakelock stats + final Timer mcTimer = u.getMulticastWakelockStats(); + if (mcTimer != null) { + final long multicastWakeLockTimeMicros = mcTimer.getTotalTimeLocked(rawRealtime, which); + final int multicastWakeLockCount = mcTimer.getCountLocked(which); + + if (multicastWakeLockTimeMicros > 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" WiFi Multicast Wakelock"); + sb.append(" count = "); + sb.append(multicastWakeLockCount); + sb.append(" time = "); + formatTimeMsNoSpace(sb, (multicastWakeLockTimeMicros + 500) / 1000); + pw.println(sb.toString()); + } + } + final ArrayMap<String, ? extends Timer> syncs = u.getSyncStats(); for (int isy=syncs.size()-1; isy>=0; isy--) { final Timer timer = syncs.valueAt(isy); @@ -5522,6 +5774,7 @@ public abstract class BatteryStats implements Parcelable { } public void prepareForDumpLocked() { + // We don't need to require subclasses implement this. } public static class HistoryPrinter { @@ -6008,6 +6261,60 @@ public abstract class BatteryStats implements Parcelable { return true; } + private static void dumpDurationSteps(ProtoOutputStream proto, long fieldId, + LevelStepTracker steps) { + if (steps == null) { + return; + } + int count = steps.mNumStepDurations; + for (int i = 0; i < count; ++i) { + long token = proto.start(fieldId); + proto.write(SystemProto.BatteryLevelStep.DURATION_MS, steps.getDurationAt(i)); + proto.write(SystemProto.BatteryLevelStep.LEVEL, steps.getLevelAt(i)); + + final long initMode = steps.getInitModeAt(i); + final long modMode = steps.getModModeAt(i); + + int ds = SystemProto.BatteryLevelStep.DS_MIXED; + if ((modMode & STEP_LEVEL_MODE_SCREEN_STATE) == 0) { + switch ((int) (initMode & STEP_LEVEL_MODE_SCREEN_STATE) + 1) { + case Display.STATE_OFF: + ds = SystemProto.BatteryLevelStep.DS_OFF; + break; + case Display.STATE_ON: + ds = SystemProto.BatteryLevelStep.DS_ON; + break; + case Display.STATE_DOZE: + ds = SystemProto.BatteryLevelStep.DS_DOZE; + break; + case Display.STATE_DOZE_SUSPEND: + ds = SystemProto.BatteryLevelStep.DS_DOZE_SUSPEND; + break; + default: + ds = SystemProto.BatteryLevelStep.DS_ERROR; + break; + } + } + proto.write(SystemProto.BatteryLevelStep.DISPLAY_STATE, ds); + + int psm = SystemProto.BatteryLevelStep.PSM_MIXED; + if ((modMode & STEP_LEVEL_MODE_POWER_SAVE) == 0) { + psm = (initMode & STEP_LEVEL_MODE_POWER_SAVE) != 0 + ? SystemProto.BatteryLevelStep.PSM_ON : SystemProto.BatteryLevelStep.PSM_OFF; + } + proto.write(SystemProto.BatteryLevelStep.POWER_SAVE_MODE, psm); + + int im = SystemProto.BatteryLevelStep.IM_MIXED; + if ((modMode & STEP_LEVEL_MODE_DEVICE_IDLE) == 0) { + im = (initMode & STEP_LEVEL_MODE_DEVICE_IDLE) != 0 + ? SystemProto.BatteryLevelStep.IM_ON : SystemProto.BatteryLevelStep.IM_OFF; + } + proto.write(SystemProto.BatteryLevelStep.IDLE_MODE, im); + + proto.end(token); + } + } + public static final int DUMP_CHARGED_ONLY = 1<<1; public static final int DUMP_DAILY_ONLY = 1<<2; public static final int DUMP_HISTORY_ONLY = 1<<3; @@ -6263,7 +6570,7 @@ public abstract class BatteryStats implements Parcelable { pw.println(); } } - if (!filtering || (flags&(DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) != 0) { + if (!filtering || (flags & DUMP_DAILY_ONLY) != 0) { pw.println("Daily stats:"); pw.print(" Current start time: "); pw.println(DateFormat.format("yyyy-MM-dd-HH-mm-ss", @@ -6343,6 +6650,7 @@ public abstract class BatteryStats implements Parcelable { } } + // This is called from BatteryStatsService. @SuppressWarnings("unused") public void dumpCheckinLocked(Context context, PrintWriter pw, List<ApplicationInfo> apps, int flags, long histStart) { @@ -6354,10 +6662,7 @@ public abstract class BatteryStats implements Parcelable { long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); - final boolean filtering = (flags & - (DUMP_HISTORY_ONLY|DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) != 0; - - if ((flags&DUMP_INCLUDE_HISTORY) != 0 || (flags&DUMP_HISTORY_ONLY) != 0) { + if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) { if (startIteratingHistoryLocked()) { try { for (int i=0; i<getHistoryStringPoolSize(); i++) { @@ -6381,7 +6686,7 @@ public abstract class BatteryStats implements Parcelable { } } - if (filtering && (flags&(DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) == 0) { + if ((flags & DUMP_HISTORY_ONLY) != 0) { return; } @@ -6414,7 +6719,7 @@ public abstract class BatteryStats implements Parcelable { } } } - if (!filtering || (flags&DUMP_CHARGED_ONLY) != 0) { + if ((flags & DUMP_DAILY_ONLY) == 0) { dumpDurationSteps(pw, "", DISCHARGE_STEP_DATA, getDischargeLevelStepTracker(), true); String[] lineArgs = new String[1]; long timeRemaining = computeBatteryTimeRemaining(SystemClock.elapsedRealtime() * 1000); @@ -6434,4 +6739,850 @@ public abstract class BatteryStats implements Parcelable { (flags&DUMP_DEVICE_WIFI_ONLY) != 0); } } + + /** Dump #STATS_SINCE_CHARGED batterystats data to a proto. @hide */ + public void dumpProtoLocked(Context context, FileDescriptor fd, List<ApplicationInfo> apps, + int flags) { + final ProtoOutputStream proto = new ProtoOutputStream(fd); + final long bToken = proto.start(BatteryStatsServiceDumpProto.BATTERYSTATS); + prepareForDumpLocked(); + + proto.write(BatteryStatsProto.REPORT_VERSION, CHECKIN_VERSION); + proto.write(BatteryStatsProto.PARCEL_VERSION, getParcelVersion()); + proto.write(BatteryStatsProto.START_PLATFORM_VERSION, getStartPlatformVersion()); + proto.write(BatteryStatsProto.END_PLATFORM_VERSION, getEndPlatformVersion()); + + // History intentionally not included in proto dump. + + if ((flags & (DUMP_HISTORY_ONLY | DUMP_DAILY_ONLY)) == 0) { + final BatteryStatsHelper helper = new BatteryStatsHelper(context, false, + (flags & DUMP_DEVICE_WIFI_ONLY) != 0); + helper.create(this); + helper.refreshStats(STATS_SINCE_CHARGED, UserHandle.USER_ALL); + + dumpProtoAppsLocked(proto, helper, apps); + dumpProtoSystemLocked(proto, helper); + } + + proto.end(bToken); + proto.flush(); + } + + private void dumpProtoAppsLocked(ProtoOutputStream proto, BatteryStatsHelper helper, + List<ApplicationInfo> apps) { + final int which = STATS_SINCE_CHARGED; + final long rawUptimeUs = SystemClock.uptimeMillis() * 1000; + final long rawRealtimeMs = SystemClock.elapsedRealtime(); + final long rawRealtimeUs = rawRealtimeMs * 1000; + final long batteryUptimeUs = getBatteryUptime(rawUptimeUs); + + SparseArray<ArrayList<String>> aidToPackages = new SparseArray<>(); + if (apps != null) { + for (int i = 0; i < apps.size(); ++i) { + ApplicationInfo ai = apps.get(i); + int aid = UserHandle.getAppId(ai.uid); + ArrayList<String> pkgs = aidToPackages.get(aid); + if (pkgs == null) { + pkgs = new ArrayList<String>(); + aidToPackages.put(aid, pkgs); + } + pkgs.add(ai.packageName); + } + } + + SparseArray<BatterySipper> uidToSipper = new SparseArray<>(); + final List<BatterySipper> sippers = helper.getUsageList(); + if (sippers != null) { + for (int i = 0; i < sippers.size(); ++i) { + final BatterySipper bs = sippers.get(i); + if (bs.drainType != BatterySipper.DrainType.APP) { + // Others are handled by dumpProtoSystemLocked() + continue; + } + uidToSipper.put(bs.uidObj.getUid(), bs); + } + } + + SparseArray<? extends Uid> uidStats = getUidStats(); + final int n = uidStats.size(); + for (int iu = 0; iu < n; ++iu) { + final long uTkn = proto.start(BatteryStatsProto.UIDS); + final Uid u = uidStats.valueAt(iu); + + final int uid = uidStats.keyAt(iu); + proto.write(UidProto.UID, uid); + + // Print packages and apk stats (UID_DATA & APK_DATA) + ArrayList<String> pkgs = aidToPackages.get(UserHandle.getAppId(uid)); + if (pkgs == null) { + pkgs = new ArrayList<String>(); + } + final ArrayMap<String, ? extends BatteryStats.Uid.Pkg> packageStats = + u.getPackageStats(); + for (int ipkg = packageStats.size() - 1; ipkg >= 0; --ipkg) { + String pkg = packageStats.keyAt(ipkg); + final ArrayMap<String, ? extends Uid.Pkg.Serv> serviceStats = + packageStats.valueAt(ipkg).getServiceStats(); + if (serviceStats.size() == 0) { + // Due to the way ActivityManagerService logs wakeup alarms, some packages (for + // example, "android") may be included in the packageStats that aren't part of + // the UID. If they don't have any services, then they shouldn't be listed here. + // These packages won't be a part in the pkgs List. + continue; + } + + final long pToken = proto.start(UidProto.PACKAGES); + proto.write(UidProto.Package.NAME, pkg); + // Remove from the packages list since we're logging it here. + pkgs.remove(pkg); + + for (int isvc = serviceStats.size() - 1; isvc >= 0; --isvc) { + final BatteryStats.Uid.Pkg.Serv ss = serviceStats.valueAt(isvc); + long sToken = proto.start(UidProto.Package.SERVICES); + + proto.write(UidProto.Package.Service.NAME, serviceStats.keyAt(isvc)); + proto.write(UidProto.Package.Service.START_DURATION_MS, + roundUsToMs(ss.getStartTime(batteryUptimeUs, which))); + proto.write(UidProto.Package.Service.START_COUNT, ss.getStarts(which)); + proto.write(UidProto.Package.Service.LAUNCH_COUNT, ss.getLaunches(which)); + + proto.end(sToken); + } + proto.end(pToken); + } + // Print any remaining packages that weren't in the packageStats map. pkgs is pulled + // from PackageManager data. Packages are only included in packageStats if there was + // specific data tracked for them (services and wakeup alarms, etc.). + for (String p : pkgs) { + final long pToken = proto.start(UidProto.PACKAGES); + proto.write(UidProto.Package.NAME, p); + proto.end(pToken); + } + + // Total wakelock data (AGGREGATED_WAKELOCK_DATA) + if (u.getAggregatedPartialWakelockTimer() != null) { + final Timer timer = u.getAggregatedPartialWakelockTimer(); + // Times are since reset (regardless of 'which') + final long totTimeMs = timer.getTotalDurationMsLocked(rawRealtimeMs); + final Timer bgTimer = timer.getSubTimer(); + final long bgTimeMs = bgTimer != null + ? bgTimer.getTotalDurationMsLocked(rawRealtimeMs) : 0; + final long awToken = proto.start(UidProto.AGGREGATED_WAKELOCK); + proto.write(UidProto.AggregatedWakelock.PARTIAL_DURATION_MS, totTimeMs); + proto.write(UidProto.AggregatedWakelock.BACKGROUND_PARTIAL_DURATION_MS, bgTimeMs); + proto.end(awToken); + } + + // Audio (AUDIO_DATA) + dumpTimer(proto, UidProto.AUDIO, u.getAudioTurnedOnTimer(), rawRealtimeUs, which); + + // Bluetooth Controller (BLUETOOTH_CONTROLLER_DATA) + dumpControllerActivityProto(proto, UidProto.BLUETOOTH_CONTROLLER, + u.getBluetoothControllerActivity(), which); + + // BLE scans (BLUETOOTH_MISC_DATA) (uses totalDurationMsLocked and MaxDurationMsLocked) + final Timer bleTimer = u.getBluetoothScanTimer(); + if (bleTimer != null) { + final long bmToken = proto.start(UidProto.BLUETOOTH_MISC); + + dumpTimer(proto, UidProto.BluetoothMisc.APPORTIONED_BLE_SCAN, bleTimer, + rawRealtimeUs, which); + dumpTimer(proto, UidProto.BluetoothMisc.BACKGROUND_BLE_SCAN, + u.getBluetoothScanBackgroundTimer(), rawRealtimeUs, which); + // Unoptimized scan timer. Unpooled and since reset (regardless of 'which'). + dumpTimer(proto, UidProto.BluetoothMisc.UNOPTIMIZED_BLE_SCAN, + u.getBluetoothUnoptimizedScanTimer(), rawRealtimeUs, which); + // Unoptimized bg scan timer. Unpooled and since reset (regardless of 'which'). + dumpTimer(proto, UidProto.BluetoothMisc.BACKGROUND_UNOPTIMIZED_BLE_SCAN, + u.getBluetoothUnoptimizedScanBackgroundTimer(), rawRealtimeUs, which); + // Result counters + proto.write(UidProto.BluetoothMisc.BLE_SCAN_RESULT_COUNT, + u.getBluetoothScanResultCounter() != null + ? u.getBluetoothScanResultCounter().getCountLocked(which) : 0); + proto.write(UidProto.BluetoothMisc.BACKGROUND_BLE_SCAN_RESULT_COUNT, + u.getBluetoothScanResultBgCounter() != null + ? u.getBluetoothScanResultBgCounter().getCountLocked(which) : 0); + + proto.end(bmToken); + } + + // Camera (CAMERA_DATA) + dumpTimer(proto, UidProto.CAMERA, u.getCameraTurnedOnTimer(), rawRealtimeUs, which); + + // CPU stats (CPU_DATA & CPU_TIMES_AT_FREQ_DATA) + final long cpuToken = proto.start(UidProto.CPU); + proto.write(UidProto.Cpu.USER_DURATION_MS, roundUsToMs(u.getUserCpuTimeUs(which))); + proto.write(UidProto.Cpu.SYSTEM_DURATION_MS, roundUsToMs(u.getSystemCpuTimeUs(which))); + + final long[] cpuFreqs = getCpuFreqs(); + if (cpuFreqs != null) { + final long[] cpuFreqTimeMs = u.getCpuFreqTimes(which); + // If total cpuFreqTimes is null, then we don't need to check for + // screenOffCpuFreqTimes. + if (cpuFreqTimeMs != null && cpuFreqTimeMs.length == cpuFreqs.length) { + long[] screenOffCpuFreqTimeMs = u.getScreenOffCpuFreqTimes(which); + if (screenOffCpuFreqTimeMs == null) { + screenOffCpuFreqTimeMs = new long[cpuFreqTimeMs.length]; + } + for (int ic = 0; ic < cpuFreqTimeMs.length; ++ic) { + long cToken = proto.start(UidProto.Cpu.BY_FREQUENCY); + proto.write(UidProto.Cpu.ByFrequency.FREQUENCY_INDEX, ic + 1); + proto.write(UidProto.Cpu.ByFrequency.TOTAL_DURATION_MS, + cpuFreqTimeMs[ic]); + proto.write(UidProto.Cpu.ByFrequency.SCREEN_OFF_DURATION_MS, + screenOffCpuFreqTimeMs[ic]); + proto.end(cToken); + } + } + } + proto.end(cpuToken); + + // Flashlight (FLASHLIGHT_DATA) + dumpTimer(proto, UidProto.FLASHLIGHT, u.getFlashlightTurnedOnTimer(), + rawRealtimeUs, which); + + // Foreground activity (FOREGROUND_ACTIVITY_DATA) + dumpTimer(proto, UidProto.FOREGROUND_ACTIVITY, u.getForegroundActivityTimer(), + rawRealtimeUs, which); + + // Foreground service (FOREGROUND_SERVICE_DATA) + dumpTimer(proto, UidProto.FOREGROUND_SERVICE, u.getForegroundServiceTimer(), + rawRealtimeUs, which); + + // Job completion (JOB_COMPLETION_DATA) + final ArrayMap<String, SparseIntArray> completions = u.getJobCompletionStats(); + final int[] reasons = new int[]{ + JobParameters.REASON_CANCELED, + JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED, + JobParameters.REASON_PREEMPT, + JobParameters.REASON_TIMEOUT, + JobParameters.REASON_DEVICE_IDLE, + }; + for (int ic = 0; ic < completions.size(); ++ic) { + SparseIntArray types = completions.valueAt(ic); + if (types != null) { + final long jcToken = proto.start(UidProto.JOB_COMPLETION); + + proto.write(UidProto.JobCompletion.NAME, completions.keyAt(ic)); + + for (int r : reasons) { + long rToken = proto.start(UidProto.JobCompletion.REASON_COUNT); + proto.write(UidProto.JobCompletion.ReasonCount.NAME, r); + proto.write(UidProto.JobCompletion.ReasonCount.COUNT, types.get(r, 0)); + proto.end(rToken); + } + + proto.end(jcToken); + } + } + + // Scheduled jobs (JOB_DATA) + final ArrayMap<String, ? extends Timer> jobs = u.getJobStats(); + for (int ij = jobs.size() - 1; ij >= 0; --ij) { + final Timer timer = jobs.valueAt(ij); + final Timer bgTimer = timer.getSubTimer(); + final long jToken = proto.start(UidProto.JOBS); + + proto.write(UidProto.Job.NAME, jobs.keyAt(ij)); + // Background uses totalDurationMsLocked, while total uses totalTimeLocked + dumpTimer(proto, UidProto.Job.TOTAL, timer, rawRealtimeUs, which); + dumpTimer(proto, UidProto.Job.BACKGROUND, bgTimer, rawRealtimeUs, which); + + proto.end(jToken); + } + + // Modem Controller (MODEM_CONTROLLER_DATA) + dumpControllerActivityProto(proto, UidProto.MODEM_CONTROLLER, + u.getModemControllerActivity(), which); + + // Network stats (NETWORK_DATA) + final long nToken = proto.start(UidProto.NETWORK); + proto.write(UidProto.Network.MOBILE_BYTES_RX, + u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which)); + proto.write(UidProto.Network.MOBILE_BYTES_TX, + u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which)); + proto.write(UidProto.Network.WIFI_BYTES_RX, + u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which)); + proto.write(UidProto.Network.WIFI_BYTES_TX, + u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which)); + proto.write(UidProto.Network.BT_BYTES_RX, + u.getNetworkActivityBytes(NETWORK_BT_RX_DATA, which)); + proto.write(UidProto.Network.BT_BYTES_TX, + u.getNetworkActivityBytes(NETWORK_BT_TX_DATA, which)); + proto.write(UidProto.Network.MOBILE_PACKETS_RX, + u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which)); + proto.write(UidProto.Network.MOBILE_PACKETS_TX, + u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which)); + proto.write(UidProto.Network.WIFI_PACKETS_RX, + u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which)); + proto.write(UidProto.Network.WIFI_PACKETS_TX, + u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which)); + proto.write(UidProto.Network.MOBILE_ACTIVE_DURATION_MS, + roundUsToMs(u.getMobileRadioActiveTime(which))); + proto.write(UidProto.Network.MOBILE_ACTIVE_COUNT, + u.getMobileRadioActiveCount(which)); + proto.write(UidProto.Network.MOBILE_WAKEUP_COUNT, + u.getMobileRadioApWakeupCount(which)); + proto.write(UidProto.Network.WIFI_WAKEUP_COUNT, + u.getWifiRadioApWakeupCount(which)); + proto.write(UidProto.Network.MOBILE_BYTES_BG_RX, + u.getNetworkActivityBytes(NETWORK_MOBILE_BG_RX_DATA, which)); + proto.write(UidProto.Network.MOBILE_BYTES_BG_TX, + u.getNetworkActivityBytes(NETWORK_MOBILE_BG_TX_DATA, which)); + proto.write(UidProto.Network.WIFI_BYTES_BG_RX, + u.getNetworkActivityBytes(NETWORK_WIFI_BG_RX_DATA, which)); + proto.write(UidProto.Network.WIFI_BYTES_BG_TX, + u.getNetworkActivityBytes(NETWORK_WIFI_BG_TX_DATA, which)); + proto.write(UidProto.Network.MOBILE_PACKETS_BG_RX, + u.getNetworkActivityPackets(NETWORK_MOBILE_BG_RX_DATA, which)); + proto.write(UidProto.Network.MOBILE_PACKETS_BG_TX, + u.getNetworkActivityPackets(NETWORK_MOBILE_BG_TX_DATA, which)); + proto.write(UidProto.Network.WIFI_PACKETS_BG_RX, + u.getNetworkActivityPackets(NETWORK_WIFI_BG_RX_DATA, which)); + proto.write(UidProto.Network.WIFI_PACKETS_BG_TX, + u.getNetworkActivityPackets(NETWORK_WIFI_BG_TX_DATA, which)); + proto.end(nToken); + + // Power use item (POWER_USE_ITEM_DATA) + BatterySipper bs = uidToSipper.get(uid); + if (bs != null) { + final long bsToken = proto.start(UidProto.POWER_USE_ITEM); + proto.write(UidProto.PowerUseItem.COMPUTED_POWER_MAH, bs.totalPowerMah); + proto.write(UidProto.PowerUseItem.SHOULD_HIDE, bs.shouldHide); + proto.write(UidProto.PowerUseItem.SCREEN_POWER_MAH, bs.screenPowerMah); + proto.write(UidProto.PowerUseItem.PROPORTIONAL_SMEAR_MAH, + bs.proportionalSmearMah); + proto.end(bsToken); + } + + // Processes (PROCESS_DATA) + final ArrayMap<String, ? extends BatteryStats.Uid.Proc> processStats = + u.getProcessStats(); + for (int ipr = processStats.size() - 1; ipr >= 0; --ipr) { + final Uid.Proc ps = processStats.valueAt(ipr); + final long prToken = proto.start(UidProto.PROCESS); + + proto.write(UidProto.Process.NAME, processStats.keyAt(ipr)); + proto.write(UidProto.Process.USER_DURATION_MS, ps.getUserTime(which)); + proto.write(UidProto.Process.SYSTEM_DURATION_MS, ps.getSystemTime(which)); + proto.write(UidProto.Process.FOREGROUND_DURATION_MS, ps.getForegroundTime(which)); + proto.write(UidProto.Process.START_COUNT, ps.getStarts(which)); + proto.write(UidProto.Process.ANR_COUNT, ps.getNumAnrs(which)); + proto.write(UidProto.Process.CRASH_COUNT, ps.getNumCrashes(which)); + + proto.end(prToken); + } + + // Sensors (SENSOR_DATA) + final SparseArray<? extends BatteryStats.Uid.Sensor> sensors = u.getSensorStats(); + for (int ise = 0; ise < sensors.size(); ++ise) { + final Uid.Sensor se = sensors.valueAt(ise); + final Timer timer = se.getSensorTime(); + if (timer == null) { + continue; + } + final Timer bgTimer = se.getSensorBackgroundTime(); + final int sensorNumber = sensors.keyAt(ise); + final long seToken = proto.start(UidProto.SENSORS); + + proto.write(UidProto.Sensor.ID, sensorNumber); + // Background uses totalDurationMsLocked, while total uses totalTimeLocked + dumpTimer(proto, UidProto.Sensor.APPORTIONED, timer, rawRealtimeUs, which); + dumpTimer(proto, UidProto.Sensor.BACKGROUND, bgTimer, rawRealtimeUs, which); + + proto.end(seToken); + } + + // State times (STATE_TIME_DATA) + for (int ips = 0; ips < Uid.NUM_PROCESS_STATE; ++ips) { + long durMs = roundUsToMs(u.getProcessStateTime(ips, rawRealtimeUs, which)); + if (durMs == 0) { + continue; + } + final long stToken = proto.start(UidProto.STATES); + proto.write(UidProto.StateTime.STATE, ips); + proto.write(UidProto.StateTime.DURATION_MS, durMs); + proto.end(stToken); + } + + // Syncs (SYNC_DATA) + final ArrayMap<String, ? extends Timer> syncs = u.getSyncStats(); + for (int isy = syncs.size() - 1; isy >= 0; --isy) { + final Timer timer = syncs.valueAt(isy); + final Timer bgTimer = timer.getSubTimer(); + final long syToken = proto.start(UidProto.SYNCS); + + proto.write(UidProto.Sync.NAME, syncs.keyAt(isy)); + // Background uses totalDurationMsLocked, while total uses totalTimeLocked + dumpTimer(proto, UidProto.Sync.TOTAL, timer, rawRealtimeUs, which); + dumpTimer(proto, UidProto.Sync.BACKGROUND, bgTimer, rawRealtimeUs, which); + + proto.end(syToken); + } + + // User activity (USER_ACTIVITY_DATA) + if (u.hasUserActivity()) { + for (int i = 0; i < Uid.NUM_USER_ACTIVITY_TYPES; ++i) { + int val = u.getUserActivityCount(i, which); + if (val != 0) { + final long uaToken = proto.start(UidProto.USER_ACTIVITY); + proto.write(UidProto.UserActivity.NAME, i); + proto.write(UidProto.UserActivity.COUNT, val); + proto.end(uaToken); + } + } + } + + // Vibrator (VIBRATOR_DATA) + dumpTimer(proto, UidProto.VIBRATOR, u.getVibratorOnTimer(), rawRealtimeUs, which); + + // Video (VIDEO_DATA) + dumpTimer(proto, UidProto.VIDEO, u.getVideoTurnedOnTimer(), rawRealtimeUs, which); + + // Wakelocks (WAKELOCK_DATA) + final ArrayMap<String, ? extends Uid.Wakelock> wakelocks = u.getWakelockStats(); + for (int iw = wakelocks.size() - 1; iw >= 0; --iw) { + final Uid.Wakelock wl = wakelocks.valueAt(iw); + final long wToken = proto.start(UidProto.WAKELOCKS); + proto.write(UidProto.Wakelock.NAME, wakelocks.keyAt(iw)); + dumpTimer(proto, UidProto.Wakelock.FULL, wl.getWakeTime(WAKE_TYPE_FULL), + rawRealtimeUs, which); + final Timer pTimer = wl.getWakeTime(WAKE_TYPE_PARTIAL); + if (pTimer != null) { + dumpTimer(proto, UidProto.Wakelock.PARTIAL, pTimer, rawRealtimeUs, which); + dumpTimer(proto, UidProto.Wakelock.BACKGROUND_PARTIAL, pTimer.getSubTimer(), + rawRealtimeUs, which); + } + dumpTimer(proto, UidProto.Wakelock.WINDOW, wl.getWakeTime(WAKE_TYPE_WINDOW), + rawRealtimeUs, which); + proto.end(wToken); + } + + // Wifi Multicast Wakelock (WIFI_MULTICAST_WAKELOCK_DATA) + dumpTimer(proto, UidProto.WIFI_MULTICAST_WAKELOCK, u.getMulticastWakelockStats(), + rawRealtimeUs, which); + + // Wakeup alarms (WAKEUP_ALARM_DATA) + for (int ipkg = packageStats.size() - 1; ipkg >= 0; --ipkg) { + final Uid.Pkg ps = packageStats.valueAt(ipkg); + final ArrayMap<String, ? extends Counter> alarms = ps.getWakeupAlarmStats(); + for (int iwa = alarms.size() - 1; iwa >= 0; --iwa) { + final long waToken = proto.start(UidProto.WAKEUP_ALARM); + proto.write(UidProto.WakeupAlarm.NAME, alarms.keyAt(iwa)); + proto.write(UidProto.WakeupAlarm.COUNT, + alarms.valueAt(iwa).getCountLocked(which)); + proto.end(waToken); + } + } + + // Wifi Controller (WIFI_CONTROLLER_DATA) + dumpControllerActivityProto(proto, UidProto.WIFI_CONTROLLER, + u.getWifiControllerActivity(), which); + + // Wifi data (WIFI_DATA) + final long wToken = proto.start(UidProto.WIFI); + proto.write(UidProto.Wifi.FULL_WIFI_LOCK_DURATION_MS, + roundUsToMs(u.getFullWifiLockTime(rawRealtimeUs, which))); + dumpTimer(proto, UidProto.Wifi.APPORTIONED_SCAN, u.getWifiScanTimer(), + rawRealtimeUs, which); + proto.write(UidProto.Wifi.RUNNING_DURATION_MS, + roundUsToMs(u.getWifiRunningTime(rawRealtimeUs, which))); + dumpTimer(proto, UidProto.Wifi.BACKGROUND_SCAN, u.getWifiScanBackgroundTimer(), + rawRealtimeUs, which); + proto.end(wToken); + + proto.end(uTkn); + } + } + + private void dumpProtoSystemLocked(ProtoOutputStream proto, BatteryStatsHelper helper) { + final long sToken = proto.start(BatteryStatsProto.SYSTEM); + final long rawUptimeUs = SystemClock.uptimeMillis() * 1000; + final long rawRealtimeMs = SystemClock.elapsedRealtime(); + final long rawRealtimeUs = rawRealtimeMs * 1000; + final int which = STATS_SINCE_CHARGED; + + // Battery data (BATTERY_DATA) + final long bToken = proto.start(SystemProto.BATTERY); + proto.write(SystemProto.Battery.START_CLOCK_TIME_MS, getStartClockTime()); + proto.write(SystemProto.Battery.START_COUNT, getStartCount()); + proto.write(SystemProto.Battery.TOTAL_REALTIME_MS, + computeRealtime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Battery.TOTAL_UPTIME_MS, + computeUptime(rawUptimeUs, which) / 1000); + proto.write(SystemProto.Battery.BATTERY_REALTIME_MS, + computeBatteryRealtime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Battery.BATTERY_UPTIME_MS, + computeBatteryUptime(rawUptimeUs, which) / 1000); + proto.write(SystemProto.Battery.SCREEN_OFF_REALTIME_MS, + computeBatteryScreenOffRealtime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Battery.SCREEN_OFF_UPTIME_MS, + computeBatteryScreenOffUptime(rawUptimeUs, which) / 1000); + proto.write(SystemProto.Battery.SCREEN_DOZE_DURATION_MS, + getScreenDozeTime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Battery.ESTIMATED_BATTERY_CAPACITY_MAH, + getEstimatedBatteryCapacity()); + proto.write(SystemProto.Battery.MIN_LEARNED_BATTERY_CAPACITY_UAH, + getMinLearnedBatteryCapacity()); + proto.write(SystemProto.Battery.MAX_LEARNED_BATTERY_CAPACITY_UAH, + getMaxLearnedBatteryCapacity()); + proto.end(bToken); + + // Battery discharge (BATTERY_DISCHARGE_DATA) + final long bdToken = proto.start(SystemProto.BATTERY_DISCHARGE); + proto.write(SystemProto.BatteryDischarge.LOWER_BOUND_SINCE_CHARGE, + getLowDischargeAmountSinceCharge()); + proto.write(SystemProto.BatteryDischarge.UPPER_BOUND_SINCE_CHARGE, + getHighDischargeAmountSinceCharge()); + proto.write(SystemProto.BatteryDischarge.SCREEN_ON_SINCE_CHARGE, + getDischargeAmountScreenOnSinceCharge()); + proto.write(SystemProto.BatteryDischarge.SCREEN_OFF_SINCE_CHARGE, + getDischargeAmountScreenOffSinceCharge()); + proto.write(SystemProto.BatteryDischarge.SCREEN_DOZE_SINCE_CHARGE, + getDischargeAmountScreenDozeSinceCharge()); + proto.write(SystemProto.BatteryDischarge.TOTAL_MAH, + getUahDischarge(which) / 1000); + proto.write(SystemProto.BatteryDischarge.TOTAL_MAH_SCREEN_OFF, + getUahDischargeScreenOff(which) / 1000); + proto.write(SystemProto.BatteryDischarge.TOTAL_MAH_SCREEN_DOZE, + getUahDischargeScreenDoze(which) / 1000); + proto.write(SystemProto.BatteryDischarge.TOTAL_MAH_LIGHT_DOZE, + getUahDischargeLightDoze(which) / 1000); + proto.write(SystemProto.BatteryDischarge.TOTAL_MAH_DEEP_DOZE, + getUahDischargeDeepDoze(which) / 1000); + proto.end(bdToken); + + // Time remaining + long timeRemainingUs = computeChargeTimeRemaining(rawRealtimeUs); + // These are part of a oneof, so we should only set one of them. + if (timeRemainingUs >= 0) { + // Charge time remaining (CHARGE_TIME_REMAIN_DATA) + proto.write(SystemProto.CHARGE_TIME_REMAINING_MS, timeRemainingUs / 1000); + } else { + timeRemainingUs = computeBatteryTimeRemaining(rawRealtimeUs); + // Discharge time remaining (DISCHARGE_TIME_REMAIN_DATA) + if (timeRemainingUs >= 0) { + proto.write(SystemProto.DISCHARGE_TIME_REMAINING_MS, timeRemainingUs / 1000); + } else { + proto.write(SystemProto.DISCHARGE_TIME_REMAINING_MS, -1); + } + } + + // Charge step (CHARGE_STEP_DATA) + dumpDurationSteps(proto, SystemProto.CHARGE_STEP, getChargeLevelStepTracker()); + + // Phone data connection (DATA_CONNECTION_TIME_DATA and DATA_CONNECTION_COUNT_DATA) + for (int i = 0; i < NUM_DATA_CONNECTION_TYPES; ++i) { + final long pdcToken = proto.start(SystemProto.DATA_CONNECTION); + proto.write(SystemProto.DataConnection.NAME, i); + dumpTimer(proto, SystemProto.DataConnection.TOTAL, getPhoneDataConnectionTimer(i), + rawRealtimeUs, which); + proto.end(pdcToken); + } + + // Discharge step (DISCHARGE_STEP_DATA) + dumpDurationSteps(proto, SystemProto.DISCHARGE_STEP, getDischargeLevelStepTracker()); + + // CPU frequencies (GLOBAL_CPU_FREQ_DATA) + final long[] cpuFreqs = getCpuFreqs(); + if (cpuFreqs != null) { + for (long i : cpuFreqs) { + proto.write(SystemProto.CPU_FREQUENCY, i); + } + } + + // Bluetooth controller (GLOBAL_BLUETOOTH_CONTROLLER_DATA) + dumpControllerActivityProto(proto, SystemProto.GLOBAL_BLUETOOTH_CONTROLLER, + getBluetoothControllerActivity(), which); + + // Modem controller (GLOBAL_MODEM_CONTROLLER_DATA) + dumpControllerActivityProto(proto, SystemProto.GLOBAL_MODEM_CONTROLLER, + getModemControllerActivity(), which); + + // Global network data (GLOBAL_NETWORK_DATA) + final long gnToken = proto.start(SystemProto.GLOBAL_NETWORK); + proto.write(SystemProto.GlobalNetwork.MOBILE_BYTES_RX, + getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.MOBILE_BYTES_TX, + getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.MOBILE_PACKETS_RX, + getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.MOBILE_PACKETS_TX, + getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.WIFI_BYTES_RX, + getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.WIFI_BYTES_TX, + getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.WIFI_PACKETS_RX, + getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.WIFI_PACKETS_TX, + getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.BT_BYTES_RX, + getNetworkActivityBytes(NETWORK_BT_RX_DATA, which)); + proto.write(SystemProto.GlobalNetwork.BT_BYTES_TX, + getNetworkActivityBytes(NETWORK_BT_TX_DATA, which)); + proto.end(gnToken); + + // Wifi controller (GLOBAL_WIFI_CONTROLLER_DATA) + dumpControllerActivityProto(proto, SystemProto.GLOBAL_WIFI_CONTROLLER, + getWifiControllerActivity(), which); + + + // Global wifi (GLOBAL_WIFI_DATA) + final long gwToken = proto.start(SystemProto.GLOBAL_WIFI); + proto.write(SystemProto.GlobalWifi.ON_DURATION_MS, + getWifiOnTime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.GlobalWifi.RUNNING_DURATION_MS, + getGlobalWifiRunningTime(rawRealtimeUs, which) / 1000); + proto.end(gwToken); + + // Kernel wakelock (KERNEL_WAKELOCK_DATA) + final Map<String, ? extends Timer> kernelWakelocks = getKernelWakelockStats(); + for (Map.Entry<String, ? extends Timer> ent : kernelWakelocks.entrySet()) { + final long kwToken = proto.start(SystemProto.KERNEL_WAKELOCK); + proto.write(SystemProto.KernelWakelock.NAME, ent.getKey()); + dumpTimer(proto, SystemProto.KernelWakelock.TOTAL, ent.getValue(), + rawRealtimeUs, which); + proto.end(kwToken); + } + + // Misc (MISC_DATA) + // Calculate wakelock times across all uids. + long fullWakeLockTimeTotalUs = 0; + long partialWakeLockTimeTotalUs = 0; + + final SparseArray<? extends Uid> uidStats = getUidStats(); + for (int iu = 0; iu < uidStats.size(); iu++) { + final Uid u = uidStats.valueAt(iu); + + final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelocks = + u.getWakelockStats(); + for (int iw = wakelocks.size() - 1; iw >= 0; --iw) { + final Uid.Wakelock wl = wakelocks.valueAt(iw); + + final Timer fullWakeTimer = wl.getWakeTime(WAKE_TYPE_FULL); + if (fullWakeTimer != null) { + fullWakeLockTimeTotalUs += fullWakeTimer.getTotalTimeLocked(rawRealtimeUs, + which); + } + + final Timer partialWakeTimer = wl.getWakeTime(WAKE_TYPE_PARTIAL); + if (partialWakeTimer != null) { + partialWakeLockTimeTotalUs += partialWakeTimer.getTotalTimeLocked( + rawRealtimeUs, which); + } + } + } + final long mToken = proto.start(SystemProto.MISC); + proto.write(SystemProto.Misc.SCREEN_ON_DURATION_MS, + getScreenOnTime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.PHONE_ON_DURATION_MS, + getPhoneOnTime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.FULL_WAKELOCK_TOTAL_DURATION_MS, + fullWakeLockTimeTotalUs / 1000); + proto.write(SystemProto.Misc.PARTIAL_WAKELOCK_TOTAL_DURATION_MS, + partialWakeLockTimeTotalUs / 1000); + proto.write(SystemProto.Misc.MOBILE_RADIO_ACTIVE_DURATION_MS, + getMobileRadioActiveTime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.MOBILE_RADIO_ACTIVE_ADJUSTED_TIME_MS, + getMobileRadioActiveAdjustedTime(which) / 1000); + proto.write(SystemProto.Misc.MOBILE_RADIO_ACTIVE_COUNT, + getMobileRadioActiveCount(which)); + proto.write(SystemProto.Misc.MOBILE_RADIO_ACTIVE_UNKNOWN_DURATION_MS, + getMobileRadioActiveUnknownTime(which) / 1000); + proto.write(SystemProto.Misc.INTERACTIVE_DURATION_MS, + getInteractiveTime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.BATTERY_SAVER_MODE_ENABLED_DURATION_MS, + getPowerSaveModeEnabledTime(rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.NUM_CONNECTIVITY_CHANGES, + getNumConnectivityChange(which)); + proto.write(SystemProto.Misc.DEEP_DOZE_ENABLED_DURATION_MS, + getDeviceIdleModeTime(DEVICE_IDLE_MODE_DEEP, rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.DEEP_DOZE_COUNT, + getDeviceIdleModeCount(DEVICE_IDLE_MODE_DEEP, which)); + proto.write(SystemProto.Misc.DEEP_DOZE_IDLING_DURATION_MS, + getDeviceIdlingTime(DEVICE_IDLE_MODE_DEEP, rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.DEEP_DOZE_IDLING_COUNT, + getDeviceIdlingCount(DEVICE_IDLE_MODE_DEEP, which)); + proto.write(SystemProto.Misc.LONGEST_DEEP_DOZE_DURATION_MS, + getLongestDeviceIdleModeTime(DEVICE_IDLE_MODE_DEEP)); + proto.write(SystemProto.Misc.LIGHT_DOZE_ENABLED_DURATION_MS, + getDeviceIdleModeTime(DEVICE_IDLE_MODE_LIGHT, rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.LIGHT_DOZE_COUNT, + getDeviceIdleModeCount(DEVICE_IDLE_MODE_LIGHT, which)); + proto.write(SystemProto.Misc.LIGHT_DOZE_IDLING_DURATION_MS, + getDeviceIdlingTime(DEVICE_IDLE_MODE_LIGHT, rawRealtimeUs, which) / 1000); + proto.write(SystemProto.Misc.LIGHT_DOZE_IDLING_COUNT, + getDeviceIdlingCount(DEVICE_IDLE_MODE_LIGHT, which)); + proto.write(SystemProto.Misc.LONGEST_LIGHT_DOZE_DURATION_MS, + getLongestDeviceIdleModeTime(DEVICE_IDLE_MODE_LIGHT)); + proto.end(mToken); + + // Wifi multicast wakelock total stats (WIFI_MULTICAST_WAKELOCK_TOTAL_DATA) + // Calculate multicast wakelock stats across all uids. + long multicastWakeLockTimeTotalUs = 0; + int multicastWakeLockCountTotal = 0; + + for (int iu = 0; iu < uidStats.size(); iu++) { + final Uid u = uidStats.valueAt(iu); + + final Timer mcTimer = u.getMulticastWakelockStats(); + + if (mcTimer != null) { + multicastWakeLockTimeTotalUs += + mcTimer.getTotalTimeLocked(rawRealtimeUs, which); + multicastWakeLockCountTotal += mcTimer.getCountLocked(which); + } + } + + final long wmctToken = proto.start(SystemProto.WIFI_MULTICAST_WAKELOCK_TOTAL); + proto.write(SystemProto.WifiMulticastWakelockTotal.DURATION_MS, + multicastWakeLockTimeTotalUs / 1000); + proto.write(SystemProto.WifiMulticastWakelockTotal.COUNT, + multicastWakeLockCountTotal); + proto.end(wmctToken); + + // Power use item (POWER_USE_ITEM_DATA) + final List<BatterySipper> sippers = helper.getUsageList(); + if (sippers != null) { + for (int i = 0; i < sippers.size(); ++i) { + final BatterySipper bs = sippers.get(i); + int n = SystemProto.PowerUseItem.UNKNOWN_SIPPER; + int uid = 0; + switch (bs.drainType) { + case IDLE: + n = SystemProto.PowerUseItem.IDLE; + break; + case CELL: + n = SystemProto.PowerUseItem.CELL; + break; + case PHONE: + n = SystemProto.PowerUseItem.PHONE; + break; + case WIFI: + n = SystemProto.PowerUseItem.WIFI; + break; + case BLUETOOTH: + n = SystemProto.PowerUseItem.BLUETOOTH; + break; + case SCREEN: + n = SystemProto.PowerUseItem.SCREEN; + break; + case FLASHLIGHT: + n = SystemProto.PowerUseItem.FLASHLIGHT; + break; + case APP: + // dumpProtoAppsLocked will handle this. + continue; + case USER: + n = SystemProto.PowerUseItem.USER; + uid = UserHandle.getUid(bs.userId, 0); + break; + case UNACCOUNTED: + n = SystemProto.PowerUseItem.UNACCOUNTED; + break; + case OVERCOUNTED: + n = SystemProto.PowerUseItem.OVERCOUNTED; + break; + case CAMERA: + n = SystemProto.PowerUseItem.CAMERA; + break; + case MEMORY: + n = SystemProto.PowerUseItem.MEMORY; + break; + } + final long puiToken = proto.start(SystemProto.POWER_USE_ITEM); + proto.write(SystemProto.PowerUseItem.NAME, n); + proto.write(SystemProto.PowerUseItem.UID, uid); + proto.write(SystemProto.PowerUseItem.COMPUTED_POWER_MAH, bs.totalPowerMah); + proto.write(SystemProto.PowerUseItem.SHOULD_HIDE, bs.shouldHide); + proto.write(SystemProto.PowerUseItem.SCREEN_POWER_MAH, bs.screenPowerMah); + proto.write(SystemProto.PowerUseItem.PROPORTIONAL_SMEAR_MAH, + bs.proportionalSmearMah); + proto.end(puiToken); + } + } + + // Power use summary (POWER_USE_SUMMARY_DATA) + final long pusToken = proto.start(SystemProto.POWER_USE_SUMMARY); + proto.write(SystemProto.PowerUseSummary.BATTERY_CAPACITY_MAH, + helper.getPowerProfile().getBatteryCapacity()); + proto.write(SystemProto.PowerUseSummary.COMPUTED_POWER_MAH, helper.getComputedPower()); + proto.write(SystemProto.PowerUseSummary.MIN_DRAINED_POWER_MAH, helper.getMinDrainedPower()); + proto.write(SystemProto.PowerUseSummary.MAX_DRAINED_POWER_MAH, helper.getMaxDrainedPower()); + proto.end(pusToken); + + // RPM stats (RESOURCE_POWER_MANAGER_DATA) + final Map<String, ? extends Timer> rpmStats = getRpmStats(); + final Map<String, ? extends Timer> screenOffRpmStats = getScreenOffRpmStats(); + for (Map.Entry<String, ? extends Timer> ent : rpmStats.entrySet()) { + final long rpmToken = proto.start(SystemProto.RESOURCE_POWER_MANAGER); + proto.write(SystemProto.ResourcePowerManager.NAME, ent.getKey()); + dumpTimer(proto, SystemProto.ResourcePowerManager.TOTAL, + ent.getValue(), rawRealtimeUs, which); + dumpTimer(proto, SystemProto.ResourcePowerManager.SCREEN_OFF, + screenOffRpmStats.get(ent.getKey()), rawRealtimeUs, which); + proto.end(rpmToken); + } + + // Screen brightness (SCREEN_BRIGHTNESS_DATA) + for (int i = 0; i < NUM_SCREEN_BRIGHTNESS_BINS; ++i) { + final long sbToken = proto.start(SystemProto.SCREEN_BRIGHTNESS); + proto.write(SystemProto.ScreenBrightness.NAME, i); + dumpTimer(proto, SystemProto.ScreenBrightness.TOTAL, getScreenBrightnessTimer(i), + rawRealtimeUs, which); + proto.end(sbToken); + } + + // Signal scanning time (SIGNAL_SCANNING_TIME_DATA) + dumpTimer(proto, SystemProto.SIGNAL_SCANNING, getPhoneSignalScanningTimer(), rawRealtimeUs, + which); + + // Phone signal strength (SIGNAL_STRENGTH_TIME_DATA and SIGNAL_STRENGTH_COUNT_DATA) + for (int i = 0; i < SignalStrength.NUM_SIGNAL_STRENGTH_BINS; ++i) { + final long pssToken = proto.start(SystemProto.PHONE_SIGNAL_STRENGTH); + proto.write(SystemProto.PhoneSignalStrength.NAME, i); + dumpTimer(proto, SystemProto.PhoneSignalStrength.TOTAL, getPhoneSignalStrengthTimer(i), + rawRealtimeUs, which); + proto.end(pssToken); + } + + // Wakeup reasons (WAKEUP_REASON_DATA) + final Map<String, ? extends Timer> wakeupReasons = getWakeupReasonStats(); + for (Map.Entry<String, ? extends Timer> ent : wakeupReasons.entrySet()) { + final long wrToken = proto.start(SystemProto.WAKEUP_REASON); + proto.write(SystemProto.WakeupReason.NAME, ent.getKey()); + dumpTimer(proto, SystemProto.WakeupReason.TOTAL, ent.getValue(), rawRealtimeUs, which); + proto.end(wrToken); + } + + // Wifi signal strength (WIFI_SIGNAL_STRENGTH_TIME_DATA and WIFI_SIGNAL_STRENGTH_COUNT_DATA) + for (int i = 0; i < NUM_WIFI_SIGNAL_STRENGTH_BINS; ++i) { + final long wssToken = proto.start(SystemProto.WIFI_SIGNAL_STRENGTH); + proto.write(SystemProto.WifiSignalStrength.NAME, i); + dumpTimer(proto, SystemProto.WifiSignalStrength.TOTAL, getWifiSignalStrengthTimer(i), + rawRealtimeUs, which); + proto.end(wssToken); + } + + // Wifi state (WIFI_STATE_TIME_DATA and WIFI_STATE_COUNT_DATA) + for (int i = 0; i < NUM_WIFI_STATES; ++i) { + final long wsToken = proto.start(SystemProto.WIFI_STATE); + proto.write(SystemProto.WifiState.NAME, i); + dumpTimer(proto, SystemProto.WifiState.TOTAL, getWifiStateTimer(i), + rawRealtimeUs, which); + proto.end(wsToken); + } + + // Wifi supplicant state (WIFI_SUPPL_STATE_TIME_DATA and WIFI_SUPPL_STATE_COUNT_DATA) + for (int i = 0; i < NUM_WIFI_SUPPL_STATES; ++i) { + final long wssToken = proto.start(SystemProto.WIFI_SUPPLICANT_STATE); + proto.write(SystemProto.WifiSupplicantState.NAME, i); + dumpTimer(proto, SystemProto.WifiSupplicantState.TOTAL, getWifiSupplStateTimer(i), + rawRealtimeUs, which); + proto.end(wssToken); + } + + proto.end(sToken); + } } diff --git a/core/java/android/os/BatteryStatsInternal.java b/core/java/android/os/BatteryStatsInternal.java new file mode 100644 index 000000000000..b0436eb5f8af --- /dev/null +++ b/core/java/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/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index 1b707bdf73a9..b7a464544fc7 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -183,7 +183,7 @@ public class Binder implements IBinder { try { if (binder instanceof BinderProxy) { ((BinderProxy) binder).mWarnOnBlocking = false; - } else if (binder != null + } else if (binder != null && binder.getInterfaceDescriptor() != null && binder.queryLocalInterface(binder.getInterfaceDescriptor()) == null) { Log.w(TAG, "Unable to allow blocking on interface " + binder); } @@ -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. * @@ -285,7 +298,7 @@ public class Binder implements IBinder { long callingIdentity = clearCallingIdentity(); Throwable throwableToPropagate = null; try { - action.run(); + action.runOrThrow(); } catch (Throwable throwable) { throwableToPropagate = throwable; } finally { @@ -309,7 +322,7 @@ public class Binder implements IBinder { long callingIdentity = clearCallingIdentity(); Throwable throwableToPropagate = null; try { - return action.get(); + return action.getOrThrow(); } catch (Throwable throwable) { throwableToPropagate = throwable; return null; // overridden by throwing in finally block @@ -434,7 +447,7 @@ public class Binder implements IBinder { * descriptor. */ public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) { - if (mDescriptor.equals(descriptor)) { + if (mDescriptor != null && mDescriptor.equals(descriptor)) { return mOwner; } return null; diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index a352cdb55615..c1722d54c541 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -105,7 +105,7 @@ public class Build { /** * A hardware serial number, if available. Alphanumeric only, case-insensitive. - * For apps targeting SDK higher than {@link Build.VERSION_CODES#N_MR1} this + * For apps targeting SDK higher than {@link Build.VERSION_CODES#O_MR1} this * field is set to {@link Build#UNKNOWN}. * * @deprecated Use {@link #getSerial()} instead. @@ -865,6 +865,11 @@ public class Build { * </ul> */ public static final int O_MR1 = 27; + + /** + * P. + */ + public static final int P = CUR_DEVELOPMENT; // STOPSHIP Replace with the real version. } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/ConfigUpdate.java b/core/java/android/os/ConfigUpdate.java index 139687795430..94a44ec3729a 100644 --- a/core/java/android/os/ConfigUpdate.java +++ b/core/java/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/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 5b0e5bbce2f7..b1794a6dfa6c 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -24,6 +24,7 @@ import android.text.TextUtils; import android.util.Log; import java.io.File; +import java.util.LinkedList; /** * Provides access to environment variables. @@ -291,8 +292,9 @@ public class Environment { } /** {@hide} */ - public static File getReferenceProfile(String packageName) { - return buildPath(getDataDirectory(), "misc", "profiles", "ref", packageName); + public static File getProfileSnapshotPath(String packageName, String codePath) { + return buildPath(buildPath(getDataDirectory(), "misc", "profiles", "ref", packageName, + "primary.prof.snapshot")); } /** {@hide} */ @@ -608,6 +610,79 @@ public class Environment { return false; } + /** {@hide} */ public static final int HAS_MUSIC = 1 << 0; + /** {@hide} */ public static final int HAS_PODCASTS = 1 << 1; + /** {@hide} */ public static final int HAS_RINGTONES = 1 << 2; + /** {@hide} */ public static final int HAS_ALARMS = 1 << 3; + /** {@hide} */ public static final int HAS_NOTIFICATIONS = 1 << 4; + /** {@hide} */ public static final int HAS_PICTURES = 1 << 5; + /** {@hide} */ public static final int HAS_MOVIES = 1 << 6; + /** {@hide} */ public static final int HAS_DOWNLOADS = 1 << 7; + /** {@hide} */ public static final int HAS_DCIM = 1 << 8; + /** {@hide} */ public static final int HAS_DOCUMENTS = 1 << 9; + + /** {@hide} */ public static final int HAS_ANDROID = 1 << 16; + /** {@hide} */ public static final int HAS_OTHER = 1 << 17; + + /** + * Classify the content types present on the given external storage device. + * <p> + * This is typically useful for deciding if an inserted SD card is empty, or + * if it contains content like photos that should be preserved. + * + * @hide + */ + public static int classifyExternalStorageDirectory(File dir) { + int res = 0; + for (File f : FileUtils.listFilesOrEmpty(dir)) { + if (f.isFile() && isInterestingFile(f)) { + res |= HAS_OTHER; + } else if (f.isDirectory() && hasInterestingFiles(f)) { + final String name = f.getName(); + if (DIRECTORY_MUSIC.equals(name)) res |= HAS_MUSIC; + else if (DIRECTORY_PODCASTS.equals(name)) res |= HAS_PODCASTS; + else if (DIRECTORY_RINGTONES.equals(name)) res |= HAS_RINGTONES; + else if (DIRECTORY_ALARMS.equals(name)) res |= HAS_ALARMS; + else if (DIRECTORY_NOTIFICATIONS.equals(name)) res |= HAS_NOTIFICATIONS; + else if (DIRECTORY_PICTURES.equals(name)) res |= HAS_PICTURES; + else if (DIRECTORY_MOVIES.equals(name)) res |= HAS_MOVIES; + else if (DIRECTORY_DOWNLOADS.equals(name)) res |= HAS_DOWNLOADS; + else if (DIRECTORY_DCIM.equals(name)) res |= HAS_DCIM; + else if (DIRECTORY_DOCUMENTS.equals(name)) res |= HAS_DOCUMENTS; + else if (DIRECTORY_ANDROID.equals(name)) res |= HAS_ANDROID; + else res |= HAS_OTHER; + } + } + return res; + } + + private static boolean hasInterestingFiles(File dir) { + final LinkedList<File> explore = new LinkedList<>(); + explore.add(dir); + while (!explore.isEmpty()) { + dir = explore.pop(); + for (File f : FileUtils.listFilesOrEmpty(dir)) { + if (isInterestingFile(f)) return true; + if (f.isDirectory()) explore.add(f); + } + } + return false; + } + + private static boolean isInterestingFile(File file) { + if (file.isFile()) { + final String name = file.getName().toLowerCase(); + if (name.endsWith(".exe") || name.equals("autorun.inf") + || name.equals("launchpad.zip") || name.equals(".nomedia")) { + return false; + } else { + return true; + } + } else { + return false; + } + } + /** * Get a top-level shared/external storage directory for placing files of a * particular type. This is where the user will typically place and manage @@ -836,7 +911,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 +949,6 @@ public class Environment { * boolean) */ public static boolean isExternalStorageEmulated() { - if (isStorageDisabled()) return false; final File externalDir = sCurrentUser.getExternalDirs()[0]; return isExternalStorageEmulated(externalDir); } @@ -951,9 +1024,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/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index 56d6e0a62f94..7c53ec198e7d 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/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/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index 07c6055137a3..f2e0bddb93aa 100644 --- a/core/java/android/os/GraphicsEnvironment.java +++ b/core/java/android/os/GraphicsEnvironment.java @@ -22,6 +22,7 @@ import android.content.pm.PackageManager; import android.opengl.EGL14; import android.os.Build; import android.os.SystemProperties; +import android.provider.Settings; import android.util.Log; import dalvik.system.VMRuntime; @@ -29,18 +30,110 @@ import dalvik.system.VMRuntime; import java.io.File; /** @hide */ -public final class GraphicsEnvironment { +public class GraphicsEnvironment { + + private static final GraphicsEnvironment sInstance = new GraphicsEnvironment(); + + /** + * Returns the shared {@link GraphicsEnvironment} instance. + */ + public static GraphicsEnvironment getInstance() { + return sInstance; + } private static final boolean DEBUG = false; private static final String TAG = "GraphicsEnvironment"; private static final String PROPERTY_GFX_DRIVER = "ro.gfx.driver.0"; + private ClassLoader mClassLoader; + private String mLayerPath; + private String mDebugLayerPath; + + /** + * Set up GraphicsEnvironment + */ + public void setup(Context context) { + setupGpuLayers(context); + chooseDriver(context); + } + + /** + * Check whether application is debuggable + */ + private static boolean isDebuggable(Context context) { + return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) > 0; + } + + /** + * Store the layer paths available to the loader. + */ + public void setLayerPaths(ClassLoader classLoader, + String layerPath, + String debugLayerPath) { + // We have to store these in the class because they are set up before we + // have access to the Context to properly set up GraphicsEnvironment + mClassLoader = classLoader; + mLayerPath = layerPath; + mDebugLayerPath = debugLayerPath; + } + + /** + * Set up layer search paths for all apps + * If debuggable, check for additional debug settings + */ + private void setupGpuLayers(Context context) { + + String layerPaths = ""; + + // Only enable additional debug functionality if the following conditions are met: + // 1. App is debuggable + // 2. ENABLE_GPU_DEBUG_LAYERS is true + // 3. Package name is equal to GPU_DEBUG_APP + + if (isDebuggable(context)) { + + int enable = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.ENABLE_GPU_DEBUG_LAYERS, 0); + + if (enable != 0) { + + String gpuDebugApp = Settings.Global.getString(context.getContentResolver(), + Settings.Global.GPU_DEBUG_APP); + + String packageName = context.getPackageName(); + + if ((gpuDebugApp != null && packageName != null) + && (!gpuDebugApp.isEmpty() && !packageName.isEmpty()) + && gpuDebugApp.equals(packageName)) { + Log.i(TAG, "GPU debug layers enabled for " + packageName); + + // Prepend the debug layer path as a searchable path. + // This will ensure debug layers added will take precedence over + // the layers specified by the app. + layerPaths = mDebugLayerPath + ":"; + + String layers = Settings.Global.getString(context.getContentResolver(), + Settings.Global.GPU_DEBUG_LAYERS); + + Log.i(TAG, "Debug layer list: " + layers); + if (layers != null && !layers.isEmpty()) { + setDebugLayers(layers); + } + } + } + + } + + // Include the app's lib directory in all cases + layerPaths += mLayerPath; + + setLayerPaths(mClassLoader, layerPaths); + } + /** * Choose whether the current process should use the builtin or an updated driver. - * - * @hide */ - public static void chooseDriver(Context context) { + private static void chooseDriver(Context context) { String driverPackageName = SystemProperties.get(PROPERTY_GFX_DRIVER); if (driverPackageName == null || driverPackageName.isEmpty()) { return; @@ -99,8 +192,6 @@ public final class GraphicsEnvironment { * on a separate thread, it can usually be finished well before the UI is ready to be drawn. * * Should only be called after chooseDriver(). - * - * @hide */ public static void earlyInitEGL() { Thread eglInitThread = new Thread( @@ -124,6 +215,7 @@ public final class GraphicsEnvironment { return null; } + private static native void setLayerPaths(ClassLoader classLoader, String layerPaths); + private static native void setDebugLayers(String layers); private static native void setDriverPath(String path); - } diff --git a/core/java/android/os/IDeviceIdleController.aidl b/core/java/android/os/IDeviceIdleController.aidl index cc2af215c2c6..827170144dea 100644 --- a/core/java/android/os/IDeviceIdleController.aidl +++ b/core/java/android/os/IDeviceIdleController.aidl @@ -23,6 +23,11 @@ import android.os.UserHandle; interface IDeviceIdleController { void addPowerSaveWhitelistApp(String name); void removePowerSaveWhitelistApp(String name); + /* Removes an app from the system whitelist. Calling restoreSystemPowerWhitelistApp will add + the app back into the system whitelist */ + void removeSystemPowerWhitelistApp(String name); + void restoreSystemPowerWhitelistApp(String name); + String[] getRemovedSystemPowerWhitelistApps(); String[] getSystemPowerWhitelistExceptIdle(); String[] getSystemPowerWhitelist(); String[] getUserPowerWhitelist(); diff --git a/core/java/android/os/IServiceManager.java b/core/java/android/os/IServiceManager.java index 7b11c283d1e1..2176a785e0a2 100644 --- a/core/java/android/os/IServiceManager.java +++ b/core/java/android/os/IServiceManager.java @@ -18,12 +18,12 @@ package android.os; /** * Basic interface for finding and publishing system services. - * + * * An implementation of this interface is usually published as the * global context object, which can be retrieved via * BinderNative.getContextObject(). An easy way to retrieve this * is with the static method BnServiceManager.getDefault(). - * + * * @hide */ public interface IServiceManager extends IInterface @@ -33,33 +33,33 @@ public interface IServiceManager extends IInterface * service manager. Blocks for a few seconds waiting for it to be * published if it does not already exist. */ - public IBinder getService(String name) throws RemoteException; - + IBinder getService(String name) throws RemoteException; + /** * Retrieve an existing service called @a name from the * service manager. Non-blocking. */ - public IBinder checkService(String name) throws RemoteException; + IBinder checkService(String name) throws RemoteException; /** * Place a new @a service called @a name into the service * manager. */ - public void addService(String name, IBinder service, boolean allowIsolated) - throws RemoteException; + void addService(String name, IBinder service, boolean allowIsolated, int dumpFlags) + throws RemoteException; /** * Return a list of all currently running services. */ - public String[] listServices() throws RemoteException; + String[] listServices(int dumpFlags) throws RemoteException; /** * Assign a permission controller to the service manager. After set, this * interface is checked before any services are added. */ - public void setPermissionController(IPermissionController controller) + void setPermissionController(IPermissionController controller) throws RemoteException; - + static final String descriptor = "android.os.IServiceManager"; int GET_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; @@ -68,4 +68,17 @@ public interface IServiceManager extends IInterface int LIST_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3; int CHECK_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4; int SET_PERMISSION_CONTROLLER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+5; + + /* + * Must update values in IServiceManager.h + */ + /* Allows services to dump sections according to priorities. */ + int DUMP_FLAG_PRIORITY_CRITICAL = 1 << 0; + int DUMP_FLAG_PRIORITY_HIGH = 1 << 1; + int DUMP_FLAG_PRIORITY_NORMAL = 1 << 2; + int DUMP_FLAG_PRIORITY_ALL = DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_HIGH + | DUMP_FLAG_PRIORITY_NORMAL; + /* Allows services to dump sections in protobuf format. */ + int DUMP_FLAG_PROTO = 1 << 3; + } diff --git a/core/java/android/os/IStatsCompanionService.aidl b/core/java/android/os/IStatsCompanionService.aidl new file mode 100644 index 000000000000..1d2a40850454 --- /dev/null +++ b/core/java/android/os/IStatsCompanionService.aidl @@ -0,0 +1,62 @@ +/* + * 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 android.os.StatsLogEventWrapper; + +/** + * Binder interface to communicate with the Java-based statistics service helper. + * {@hide} + */ +interface IStatsCompanionService { + /** + * Tell statscompanion that stastd is up and running. + */ + oneway void statsdReady(); + + /** + * Register an alarm for anomaly detection to fire at the given timestamp (ms since epoch). + * If anomaly alarm had already been registered, it will be replaced with the new timestamp. + * Uses AlarmManager.set API, so if the timestamp is in the past, alarm fires immediately, and + * alarm is inexact. + */ + oneway void setAnomalyAlarm(long timestampMs); + + /** Cancel any anomaly detection alarm. */ + oneway void cancelAnomalyAlarm(); + + /** + * Register a repeating alarm for polling to fire at the given timestamp and every + * intervalMs thereafter (in ms since epoch). + * If polling alarm had already been registered, it will be replaced by new one. + * Uses AlarmManager.setRepeating API, so if the timestamp is in past, alarm fires immediately, + * and alarm is inexact. + */ + oneway void setPullingAlarms(long timestampMs, long intervalMs); + + /** Cancel any repeating polling alarm. */ + oneway void cancelPullingAlarms(); + + /** Pull the specified data. Results will be sent to statsd when complete. */ + StatsLogEventWrapper[] pullData(int pullCode); + + /** Send a broadcast to the specified pkg and class that it should getData now. */ + oneway void sendBroadcast(String pkg, String cls); + + /** Tells StatsCompaionService to grab the uid map snapshot and send it to statsd. */ + oneway void triggerUidSnapshot(); +} diff --git a/core/java/android/os/IStatsManager.aidl b/core/java/android/os/IStatsManager.aidl new file mode 100644 index 000000000000..3db12ed0815f --- /dev/null +++ b/core/java/android/os/IStatsManager.aidl @@ -0,0 +1,98 @@ +/** + * 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; + +/** + * Binder interface to communicate with the statistics management service. + * {@hide} + */ +interface IStatsManager { + /** + * Tell the stats daemon that the android system server is up and running. + */ + oneway void systemRunning(); + + /** + * Tell the stats daemon that the StatsCompanionService is up and running. + * Two-way binder call so that caller knows message received. + */ + void statsCompanionReady(); + + /** + * Tells statsd that an anomaly may have occurred, so statsd can check whether this is so and + * act accordingly. + * Two-way binder call so that caller's method (and corresponding wakelocks) will linger. + */ + void informAnomalyAlarmFired(); + + /** + * Tells statsd that it is time to poll some stats. Statsd will be responsible for determing + * what stats to poll and initiating the polling. + * Two-way binder call so that caller's method (and corresponding wakelocks) will linger. + */ + void informPollAlarmFired(); + + /** + * Tells statsd to store data to disk. + */ + void writeDataToDisk(); + + /** + * Inform statsd what the version and package are for each uid. Note that each array should + * have the same number of elements, and version[i] and package[i] correspond to uid[i]. + */ + oneway void informAllUidData(in int[] uid, in long[] version, in String[] app); + + /** + * Inform statsd what the uid and version are for one app that was updated. + */ + oneway void informOnePackage(in String app, in int uid, in long version); + + /** + * Inform stats that an app was removed. + */ + oneway void informOnePackageRemoved(in String app, in int uid); + + /** + * Fetches data for the specified configuration key. Returns a byte array representing proto + * wire-encoded of ConfigMetricsReportList. + */ + byte[] getData(in String key); + + /** + * Fetches metadata across statsd. Returns byte array representing wire-encoded proto. + */ + byte[] getMetadata(); + + /** + * Sets a configuration with the specified config key and subscribes to updates for this + * configuration key. Broadcasts will be sent if this configuration needs to be collected. + * The configuration must be a wire-encoded StatsDConfig. The caller specifies the name of the + * package and class that should receive these broadcasts. + * + * Returns if this configuration was correctly registered. + */ + boolean addConfiguration(in String configKey, in byte[] config, in String pkg, in String cls); + + /** + * Removes the configuration with the matching config key. No-op if this config key does not + * exist. + * + * Returns if this configuration key was removed. + */ + boolean removeConfiguration(in String configKey); +} diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 6746120f410d..9c90c3802caf 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -79,7 +79,7 @@ interface IUserManager { void setDefaultGuestRestrictions(in Bundle restrictions); Bundle getDefaultGuestRestrictions(); boolean markGuestForDeletion(int userHandle); - void setQuietModeEnabled(int userHandle, boolean enableQuietMode); + void setQuietModeEnabled(int userHandle, boolean enableQuietMode, in IntentSender target); boolean isQuietModeEnabled(int userHandle); boolean trySetQuietModeDisabled(int userHandle, in IntentSender target); void setSeedAccountData(int userHandle, in String accountName, @@ -98,4 +98,5 @@ interface IUserManager { boolean isUserUnlocked(int userId); boolean isUserRunning(int userId); boolean isUserNameSet(int userHandle); + boolean hasRestrictedProfiles(); } diff --git a/core/java/android/os/IncidentManager.java b/core/java/android/os/IncidentManager.java index bc8e2e470c55..1336c66b7133 100644 --- a/core/java/android/os/IncidentManager.java +++ b/core/java/android/os/IncidentManager.java @@ -21,8 +21,6 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.content.Context; -import android.os.IIncidentManager; -import android.os.ServiceManager; import android.provider.Settings; import android.util.Slog; @@ -37,7 +35,7 @@ import android.util.Slog; public class IncidentManager { private static final String TAG = "incident"; - private Context mContext; + private final Context mContext; /** * @hide @@ -54,18 +52,7 @@ public class IncidentManager { android.Manifest.permission.PACKAGE_USAGE_STATS }) public void reportIncident(IncidentReportArgs args) { - final IIncidentManager service = IIncidentManager.Stub.asInterface( - ServiceManager.getService("incident")); - if (service == null) { - Slog.e(TAG, "reportIncident can't find incident binder service"); - return; - } - - try { - service.reportIncident(args); - } catch (RemoteException ex) { - Slog.e(TAG, "reportIncident failed", ex); - } + reportIncidentInternal(args); } /** @@ -89,7 +76,7 @@ public class IncidentManager { }) public void reportIncident(String settingName, byte[] headerProto) { // Sections - String setting = Settings.System.getString(mContext.getContentResolver(), settingName); + String setting = Settings.Global.getString(mContext.getContentResolver(), settingName); IncidentReportArgs args; try { args = IncidentReportArgs.parseSetting(setting); @@ -98,23 +85,25 @@ public class IncidentManager { return; } if (args == null) { - Slog.i(TAG, "Incident report requested but disabled: " + settingName); + Slog.i(TAG, String.format("Incident report requested but disabled with " + + "settings [name: %s, value: %s]", settingName, setting)); return; } - // Header args.addHeader(headerProto); - // Look up the service + Slog.i(TAG, "Taking incident report: " + settingName); + reportIncidentInternal(args); + } + + private void reportIncidentInternal(IncidentReportArgs args) { final IIncidentManager service = IIncidentManager.Stub.asInterface( - ServiceManager.getService("incident")); + ServiceManager.getService(Context.INCIDENT_SERVICE)); if (service == null) { Slog.e(TAG, "reportIncident can't find incident binder service"); return; } - // Call the service - Slog.i(TAG, "Taking incident report: " + settingName); try { service.reportIncident(args); } catch (RemoteException ex) { diff --git a/core/java/android/os/IncidentReportArgs.java b/core/java/android/os/IncidentReportArgs.java index ce2ae1071d01..fd0ebcfea080 100644 --- a/core/java/android/os/IncidentReportArgs.java +++ b/core/java/android/os/IncidentReportArgs.java @@ -18,7 +18,6 @@ package android.os; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; import android.util.IntArray; @@ -36,6 +35,7 @@ public final class IncidentReportArgs implements Parcelable { private final IntArray mSections = new IntArray(); private final ArrayList<byte[]> mHeaders = new ArrayList<byte[]>(); private boolean mAll; + private int mDest; /** * Construct an incident report args with no fields. @@ -50,10 +50,12 @@ public final class IncidentReportArgs implements Parcelable { readFromParcel(in); } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(mAll ? 1 : 0); @@ -68,6 +70,8 @@ public final class IncidentReportArgs implements Parcelable { for (int i=0; i<N; i++) { out.writeByteArray(mHeaders.get(i)); } + + out.writeInt(mDest); } public void readFromParcel(Parcel in) { @@ -84,6 +88,8 @@ public final class IncidentReportArgs implements Parcelable { for (int i=0; i<N; i++) { mHeaders.add(in.createByteArray()); } + + mDest = in.readInt(); } public static final Parcelable.Creator<IncidentReportArgs> CREATOR @@ -100,6 +106,7 @@ public final class IncidentReportArgs implements Parcelable { /** * Print this report as a string. */ + @Override public String toString() { final StringBuilder sb = new StringBuilder("Incident("); if (mAll) { @@ -116,7 +123,8 @@ public final class IncidentReportArgs implements Parcelable { } sb.append(", "); sb.append(mHeaders.size()); - sb.append(" headers)"); + sb.append(" headers), "); + sb.append("Dest enum value: ").append(mDest); return sb.toString(); } @@ -131,10 +139,19 @@ public final class IncidentReportArgs implements Parcelable { } /** - * Add this section to the incident report. + * Set this incident report privacy policy spec. + * @hide + */ + public void setPrivacyPolicy(int dest) { + mDest = dest; + } + + /** + * Add this section to the incident report. Skip if the input is smaller than 2 since section + * id are only valid for positive integer as Protobuf field id. Here 1 is reserved for Header. */ public void addSection(int section) { - if (!mAll) { + if (!mAll && section > 1) { mSections.add(section); } } diff --git a/core/java/android/os/LocaleList.java b/core/java/android/os/LocaleList.java index 2dc3bebb2d10..ca9cbec99cde 100644 --- a/core/java/android/os/LocaleList.java +++ b/core/java/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/core/java/android/os/Message.java b/core/java/android/os/Message.java index d066db1fc4cc..b303e10fa64b 100644 --- a/core/java/android/os/Message.java +++ b/core/java/android/os/Message.java @@ -109,7 +109,9 @@ public final class Message implements Parcelable { // sometimes we store linked lists of these things /*package*/ Message next; - private static final Object sPoolSync = new Object(); + + /** @hide */ + public static final Object sPoolSync = new Object(); private static Message sPool; private static int sPoolSize = 0; @@ -370,6 +372,12 @@ public final class Message implements Parcelable { return callback; } + /** @hide */ + public Message setCallback(Runnable r) { + callback = r; + return this; + } + /** * Obtains a Bundle of arbitrary data associated with this * event, lazily creating it if necessary. Set this value by calling @@ -411,6 +419,16 @@ public final class Message implements Parcelable { } /** + * Chainable setter for {@link #what} + * + * @hide + */ + public Message setWhat(int what) { + this.what = what; + return this; + } + + /** * Sends this Message to the Handler specified by {@link #getTarget}. * Throws a null pointer exception if this field has not been set. */ diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index fae9d5310f8e..62bb38540e44 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -20,6 +20,7 @@ import android.annotation.Nullable; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.ExceptionUtils; import android.util.Log; import android.util.Size; import android.util.SizeF; @@ -191,6 +192,7 @@ import java.util.Set; * {@link #readSparseArray(ClassLoader)}. */ public final class Parcel { + private static final boolean DEBUG_RECYCLE = false; private static final boolean DEBUG_ARRAY_MAP = false; private static final String TAG = "Parcel"; @@ -209,6 +211,12 @@ public final class Parcel { private RuntimeException mStack; + /** + * Whether or not to parcel the stack trace of an exception. This has a performance + * impact, so should only be included in specific processes and only on debug builds. + */ + private static boolean sParcelExceptionStackTrace; + private static final int POOL_SIZE = 6; private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE]; private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE]; @@ -325,6 +333,11 @@ public final class Parcel { private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName); private static native void nativeEnforceInterface(long nativePtr, String interfaceName); + /** Last time exception with a stack trace was written */ + private static volatile long sLastWriteExceptionStackTrace; + /** Used for throttling of writing stack trace, which is costly */ + private static final int WRITE_EXCEPTION_STACK_TRACE_THRESHOLD_MS = 1000; + @CriticalNative private static native long nativeGetBlobAshmemSize(long nativePtr); @@ -561,6 +574,22 @@ public final class Parcel { mClassCookies = from.mClassCookies; } + /** @hide */ + public Map<Class, Object> copyClassCookies() { + return new ArrayMap<>(mClassCookies); + } + + /** @hide */ + public void putClassCookies(Map<Class, Object> cookies) { + if (cookies == null) { + return; + } + if (mClassCookies == null) { + mClassCookies = new ArrayMap<>(); + } + mClassCookies.putAll(cookies); + } + /** * Report whether the parcel contains any marshalled file descriptors. */ @@ -1340,6 +1369,13 @@ public final class Parcel { * @see Parcelable */ public final <T extends Parcelable> void writeTypedList(List<T> val) { + writeTypedList(val, 0); + } + + /** + * @hide + */ + public <T extends Parcelable> void writeTypedList(List<T> val, int parcelableFlags) { if (val == null) { writeInt(-1); return; @@ -1348,13 +1384,7 @@ public final class Parcel { int i=0; writeInt(N); while (i < N) { - T item = val.get(i); - if (item != null) { - writeInt(1); - item.writeToParcel(this, 0); - } else { - writeInt(0); - } + writeTypedObject(val.get(i), parcelableFlags); i++; } } @@ -1456,116 +1486,7 @@ public final class Parcel { int N = val.length; writeInt(N); for (int i = 0; i < N; i++) { - T item = val[i]; - if (item != null) { - writeInt(1); - item.writeToParcel(this, parcelableFlags); - } else { - writeInt(0); - } - } - } else { - writeInt(-1); - } - } - - /** - * Write a uniform (all items are null or the same class) array list of - * parcelables. - * - * @param list The list to write. - * - * @hide - */ - public final <T extends Parcelable> void writeTypedArrayList(@Nullable ArrayList<T> list, - int parcelableFlags) { - if (list != null) { - int N = list.size(); - writeInt(N); - boolean wroteCreator = false; - for (int i = 0; i < N; i++) { - T item = list.get(i); - if (item != null) { - writeInt(1); - if (!wroteCreator) { - writeParcelableCreator(item); - wroteCreator = true; - } - item.writeToParcel(this, parcelableFlags); - } else { - writeInt(0); - } - } - } else { - writeInt(-1); - } - } - - /** - * Reads a uniform (all items are null or the same class) array list of - * parcelables. - * - * @return The list or null. - * - * @hide - */ - public final @Nullable <T> ArrayList<T> readTypedArrayList(@Nullable ClassLoader loader) { - int N = readInt(); - if (N <= 0) { - return null; - } - Parcelable.Creator<?> creator = null; - ArrayList<T> result = new ArrayList<T>(N); - for (int i = 0; i < N; i++) { - if (readInt() != 0) { - if (creator == null) { - creator = readParcelableCreator(loader); - if (creator == null) { - return null; - } - } - final T parcelable; - if (creator instanceof Parcelable.ClassLoaderCreator<?>) { - Parcelable.ClassLoaderCreator<?> classLoaderCreator = - (Parcelable.ClassLoaderCreator<?>) creator; - parcelable = (T) classLoaderCreator.createFromParcel(this, loader); - } else { - parcelable = (T) creator.createFromParcel(this); - } - result.add(parcelable); - } else { - result.add(null); - } - } - return result; - } - - /** - * Write a uniform (all items are null or the same class) array set of - * parcelables. - * - * @param set The set to write. - * - * @hide - */ - public final <T extends Parcelable> void writeTypedArraySet(@Nullable ArraySet<T> set, - int parcelableFlags) { - if (set != null) { - int N = set.size(); - writeInt(N); - boolean wroteCreator = false; - for (int i = 0; i < N; i++) { - T item = set.valueAt(i); - if (item != null) { - writeInt(1); - if (!wroteCreator) { - writeParcelableCreator(item); - wroteCreator = true; - } - item.writeToParcel(this, parcelableFlags); - } else { - writeInt(0); - } + writeTypedObject(val[i], parcelableFlags); } } else { writeInt(-1); @@ -1573,43 +1494,6 @@ public final class Parcel { } /** - * Reads a uniform (all items are null or the same class) array set of - * parcelables. - * - * @return The set or null. - * - * @hide - */ - public final @Nullable <T> ArraySet<T> readTypedArraySet(@Nullable ClassLoader loader) { - int N = readInt(); - if (N <= 0) { - return null; - } - Parcelable.Creator<?> creator = null; - ArraySet<T> result = new ArraySet<T>(N); - for (int i = 0; i < N; i++) { - T parcelable = null; - if (readInt() != 0) { - if (creator == null) { - creator = readParcelableCreator(loader); - if (creator == null) { - return null; - } - } - if (creator instanceof Parcelable.ClassLoaderCreator<?>) { - Parcelable.ClassLoaderCreator<?> classLoaderCreator = - (Parcelable.ClassLoaderCreator<?>) creator; - parcelable = (T) classLoaderCreator.createFromParcel(this, loader); - } else { - parcelable = (T) creator.createFromParcel(this); - } - } - result.append(parcelable); - } - return result; - } - - /** * Flatten the Parcelable object into the parcel. * * @param val The Parcelable object to be written. @@ -1825,6 +1709,11 @@ public final class Parcel { } } + /** @hide For debugging purposes */ + public static void setStackTraceParceling(boolean enabled) { + sParcelExceptionStackTrace = enabled; + } + /** * Special function for writing an exception result at the header of * a parcel, to be used when returning an exception from a transaction. @@ -1882,6 +1771,27 @@ public final class Parcel { throw new RuntimeException(e); } writeString(e.getMessage()); + final long timeNow = sParcelExceptionStackTrace ? SystemClock.elapsedRealtime() : 0; + if (sParcelExceptionStackTrace && (timeNow - sLastWriteExceptionStackTrace + > WRITE_EXCEPTION_STACK_TRACE_THRESHOLD_MS)) { + sLastWriteExceptionStackTrace = timeNow; + final int sizePosition = dataPosition(); + writeInt(0); // Header size will be filled in later + StackTraceElement[] stackTrace = e.getStackTrace(); + final int truncatedSize = Math.min(stackTrace.length, 5); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < truncatedSize; i++) { + sb.append("\tat ").append(stackTrace[i]).append('\n'); + } + writeString(sb.toString()); + final int payloadPosition = dataPosition(); + setDataPosition(sizePosition); + // Write stack trace header size. Used in native side to skip the header + writeInt(payloadPosition - sizePosition); + setDataPosition(payloadPosition); + } else { + writeInt(0); + } switch (code) { case EX_SERVICE_SPECIFIC: writeInt(((ServiceSpecificException) e).errorCode); @@ -1947,7 +1857,26 @@ public final class Parcel { int code = readExceptionCode(); if (code != 0) { String msg = readString(); - readException(code, msg); + String remoteStackTrace = null; + final int remoteStackPayloadSize = readInt(); + if (remoteStackPayloadSize > 0) { + remoteStackTrace = readString(); + } + Exception e = createException(code, msg); + // Attach remote stack trace if availalble + if (remoteStackTrace != null) { + RemoteException cause = new RemoteException( + "Remote stack trace:\n" + remoteStackTrace, null, false, false); + try { + Throwable rootCause = ExceptionUtils.getRootCause(e); + if (rootCause != null) { + rootCause.initCause(cause); + } + } catch (RuntimeException ex) { + Log.e(TAG, "Cannot set cause " + cause + " for " + e, ex); + } + } + SneakyThrow.sneakyThrow(e); } } @@ -1992,32 +1921,41 @@ public final class Parcel { * @param msg The exception message. */ public final void readException(int code, String msg) { + SneakyThrow.sneakyThrow(createException(code, msg)); + } + + /** + * Creates an exception with the given message. + * + * @param code Used to determine which exception class to throw. + * @param msg The exception message. + */ + private Exception createException(int code, String msg) { switch (code) { case EX_PARCELABLE: if (readInt() > 0) { - SneakyThrow.sneakyThrow( - (Exception) readParcelable(Parcelable.class.getClassLoader())); + return (Exception) readParcelable(Parcelable.class.getClassLoader()); } else { - throw new RuntimeException(msg + " [missing Parcelable]"); + return new RuntimeException(msg + " [missing Parcelable]"); } case EX_SECURITY: - throw new SecurityException(msg); + return new SecurityException(msg); case EX_BAD_PARCELABLE: - throw new BadParcelableException(msg); + return new BadParcelableException(msg); case EX_ILLEGAL_ARGUMENT: - throw new IllegalArgumentException(msg); + return new IllegalArgumentException(msg); case EX_NULL_POINTER: - throw new NullPointerException(msg); + return new NullPointerException(msg); case EX_ILLEGAL_STATE: - throw new IllegalStateException(msg); + return new IllegalStateException(msg); case EX_NETWORK_MAIN_THREAD: - throw new NetworkOnMainThreadException(); + return new NetworkOnMainThreadException(); case EX_UNSUPPORTED_OPERATION: - throw new UnsupportedOperationException(msg); + return new UnsupportedOperationException(msg); case EX_SERVICE_SPECIFIC: - throw new ServiceSpecificException(readInt(), msg); + return new ServiceSpecificException(readInt(), msg); } - throw new RuntimeException("Unknown exception code: " + code + return new RuntimeException("Unknown exception code: " + code + " msg " + msg); } @@ -2149,8 +2087,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(). */ @@ -2458,11 +2394,7 @@ public final class Parcel { } ArrayList<T> l = new ArrayList<T>(N); while (N > 0) { - if (readInt() != 0) { - l.add(c.createFromParcel(this)); - } else { - l.add(null); - } + l.add(readTypedObject(c)); N--; } return l; @@ -2485,18 +2417,10 @@ public final class Parcel { int N = readInt(); int i = 0; for (; i < M && i < N; i++) { - if (readInt() != 0) { - list.set(i, c.createFromParcel(this)); - } else { - list.set(i, null); - } + list.set(i, readTypedObject(c)); } for (; i<N; i++) { - if (readInt() != 0) { - list.add(c.createFromParcel(this)); - } else { - list.add(null); - } + list.add(readTypedObject(c)); } for (; i<M; i++) { list.remove(N); @@ -2555,9 +2479,6 @@ public final class Parcel { * Read into the given List items String objects that were written with * {@link #writeStringList} at the current dataPosition(). * - * @return A newly created ArrayList containing strings with the same data - * as those that were previously written. - * * @see #writeStringList */ public final void readStringList(List<String> list) { @@ -2644,9 +2565,7 @@ public final class Parcel { } T[] l = c.newArray(N); for (int i=0; i<N; i++) { - if (readInt() != 0) { - l[i] = c.createFromParcel(this); - } + l[i] = readTypedObject(c); } return l; } @@ -2655,11 +2574,7 @@ public final class Parcel { int N = readInt(); if (N == val.length) { for (int i=0; i<N; i++) { - if (readInt() != 0) { - val[i] = c.createFromParcel(this); - } else { - val[i] = null; - } + val[i] = readTypedObject(c); } } else { throw new RuntimeException("bad array lengths"); diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 7f588adbd69d..7556f0921b4d 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/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/core/java/android/os/PatternMatcher.java b/core/java/android/os/PatternMatcher.java index 1f3a1e68c9de..76b214263f22 100644 --- a/core/java/android/os/PatternMatcher.java +++ b/core/java/android/os/PatternMatcher.java @@ -16,7 +16,7 @@ package android.os; -import android.util.Log; +import android.util.proto.ProtoOutputStream; import java.util.Arrays; @@ -131,7 +131,17 @@ public class PatternMatcher implements Parcelable { } return "PatternMatcher{" + type + mPattern + "}"; } - + + /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + long token = proto.start(fieldId); + proto.write(PatternMatcherProto.PATTERN, mPattern); + proto.write(PatternMatcherProto.TYPE, mType); + // PatternMatcherProto.PARSED_PATTERN is too much to dump, but the field is reserved to + // match the current data structure. + proto.end(token); + } + public int describeContents() { return 0; } @@ -141,7 +151,7 @@ public class PatternMatcher implements Parcelable { dest.writeInt(mType); dest.writeIntArray(mParsedPattern); } - + public PatternMatcher(Parcel src) { mPattern = src.readString(); mType = src.readInt(); diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 960c9f5cf22c..01fe5bfee6c7 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -387,6 +387,12 @@ public final class PowerManager { public static final int GO_TO_SLEEP_REASON_SLEEP_BUTTON = 6; /** + * Go to sleep reason code: Going to sleep by request of an accessibility service + * @hide + */ + public static final int GO_TO_SLEEP_REASON_ACCESSIBILITY = 7; + + /** * Go to sleep flag: Skip dozing state and directly go to full sleep. * @hide */ @@ -443,6 +449,20 @@ public final class PowerManager { public static final String SHUTDOWN_USER_REQUESTED = "userrequested"; /** + * The value to pass as the 'reason' argument to android_reboot() when battery temperature + * is too high. + * @hide + */ + public static final String SHUTDOWN_BATTERY_THERMAL_STATE = "thermal,battery"; + + /** + * The value to pass as the 'reason' argument to android_reboot() when device is running + * critically low on battery. + * @hide + */ + public static final String SHUTDOWN_LOW_BATTERY = "battery"; + + /** * @hide */ @Retention(RetentionPolicy.SOURCE) @@ -451,7 +471,9 @@ public final class PowerManager { SHUTDOWN_REASON_SHUTDOWN, SHUTDOWN_REASON_REBOOT, SHUTDOWN_REASON_USER_REQUESTED, - SHUTDOWN_REASON_THERMAL_SHUTDOWN + SHUTDOWN_REASON_THERMAL_SHUTDOWN, + SHUTDOWN_REASON_LOW_BATTERY, + SHUTDOWN_REASON_BATTERY_THERMAL }) public @interface ShutdownReason {} @@ -485,6 +507,64 @@ public final class PowerManager { */ public static final int SHUTDOWN_REASON_THERMAL_SHUTDOWN = 4; + /** + * constant for shutdown reason being low battery. + * @hide + */ + public static final int SHUTDOWN_REASON_LOW_BATTERY = 5; + + /** + * constant for shutdown reason being critical battery thermal state. + * @hide + */ + 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, + 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 to enable force-app-standby on all apps or not. + */ + int FORCE_ALL_APPS_STANDBY = 11; + + /** + * Whether to enable background check on all apps or not. + */ + int FORCE_BACKGROUND_CHECK = 12; + + /** + * Whether to disable non-essential sensors. (e.g. edge sensors.) + */ + int OPTIONAL_SENSORS = 13; + } + final Context mContext; final IPowerManager mService; final Handler mHandler; @@ -600,6 +680,26 @@ public final class PowerManager { * as the user moves between applications and doesn't require a special permission. * </p> * + * <p> + * Recommended naming conventions for tags to make debugging easier: + * <ul> + * <li>use a unique prefix delimited by a colon for your app/library (e.g. + * gmail:mytag) to make it easier to understand where the wake locks comes + * from. This namespace will also avoid collision for tags inside your app + * coming from different libraries which will make debugging easier. + * <li>use constants (e.g. do not include timestamps in the tag) to make it + * easier for tools to aggregate similar wake locks. When collecting + * debugging data, the platform only monitors a finite number of tags, + * using constants will help tools to provide better debugging data. + * <li>avoid using Class#getName() or similar method since this class name + * can be transformed by java optimizer and obfuscator tools. + * <li>avoid wrapping the tag or a prefix to avoid collision with wake lock + * tags from the platform (e.g. *alarm*). + * <li>never include personnally identifiable information for privacy + * reasons. + * </ul> + * </p> + * * @param levelAndFlags Combination of wake lock level and flag values defining * the requested behavior of the WakeLock. * @param tag Your class name (or other tag) for debugging purposes. @@ -1027,15 +1127,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) { @@ -1384,7 +1483,11 @@ public final class PowerManager { */ public void release(int flags) { synchronized (mToken) { - mInternalCount--; + if (mInternalCount > 0) { + // internal count must only be decreased if it is > 0 or state of + // the WakeLock object is broken. + mInternalCount--; + } if ((flags & RELEASE_FLAG_TIMEOUT) == 0) { mExternalCount--; } @@ -1426,6 +1529,13 @@ public final class PowerManager { * cost of that work can be accounted to the application. * </p> * + * <p> + * Make sure to follow the tag naming convention when using WorkSource + * to make it easier for app developers to understand wake locks + * attributed to them. See {@link PowerManager#newWakeLock(int, String)} + * documentation. + * </p> + * * @param ws The work source, or null if none. */ public void setWorkSource(WorkSource ws) { diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java index a01b8ed2c905..77ac26511769 100644 --- a/core/java/android/os/PowerManagerInternal.java +++ b/core/java/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/core/java/android/os/PowerSaveState.java b/core/java/android/os/PowerSaveState.java index 7058a1dca34d..de1128dfdef5 100644 --- a/core/java/android/os/PowerSaveState.java +++ b/core/java/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/core/java/android/os/RemoteException.java b/core/java/android/os/RemoteException.java index 6d25fc17bd6a..4e8b9716e852 100644 --- a/core/java/android/os/RemoteException.java +++ b/core/java/android/os/RemoteException.java @@ -30,6 +30,12 @@ public class RemoteException extends AndroidException { super(message); } + /** @hide */ + public RemoteException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + /** {@hide} */ public RuntimeException rethrowAsRuntimeException() { throw new RuntimeException(this); diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java index e11494d5cd41..42ec315c10e0 100644 --- a/core/java/android/os/ServiceManager.java +++ b/core/java/android/os/ServiceManager.java @@ -26,7 +26,6 @@ import java.util.Map; /** @hide */ public final class ServiceManager { private static final String TAG = "ServiceManager"; - private static IServiceManager sServiceManager; private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>(); @@ -43,7 +42,7 @@ public final class ServiceManager { /** * Returns a reference to a service with the given name. - * + * * @param name the name of the service to get * @return a reference to the service, or <code>null</code> if the service doesn't exist */ @@ -79,35 +78,46 @@ public final class ServiceManager { /** * Place a new @a service called @a name into the service * manager. - * + * * @param name the name of the new service * @param service the service object */ public static void addService(String name, IBinder service) { - try { - getIServiceManager().addService(name, service, false); - } catch (RemoteException e) { - Log.e(TAG, "error in addService", e); - } + addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL); } /** * Place a new @a service called @a name into the service * manager. - * + * * @param name the name of the new service * @param service the service object * @param allowIsolated set to true to allow isolated sandboxed processes * to access this service */ public static void addService(String name, IBinder service, boolean allowIsolated) { + addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL); + } + + /** + * Place a new @a service called @a name into the service + * manager. + * + * @param name the name of the new service + * @param service the service object + * @param allowIsolated set to true to allow isolated sandboxed processes + * @param dumpPriority supported dump priority levels as a bitmask + * to access this service + */ + public static void addService(String name, IBinder service, boolean allowIsolated, + int dumpPriority) { try { - getIServiceManager().addService(name, service, allowIsolated); + getIServiceManager().addService(name, service, allowIsolated, dumpPriority); } catch (RemoteException e) { Log.e(TAG, "error in addService", e); } } - + /** * Retrieve an existing service called @a name from the * service manager. Non-blocking. @@ -133,7 +143,7 @@ public final class ServiceManager { */ public static String[] listServices() { try { - return getIServiceManager().listServices(); + return getIServiceManager().listServices(IServiceManager.DUMP_FLAG_PRIORITY_ALL); } catch (RemoteException e) { Log.e(TAG, "error in listServices", e); return null; @@ -144,7 +154,7 @@ public final class ServiceManager { * This is only intended to be called when the process is first being brought * up and bound by the activity manager. There is only one thread in the process * at that time, so no locking is done. - * + * * @param cache the cache of service references * @hide */ diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java index be244264875e..589b8c492ab3 100644 --- a/core/java/android/os/ServiceManagerNative.java +++ b/core/java/android/os/ServiceManagerNative.java @@ -40,63 +40,65 @@ public abstract class ServiceManagerNative extends Binder implements IServiceMan if (in != null) { return in; } - + return new ServiceManagerProxy(obj); } - + public ServiceManagerNative() { attachInterface(this, descriptor); } - + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) { try { switch (code) { - case IServiceManager.GET_SERVICE_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - String name = data.readString(); - IBinder service = getService(name); - reply.writeStrongBinder(service); - return true; - } - - case IServiceManager.CHECK_SERVICE_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - String name = data.readString(); - IBinder service = checkService(name); - reply.writeStrongBinder(service); - return true; - } - - case IServiceManager.ADD_SERVICE_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - String name = data.readString(); - IBinder service = data.readStrongBinder(); - boolean allowIsolated = data.readInt() != 0; - addService(name, service, allowIsolated); - return true; - } - - case IServiceManager.LIST_SERVICES_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - String[] list = listServices(); - reply.writeStringArray(list); - return true; - } - - case IServiceManager.SET_PERMISSION_CONTROLLER_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - IPermissionController controller - = IPermissionController.Stub.asInterface( - data.readStrongBinder()); - setPermissionController(controller); - return true; - } + case IServiceManager.GET_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = getService(name); + reply.writeStrongBinder(service); + return true; + } + + case IServiceManager.CHECK_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = checkService(name); + reply.writeStrongBinder(service); + return true; + } + + case IServiceManager.ADD_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = data.readStrongBinder(); + boolean allowIsolated = data.readInt() != 0; + int dumpPriority = data.readInt(); + addService(name, service, allowIsolated, dumpPriority); + return true; + } + + case IServiceManager.LIST_SERVICES_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + int dumpPriority = data.readInt(); + String[] list = listServices(dumpPriority); + reply.writeStringArray(list); + return true; + } + + case IServiceManager.SET_PERMISSION_CONTROLLER_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + IPermissionController controller = + IPermissionController.Stub.asInterface( + data.readStrongBinder()); + setPermissionController(controller); + return true; + } } } catch (RemoteException e) { } - + return false; } @@ -110,11 +112,11 @@ class ServiceManagerProxy implements IServiceManager { public ServiceManagerProxy(IBinder remote) { mRemote = remote; } - + public IBinder asBinder() { return mRemote; } - + public IBinder getService(String name) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -139,7 +141,7 @@ class ServiceManagerProxy implements IServiceManager { return binder; } - public void addService(String name, IBinder service, boolean allowIsolated) + public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -147,12 +149,13 @@ class ServiceManagerProxy implements IServiceManager { data.writeString(name); data.writeStrongBinder(service); data.writeInt(allowIsolated ? 1 : 0); + data.writeInt(dumpPriority); mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0); reply.recycle(); data.recycle(); } - - public String[] listServices() throws RemoteException { + + public String[] listServices(int dumpPriority) throws RemoteException { ArrayList<String> services = new ArrayList<String>(); int n = 0; while (true) { @@ -160,6 +163,7 @@ class ServiceManagerProxy implements IServiceManager { Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IServiceManager.descriptor); data.writeInt(n); + data.writeInt(dumpPriority); n++; try { boolean res = mRemote.transact(LIST_SERVICES_TRANSACTION, data, reply, 0); diff --git a/core/java/android/os/ShellCallback.java b/core/java/android/os/ShellCallback.java index e7fe697f9c54..6a62424cc117 100644 --- a/core/java/android/os/ShellCallback.java +++ b/core/java/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; } @@ -100,6 +105,9 @@ public class ShellCallback implements Parcelable { ShellCallback(Parcel in) { mLocal = false; mShellCallback = IShellCallback.Stub.asInterface(in.readStrongBinder()); + if (mShellCallback != null) { + Binder.allowBlocking(mShellCallback.asBinder()); + } } public static final Parcelable.Creator<ShellCallback> CREATOR diff --git a/core/java/android/os/ShellCommand.java b/core/java/android/os/ShellCommand.java index e4a12e84e466..fa05a5e1b22e 100644 --- a/core/java/android/os/ShellCommand.java +++ b/core/java/android/os/ShellCommand.java @@ -17,6 +17,7 @@ package android.os; import android.util.Slog; + import com.android.internal.util.FastPrintWriter; import java.io.BufferedInputStream; @@ -90,7 +91,13 @@ public abstract class ShellCommand { mCmd = cmd; mResultReceiver = resultReceiver; - if (DEBUG) Slog.d(TAG, "Starting command " + mCmd + " on " + mTarget); + if (DEBUG) { + RuntimeException here = new RuntimeException("here"); + here.fillInStackTrace(); + Slog.d(TAG, "Starting command " + mCmd + " on " + mTarget, here); + Slog.d(TAG, "Calling uid=" + Binder.getCallingUid() + + " pid=" + Binder.getCallingPid() + " ShellCallback=" + getShellCallback()); + } int res = -1; try { res = onCommand(mCmd); @@ -118,13 +125,33 @@ public abstract class ShellCommand { mErrPrintWriter.flush(); } if (DEBUG) Slog.d(TAG, "Sending command result on " + mTarget); - mResultReceiver.send(res, null); + if (mResultReceiver != null) { + mResultReceiver.send(res, null); + } } if (DEBUG) Slog.d(TAG, "Finished command " + mCmd + " on " + mTarget); return res; } /** + * Adopt the ResultReceiver that was given to this shell command from it, taking + * it over. Primarily used to dispatch to another shell command. Once called, + * this shell command will no longer return its own result when done. + */ + public ResultReceiver adoptResultReceiver() { + ResultReceiver rr = mResultReceiver; + mResultReceiver = null; + return rr; + } + + /** + * Return the raw FileDescriptor for the output stream. + */ + public FileDescriptor getOutFileDescriptor() { + return mOut; + } + + /** * Return direct raw access (not buffered) to the command's output data stream. */ public OutputStream getRawOutputStream() { @@ -145,6 +172,13 @@ public abstract class ShellCommand { } /** + * Return the raw FileDescriptor for the error stream. + */ + public FileDescriptor getErrFileDescriptor() { + return mErr; + } + + /** * Return direct raw access (not buffered) to the command's error output data stream. */ public OutputStream getRawErrorStream() { @@ -168,6 +202,13 @@ public abstract class ShellCommand { } /** + * Return the raw FileDescriptor for the input stream. + */ + public FileDescriptor getInFileDescriptor() { + return mIn; + } + + /** * Return direct raw access (not buffered) to the command's input data stream. */ public InputStream getRawInputStream() { @@ -191,16 +232,20 @@ 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) { + if (DEBUG) Slog.d(TAG, "openFileForSystem: " + path + " mode=" + 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) { + if (DEBUG) Slog.d(TAG, "Got file: " + pfd); return pfd; } } catch (RuntimeException e) { + if (DEBUG) Slog.d(TAG, "Failure opening file: " + e.getMessage()); getErrPrintWriter().println("Failure opening file: " + e.getMessage()); } + if (DEBUG) Slog.d(TAG, "Error: Unable to open file: " + path); getErrPrintWriter().println("Error: Unable to open file: " + path); getErrPrintWriter().println("Consider using a file under /data/local/tmp/"); return null; diff --git a/core/java/android/os/StatsLogEventWrapper.aidl b/core/java/android/os/StatsLogEventWrapper.aidl new file mode 100644 index 000000000000..766343e38f3f --- /dev/null +++ b/core/java/android/os/StatsLogEventWrapper.aidl @@ -0,0 +1,20 @@ +/* + * 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; + +/** @hide */ +parcelable StatsLogEventWrapper cpp_header "android/os/StatsLogEventWrapper.h";
\ No newline at end of file diff --git a/core/java/android/os/StatsLogEventWrapper.java b/core/java/android/os/StatsLogEventWrapper.java new file mode 100644 index 000000000000..3ec744dabb81 --- /dev/null +++ b/core/java/android/os/StatsLogEventWrapper.java @@ -0,0 +1,148 @@ +/* + * 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 java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +/** + * Wrapper class for sending data from Android OS to StatsD. + * + * @hide + */ +public final class StatsLogEventWrapper implements Parcelable { + private ByteArrayOutputStream mStorage = new ByteArrayOutputStream(); + + // Below are constants copied from log/log.h + private static final int EVENT_TYPE_INT = 0; /* int32_t */ + private static final int EVENT_TYPE_LONG = 1; /* int64_t */ + private static final int EVENT_TYPE_STRING = 2; + private static final int EVENT_TYPE_LIST = 3; + private static final int EVENT_TYPE_FLOAT = 4; + + // Keep this in sync with system/core/logcat/event.logtags + private static final int STATS_BUFFER_TAG_ID = 1937006964; + /** + * Creates a log_event that is binary-encoded as implemented in + * system/core/liblog/log_event_list.c; this allows us to use the same parsing logic in statsd + * for pushed and pulled data. The write* methods must be called in the same order as their + * field number. There is no checking that the correct number of write* methods is called. + * We also write an END_LIST character before beginning to write to parcel, but this END_LIST + * may be unnecessary. + * + * @param tag The integer representing the tag for this event. + * @param fields The number of fields specified in this event. + */ + public StatsLogEventWrapper(int tag, int fields) { + // Write four bytes from tag, starting with least-significant bit. + // For pulled data, this tag number is not really used. We use the same tag number as + // pushed ones to be consistent. + write4Bytes(STATS_BUFFER_TAG_ID); + mStorage.write(EVENT_TYPE_LIST); // This is required to start the log entry. + mStorage.write(fields); // Indicate number of elements in this list. + mStorage.write(EVENT_TYPE_INT); + // The first element is the real atom tag number + write4Bytes(tag); + } + + /** + * Boilerplate for Parcel. + */ + public static final Parcelable.Creator<StatsLogEventWrapper> CREATOR = new + Parcelable.Creator<StatsLogEventWrapper>() { + public StatsLogEventWrapper createFromParcel(Parcel in) { + return new StatsLogEventWrapper(in); + } + + public StatsLogEventWrapper[] newArray(int size) { + return new StatsLogEventWrapper[size]; + } + }; + + private void write4Bytes(int val) { + mStorage.write(val); + mStorage.write(val >>> 8); + mStorage.write(val >>> 16); + mStorage.write(val >>> 24); + } + + private void write8Bytes(long val) { + write4Bytes((int) (val & 0xFFFFFFFF)); // keep the lowe 32-bits + write4Bytes((int) (val >>> 32)); // Write the high 32-bits. + } + + /** + * Adds 32-bit integer to output. + */ + public void writeInt(int val) { + mStorage.write(EVENT_TYPE_INT); + write4Bytes(val); + } + + /** + * Adds 64-bit long to output. + */ + public void writeLong(long val) { + mStorage.write(EVENT_TYPE_LONG); + write8Bytes(val); + } + + /** + * Adds a 4-byte floating point value to output. + */ + public void writeFloat(float val) { + int v = Float.floatToIntBits(val); + mStorage.write(EVENT_TYPE_FLOAT); + write4Bytes(v); + } + + /** + * Adds a string to the output. + */ + public void writeString(String val) { + mStorage.write(EVENT_TYPE_STRING); + write4Bytes(val.length()); + byte[] bytes = val.getBytes(StandardCharsets.UTF_8); + mStorage.write(bytes, 0, bytes.length); + } + + private StatsLogEventWrapper(Parcel in) { + readFromParcel(in); + } + + /** + * Writes the stored fields to a byte array. Will first write a new-line character to denote + * END_LIST before writing contents to byte array. + */ + public void writeToParcel(Parcel out, int flags) { + mStorage.write(10); // new-line character is same as END_LIST + out.writeByteArray(mStorage.toByteArray()); + } + + /** + * Not implemented. + */ + public void readFromParcel(Parcel in) { + // Not needed since this java class is for sending to statsd only. + } + + /** + * Boilerplate for Parcel. + */ + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 3b6df5df13aa..f90604abf293 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -16,17 +16,36 @@ package android.os; import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.app.ActivityManager; import android.app.ActivityThread; -import android.app.ApplicationErrorReport; import android.app.IActivityManager; 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; @@ -34,6 +53,8 @@ import android.util.Singleton; 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; @@ -47,37 +68,35 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.ArrayDeque; 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; /** - * <p>StrictMode is a developer tool which detects things you might be - * doing by accident and brings them to your attention so you can fix - * them. + * StrictMode is a developer tool which detects things you might be doing by accident and brings + * them to your attention so you can fix them. * - * <p>StrictMode is most commonly used to catch accidental disk or - * network access on the application's main thread, where UI - * operations are received and animations take place. Keeping disk - * and network operations off the main thread makes for much smoother, - * more responsive applications. By keeping your application's main thread - * responsive, you also prevent - * <a href="{@docRoot}guide/practices/design/responsiveness.html">ANR dialogs</a> - * from being shown to users. + * <p>StrictMode is most commonly used to catch accidental disk or network access on the + * application's main thread, where UI operations are received and animations take place. Keeping + * disk and network operations off the main thread makes for much smoother, more responsive + * applications. By keeping your application's main thread responsive, you also prevent <a + * href="{@docRoot}guide/practices/design/responsiveness.html">ANR dialogs</a> from being shown to + * users. * - * <p class="note">Note that even though an Android device's disk is - * often on flash memory, many devices run a filesystem on top of that - * memory with very limited concurrency. It's often the case that - * almost all disk accesses are fast, but may in individual cases be - * dramatically slower when certain I/O is happening in the background - * from other processes. If possible, it's best to assume that such - * things are not fast.</p> + * <p class="note">Note that even though an Android device's disk is often on flash memory, many + * devices run a filesystem on top of that memory with very limited concurrency. It's often the case + * that almost all disk accesses are fast, but may in individual cases be dramatically slower when + * certain I/O is happening in the background from other processes. If possible, it's best to assume + * that such things are not fast. * - * <p>Example code to enable from early in your - * {@link android.app.Application}, {@link android.app.Activity}, or - * other application component's - * {@link android.app.Application#onCreate} method: + * <p>Example code to enable from early in your {@link android.app.Application}, {@link + * android.app.Activity}, or other application component's {@link android.app.Application#onCreate} + * method: * * <pre> * public void onCreate() { @@ -99,36 +118,32 @@ import java.util.concurrent.atomic.AtomicInteger; * } * </pre> * - * <p>You can decide what should happen when a violation is detected. - * For example, using {@link ThreadPolicy.Builder#penaltyLog} you can - * watch the output of <code>adb logcat</code> while you use your - * application to see the violations as they happen. + * <p>You can decide what should happen when a violation is detected. For example, using {@link + * ThreadPolicy.Builder#penaltyLog} you can watch the output of <code>adb logcat</code> while you + * use your application to see the violations as they happen. * - * <p>If you find violations that you feel are problematic, there are - * a variety of tools to help solve them: threads, {@link android.os.Handler}, - * {@link android.os.AsyncTask}, {@link android.app.IntentService}, etc. - * But don't feel compelled to fix everything that StrictMode finds. In particular, - * many cases of disk access are often necessary during the normal activity lifecycle. Use - * StrictMode to find things you did by accident. Network requests on the UI thread + * <p>If you find violations that you feel are problematic, there are a variety of tools to help + * solve them: threads, {@link android.os.Handler}, {@link android.os.AsyncTask}, {@link + * android.app.IntentService}, etc. But don't feel compelled to fix everything that StrictMode + * finds. In particular, many cases of disk access are often necessary during the normal activity + * lifecycle. Use StrictMode to find things you did by accident. Network requests on the UI thread * are almost always a problem, though. * - * <p class="note">StrictMode is not a security mechanism and is not - * guaranteed to find all disk or network accesses. While it does - * propagate its state across process boundaries when doing - * {@link android.os.Binder} calls, it's still ultimately a best - * effort mechanism. Notably, disk or network access from JNI calls - * won't necessarily trigger it. Future versions of Android may catch - * more (or fewer) operations, so you should never leave StrictMode - * enabled in applications distributed on Google Play. + * <p class="note">StrictMode is not a security mechanism and is not guaranteed to find all disk or + * network accesses. While it does propagate its state across process boundaries when doing {@link + * android.os.Binder} calls, it's still ultimately a best effort mechanism. Notably, disk or network + * access from JNI calls won't necessarily trigger it. Future versions of Android may catch more (or + * fewer) operations, so you should never leave StrictMode enabled in applications distributed on + * Google Play. */ public final class StrictMode { private static final String TAG = "StrictMode"; private static final boolean LOG_V = Log.isLoggable(TAG, Log.VERBOSE); /** - * Boolean system property to disable strict mode checks outright. - * Set this to 'true' to force disable; 'false' has no effect on other - * enable/disable policy. + * Boolean system property to disable strict mode checks outright. Set this to 'true' to force + * disable; 'false' has no effect on other enable/disable policy. + * * @hide */ public static final String DISABLE_PROPERTY = "persist.sys.strictmode.disable"; @@ -141,12 +156,21 @@ public final class StrictMode { public static final String VISUAL_PROPERTY = "persist.sys.strictmode.visual"; /** - * Temporary property used to include {@link #DETECT_VM_CLEARTEXT_NETWORK} - * in {@link VmPolicy.Builder#detectAll()}. Apps can still always opt-into - * detection using {@link VmPolicy.Builder#detectCleartextNetwork()}. + * Temporary property used to include {@link #DETECT_VM_CLEARTEXT_NETWORK} in {@link + * VmPolicy.Builder#detectAll()}. Apps can still always opt-into detection using {@link + * VmPolicy.Builder#detectCleartextNetwork()}. */ 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; @@ -162,105 +186,97 @@ public final class StrictMode { // Byte 1: Thread-policy - /** - * @hide - */ - public static final int DETECT_DISK_WRITE = 0x01; // for ThreadPolicy + /** @hide */ + @TestApi public static final int DETECT_DISK_WRITE = 0x01; // for ThreadPolicy - /** - * @hide - */ - public static final int DETECT_DISK_READ = 0x02; // for ThreadPolicy + /** @hide */ + @TestApi public static final int DETECT_DISK_READ = 0x02; // for ThreadPolicy - /** - * @hide - */ - public static final int DETECT_NETWORK = 0x04; // for ThreadPolicy + /** @hide */ + @TestApi public static final int DETECT_NETWORK = 0x04; // for ThreadPolicy /** * For StrictMode.noteSlowCall() * * @hide */ - public static final int DETECT_CUSTOM = 0x08; // for ThreadPolicy + @TestApi public static final int DETECT_CUSTOM = 0x08; // for ThreadPolicy /** * For StrictMode.noteResourceMismatch() * * @hide */ - public static final int DETECT_RESOURCE_MISMATCH = 0x10; // for ThreadPolicy + @TestApi public static final int DETECT_RESOURCE_MISMATCH = 0x10; // for ThreadPolicy - /** - * @hide - */ - public static final int DETECT_UNBUFFERED_IO = 0x20; // for ThreadPolicy + /** @hide */ + @TestApi public static final int DETECT_UNBUFFERED_IO = 0x20; // for ThreadPolicy private static final int ALL_THREAD_DETECT_BITS = - DETECT_DISK_WRITE | DETECT_DISK_READ | DETECT_NETWORK | DETECT_CUSTOM | - DETECT_RESOURCE_MISMATCH | DETECT_UNBUFFERED_IO; + DETECT_DISK_WRITE + | DETECT_DISK_READ + | DETECT_NETWORK + | DETECT_CUSTOM + | DETECT_RESOURCE_MISMATCH + | DETECT_UNBUFFERED_IO; // Byte 2: Process-policy /** * Note, a "VM_" bit, not thread. + * * @hide */ - 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 */ - 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 */ - 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 - */ - private static final int DETECT_VM_INSTANCE_LEAKS = 0x08 << 8; // for VmPolicy + /** @hide */ + @TestApi public static final int DETECT_VM_INSTANCE_LEAKS = 0x08 << 8; // for VmPolicy - /** - * @hide - */ - public static final int DETECT_VM_REGISTRATION_LEAKS = 0x10 << 8; // for VmPolicy + /** @hide */ + @TestApi public static final int DETECT_VM_REGISTRATION_LEAKS = 0x10 << 8; // for VmPolicy - /** - * @hide - */ - private static final int DETECT_VM_FILE_URI_EXPOSURE = 0x20 << 8; // for VmPolicy + /** @hide */ + @TestApi public static final int DETECT_VM_FILE_URI_EXPOSURE = 0x20 << 8; // for VmPolicy - /** - * @hide - */ - private static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8; // for VmPolicy + /** @hide */ + @TestApi public static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8; // for VmPolicy - /** - * @hide - */ - private static final int DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION = 0x80 << 8; // for VmPolicy + /** @hide */ + @TestApi + public static final int DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION = 0x80 << 8; // for VmPolicy - /** - * @hide - */ - private static final int DETECT_VM_UNTAGGED_SOCKET = 0x80 << 24; // for VmPolicy + /** @hide */ + @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 | DETECT_VM_CLOSABLE_LEAKS | - DETECT_VM_ACTIVITY_LEAKS | DETECT_VM_INSTANCE_LEAKS | - DETECT_VM_REGISTRATION_LEAKS | DETECT_VM_FILE_URI_EXPOSURE | - DETECT_VM_CLEARTEXT_NETWORK | DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION | - DETECT_VM_UNTAGGED_SOCKET; + DETECT_VM_CURSOR_LEAKS + | DETECT_VM_CLOSABLE_LEAKS + | DETECT_VM_ACTIVITY_LEAKS + | DETECT_VM_INSTANCE_LEAKS + | DETECT_VM_REGISTRATION_LEAKS + | DETECT_VM_FILE_URI_EXPOSURE + | DETECT_VM_CLEARTEXT_NETWORK + | DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION + | DETECT_VM_UNTAGGED_SOCKET; // Byte 3: Penalty /** {@hide} */ - public static final int PENALTY_LOG = 0x01 << 16; // normal android.util.Log + public static final int PENALTY_LOG = 0x01 << 16; // normal android.util.Log /** {@hide} */ public static final int PENALTY_DIALOG = 0x02 << 16; /** {@hide} */ @@ -271,13 +287,11 @@ public final class StrictMode { public static final int PENALTY_DROPBOX = 0x20 << 16; /** - * Non-public penalty mode which overrides all the other penalty - * bits and signals that we're in a Binder call and we should - * ignore the other penalty bits and instead serialize back all - * our offending stack traces to the caller to ultimately handle - * in the originating process. + * Non-public penalty mode which overrides all the other penalty bits and signals that we're in + * a Binder call and we should ignore the other penalty bits and instead serialize back all our + * offending stack traces to the caller to ultimately handle in the originating process. * - * This must be kept in sync with the constant in libs/binder/Parcel.cpp + * <p>This must be kept in sync with the constant in libs/binder/Parcel.cpp * * @hide */ @@ -308,18 +322,23 @@ public final class StrictMode { // CAUTION: we started stealing the top bits of Byte 4 for VM above - /** - * Mask of all the penalty bits valid for thread policies. - */ + /** Mask of all the penalty bits valid for thread policies. */ private static final int THREAD_PENALTY_MASK = - PENALTY_LOG | PENALTY_DIALOG | PENALTY_DEATH | PENALTY_DROPBOX | PENALTY_GATHER | - PENALTY_DEATH_ON_NETWORK | PENALTY_FLASH; - - /** - * Mask of all the penalty bits valid for VM policies. - */ - private static final int VM_PENALTY_MASK = PENALTY_LOG | PENALTY_DEATH | PENALTY_DROPBOX - | PENALTY_DEATH_ON_CLEARTEXT_NETWORK | PENALTY_DEATH_ON_FILE_URI_EXPOSURE; + PENALTY_LOG + | PENALTY_DIALOG + | PENALTY_DEATH + | PENALTY_DROPBOX + | PENALTY_GATHER + | PENALTY_DEATH_ON_NETWORK + | PENALTY_FLASH; + + /** Mask of all the penalty bits valid for VM policies. */ + private static final int VM_PENALTY_MASK = + PENALTY_LOG + | PENALTY_DEATH + | PENALTY_DROPBOX + | PENALTY_DEATH_ON_CLEARTEXT_NETWORK + | PENALTY_DEATH_ON_FILE_URI_EXPOSURE; /** {@hide} */ public static final int NETWORK_POLICY_ACCEPT = 0; @@ -330,33 +349,67 @@ public final class StrictMode { // TODO: wrap in some ImmutableHashMap thing. // Note: must be before static initialization of sVmPolicy. - private static final HashMap<Class, Integer> EMPTY_CLASS_LIMIT_MAP = new HashMap<Class, Integer>(); + private static final HashMap<Class, Integer> EMPTY_CLASS_LIMIT_MAP = + new HashMap<Class, Integer>(); - /** - * The current VmPolicy in effect. - * - * TODO: these are redundant (mask is in VmPolicy). Should remove sVmPolicyMask. - */ - private static volatile int sVmPolicyMask = 0; + /** The current VmPolicy in effect. */ private static volatile VmPolicy sVmPolicy = VmPolicy.LAX; /** {@hide} */ @TestApi - public interface ViolationListener { - public void onViolation(String message); + public interface ViolationLogger { + + /** Called when penaltyLog is enabled and a violation needs logging. */ + void log(ViolationInfo info); } - private static volatile ViolationListener sListener; + private static final ViolationLogger LOGCAT_LOGGER = + info -> { + String msg; + if (info.durationMillis != -1) { + msg = "StrictMode policy violation; ~duration=" + info.durationMillis + " ms:"; + } else { + msg = "StrictMode policy violation:"; + } + 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 setViolationListener(ViolationListener listener) { - sListener = listener; + public static void setViolationLogger(ViolationLogger listener) { + if (listener == null) { + listener = LOGCAT_LOGGER; + } + sLogger = listener; } /** - * The number of threads trying to do an async dropbox write. - * Just to limit ourselves out of paranoia. + * The number of threads trying to do an async dropbox write. Just to limit ourselves out of + * paranoia. */ private static final AtomicInteger sDropboxCallsInFlight = new AtomicInteger(0); @@ -365,24 +418,25 @@ public final class StrictMode { /** * {@link StrictMode} policy applied to a certain thread. * - * <p>The policy is enabled by {@link #setThreadPolicy}. The current policy - * can be retrieved with {@link #getThreadPolicy}. + * <p>The policy is enabled by {@link #setThreadPolicy}. The current policy can be retrieved + * with {@link #getThreadPolicy}. * - * <p>Note that multiple penalties may be provided and they're run - * in order from least to most severe (logging before process - * death, for example). There's currently no mechanism to choose + * <p>Note that multiple penalties may be provided and they're run in order from least to most + * severe (logging before process death, for example). There's currently no mechanism to choose * different penalties for different detected actions. */ public static final class ThreadPolicy { - /** - * The default, lax policy which doesn't catch anything. - */ - public static final ThreadPolicy LAX = new ThreadPolicy(0); + /** The default, lax policy which doesn't catch anything. */ + 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 @@ -391,16 +445,15 @@ public final class StrictMode { } /** - * Creates {@link ThreadPolicy} instances. Methods whose names start - * with {@code detect} specify what problems we should look - * for. Methods whose names start with {@code penalty} specify what - * we should do when we detect a problem. + * Creates {@link ThreadPolicy} instances. Methods whose names start with {@code detect} + * specify what problems we should look for. Methods whose names start with {@code penalty} + * specify what we should do when we detect a problem. * - * <p>You can call as many {@code detect} and {@code penalty} - * methods as you like. Currently order is insignificant: all - * penalties apply to all detected problems. + * <p>You can call as many {@code detect} and {@code penalty} methods as you like. Currently + * order is insignificant: all penalties apply to all detected problems. * * <p>For example, detect everything and log anything that's found: + * * <pre> * StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder() * .detectAll() @@ -411,20 +464,19 @@ 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 #build} will default - * to enabling {@link #penaltyLog} if no other penalties - * are specified) + * Create a Builder that detects nothing and has no violations. (but note that {@link + * #build} will default to enabling {@link #penaltyLog} if no other penalties are + * specified) */ public Builder() { mMask = 0; } - /** - * Initialize a Builder from an existing ThreadPolicy. - */ + /** Initialize a Builder from an existing ThreadPolicy. */ public Builder(ThreadPolicy policy) { mMask = policy.mask; } @@ -432,8 +484,8 @@ public final class StrictMode { /** * Detect everything that's potentially suspect. * - * <p>As of the Gingerbread release this includes network and - * disk operations but will likely expand in future releases. + * <p>As of the Gingerbread release this includes network and disk operations but will + * likely expand in future releases. */ public Builder detectAll() { detectDiskReads(); @@ -453,135 +505,106 @@ public final class StrictMode { return this; } - /** - * Disable the detection of everything. - */ + /** Disable the detection of everything. */ public Builder permitAll() { return disable(ALL_THREAD_DETECT_BITS); } - /** - * Enable detection of network operations. - */ + /** Enable detection of network operations. */ public Builder detectNetwork() { return enable(DETECT_NETWORK); } - /** - * Disable detection of network operations. - */ + /** Disable detection of network operations. */ public Builder permitNetwork() { return disable(DETECT_NETWORK); } - /** - * Enable detection of disk reads. - */ + /** Enable detection of disk reads. */ public Builder detectDiskReads() { return enable(DETECT_DISK_READ); } - /** - * Disable detection of disk reads. - */ + /** Disable detection of disk reads. */ public Builder permitDiskReads() { return disable(DETECT_DISK_READ); } - /** - * Enable detection of slow calls. - */ + /** Enable detection of slow calls. */ public Builder detectCustomSlowCalls() { return enable(DETECT_CUSTOM); } - /** - * Disable detection of slow calls. - */ + /** Disable detection of slow calls. */ public Builder permitCustomSlowCalls() { return disable(DETECT_CUSTOM); } - /** - * Disable detection of mismatches between defined resource types - * and getter calls. - */ + /** Disable detection of mismatches between defined resource types and getter calls. */ public Builder permitResourceMismatches() { return disable(DETECT_RESOURCE_MISMATCH); } - /** - * Detect unbuffered input/output operations. - */ + /** Detect unbuffered input/output operations. */ public Builder detectUnbufferedIo() { return enable(DETECT_UNBUFFERED_IO); } - /** - * Disable detection of unbuffered input/output operations. - */ + /** Disable detection of unbuffered input/output operations. */ public Builder permitUnbufferedIo() { return disable(DETECT_UNBUFFERED_IO); } /** - * Enables detection of mismatches between defined resource types - * and getter calls. - * <p> - * This helps detect accidental type mismatches and potentially - * expensive type conversions when obtaining typed resources. - * <p> - * For example, a strict mode violation would be thrown when - * calling {@link android.content.res.TypedArray#getInt(int, int)} - * on an index that contains a String-type resource. If the string - * value can be parsed as an integer, this method call will return - * a value without crashing; however, the developer should format - * the resource as an integer to avoid unnecessary type conversion. + * Enables detection of mismatches between defined resource types and getter calls. + * + * <p>This helps detect accidental type mismatches and potentially expensive type + * conversions when obtaining typed resources. + * + * <p>For example, a strict mode violation would be thrown when calling {@link + * android.content.res.TypedArray#getInt(int, int)} on an index that contains a + * String-type resource. If the string value can be parsed as an integer, this method + * call will return a value without crashing; however, the developer should format the + * resource as an integer to avoid unnecessary type conversion. */ public Builder detectResourceMismatches() { return enable(DETECT_RESOURCE_MISMATCH); } - /** - * Enable detection of disk writes. - */ + /** Enable detection of disk writes. */ public Builder detectDiskWrites() { return enable(DETECT_DISK_WRITE); } - /** - * Disable detection of disk writes. - */ + /** Disable detection of disk writes. */ public Builder permitDiskWrites() { return disable(DETECT_DISK_WRITE); } /** - * Show an annoying dialog to the developer on detected - * violations, rate-limited to be only a little annoying. + * Show an annoying dialog to the developer on detected violations, rate-limited to be + * only a little annoying. */ public Builder penaltyDialog() { return enable(PENALTY_DIALOG); } /** - * Crash the whole process on violation. This penalty runs at - * the end of all enabled penalties so you'll still get - * see logging or other violations before the process dies. + * Crash the whole process on violation. This penalty runs at the end of all enabled + * penalties so you'll still get see logging or other violations before the process + * dies. * - * <p>Unlike {@link #penaltyDeathOnNetwork}, this applies - * to disk reads, disk writes, and network usage if their - * corresponding detect flags are set. + * <p>Unlike {@link #penaltyDeathOnNetwork}, this applies to disk reads, disk writes, + * and network usage if their corresponding detect flags are set. */ public Builder penaltyDeath() { return enable(PENALTY_DEATH); } /** - * Crash the whole process on any network usage. Unlike - * {@link #penaltyDeath}, this penalty runs - * <em>before</em> anything else. You must still have - * called {@link #detectNetwork} to enable this. + * Crash the whole process on any network usage. Unlike {@link #penaltyDeath}, this + * penalty runs <em>before</em> anything else. You must still have called {@link + * #detectNetwork} to enable this. * * <p>In the Honeycomb or later SDKs, this is on by default. */ @@ -589,30 +612,39 @@ public final class StrictMode { return enable(PENALTY_DEATH_ON_NETWORK); } - /** - * Flash the screen during a violation. - */ + /** Flash the screen during a violation. */ public Builder penaltyFlashScreen() { return enable(PENALTY_FLASH); } - /** - * Log detected violations to the system log. - */ + /** Log detected violations to the system log. */ public Builder penaltyLog() { return enable(PENALTY_LOG); } /** - * Enable detected violations log a stacktrace and timing data - * to the {@link android.os.DropBoxManager DropBox} on policy - * violation. Intended mostly for platform integrators doing - * beta user field data collection. + * Enable detected violations log a stacktrace and timing data to the {@link + * android.os.DropBoxManager DropBox} on policy violation. Intended mostly for platform + * integrators doing beta user field data collection. */ public Builder penaltyDropBox() { 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; @@ -626,19 +658,23 @@ public final class StrictMode { /** * Construct the ThreadPolicy instance. * - * <p>Note: if no penalties are enabled before calling - * <code>build</code>, {@link #penaltyLog} is implicitly - * set. + * <p>Note: if no penalties are enabled before calling <code>build</code>, {@link + * #penaltyLog} is implicitly set. */ public ThreadPolicy build() { // If there are detection bits set but no violation bits // set, enable simple logging. - if (mMask != 0 && - (mMask & (PENALTY_DEATH | PENALTY_LOG | - PENALTY_DROPBOX | PENALTY_DIALOG)) == 0) { + if (mListener == null + && mMask != 0 + && (mMask + & (PENALTY_DEATH + | PENALTY_LOG + | PENALTY_DROPBOX + | PENALTY_DIALOG)) + == 0) { penaltyLog(); } - return new ThreadPolicy(mMask); + return new ThreadPolicy(mMask, mListener, mExecutor); } } } @@ -649,22 +685,28 @@ public final class StrictMode { * <p>The policy is enabled by {@link #setVmPolicy}. */ 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); + /** The default, lax policy which doesn't catch anything. */ + 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 @@ -673,16 +715,15 @@ public final class StrictMode { } /** - * Creates {@link VmPolicy} instances. Methods whose names start - * with {@code detect} specify what problems we should look - * for. Methods whose names start with {@code penalty} specify what - * we should do when we detect a problem. + * Creates {@link VmPolicy} instances. Methods whose names start with {@code detect} specify + * what problems we should look for. Methods whose names start with {@code penalty} specify + * what we should do when we detect a problem. * - * <p>You can call as many {@code detect} and {@code penalty} - * methods as you like. Currently order is insignificant: all - * penalties apply to all detected problems. + * <p>You can call as many {@code detect} and {@code penalty} methods as you like. Currently + * order is insignificant: all penalties apply to all detected problems. * * <p>For example, detect everything and log anything that's found: + * * <pre> * StrictMode.VmPolicy policy = new StrictMode.VmPolicy.Builder() * .detectAll() @@ -693,34 +734,36 @@ 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 + private HashMap<Class, Integer> mClassInstanceLimit; // null until needed + private boolean mClassInstanceLimitNeedCow = false; // need copy-on-write public Builder() { mMask = 0; } - /** - * Build upon an existing VmPolicy. - */ + /** Build upon an existing VmPolicy. */ public Builder(VmPolicy base) { mMask = base.mask; mClassInstanceLimitNeedCow = true; mClassInstanceLimit = base.classInstanceLimit; + mListener = base.mListener; + mExecutor = base.mCallbackExecutor; } /** - * Set an upper bound on how many instances of a class can be in memory - * at once. Helps to prevent object leaks. + * Set an upper bound on how many instances of a class can be in memory at once. Helps + * to prevent object leaks. */ public Builder setClassInstanceLimit(Class klass, int instanceLimit) { if (klass == null) { throw new NullPointerException("klass == null"); } if (mClassInstanceLimitNeedCow) { - if (mClassInstanceLimit.containsKey(klass) && - mClassInstanceLimit.get(klass) == instanceLimit) { + if (mClassInstanceLimit.containsKey(klass) + && mClassInstanceLimit.get(klass) == instanceLimit) { // no-op; don't break COW return this; } @@ -734,19 +777,21 @@ public final class StrictMode { return this; } - /** - * Detect leaks of {@link android.app.Activity} subclasses. - */ + /** Detect leaks of {@link android.app.Activity} subclasses. */ public Builder detectActivityLeaks() { return enable(DETECT_VM_ACTIVITY_LEAKS); } + /** @hide */ + public Builder permitActivityLeaks() { + return disable(DETECT_VM_ACTIVITY_LEAKS); + } + /** * Detect everything that's potentially suspect. * - * <p>In the Honeycomb release this includes leaks of - * SQLite cursors, Activities, and other closable objects - * but will likely expand in future releases. + * <p>In the Honeycomb release this includes leaks of SQLite cursors, Activities, and + * other closable objects but will likely expand in future releases. */ public Builder detectAll() { detectLeakedSqlLiteObjects(); @@ -777,53 +822,46 @@ public final class StrictMode { } /** - * Detect when an - * {@link android.database.sqlite.SQLiteCursor} or other - * SQLite object is finalized without having been closed. + * Detect when an {@link android.database.sqlite.SQLiteCursor} or other SQLite object is + * finalized without having been closed. * - * <p>You always want to explicitly close your SQLite - * cursors to avoid unnecessary database contention and - * temporary memory leaks. + * <p>You always want to explicitly close your SQLite cursors to avoid unnecessary + * database contention and temporary memory leaks. */ public Builder detectLeakedSqlLiteObjects() { return enable(DETECT_VM_CURSOR_LEAKS); } /** - * Detect when an {@link java.io.Closeable} or other - * object with an explicit termination method is finalized - * without having been closed. + * Detect when an {@link java.io.Closeable} or other object with an explicit termination + * method is finalized without having been closed. * - * <p>You always want to explicitly close such objects to - * avoid unnecessary resources leaks. + * <p>You always want to explicitly close such objects to avoid unnecessary resources + * leaks. */ public Builder detectLeakedClosableObjects() { return enable(DETECT_VM_CLOSABLE_LEAKS); } /** - * Detect when a {@link BroadcastReceiver} or - * {@link ServiceConnection} is leaked during {@link Context} - * teardown. + * Detect when a {@link BroadcastReceiver} or {@link ServiceConnection} is leaked during + * {@link Context} teardown. */ public Builder detectLeakedRegistrationObjects() { return enable(DETECT_VM_REGISTRATION_LEAKS); } /** - * Detect when the calling application exposes a {@code file://} - * {@link android.net.Uri} to another app. - * <p> - * This exposure is discouraged since the receiving app may not have - * access to the shared path. For example, the receiving app may not - * have requested the - * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} runtime - * permission, or the platform may be sharing the - * {@link android.net.Uri} across user profile boundaries. - * <p> - * Instead, apps should use {@code content://} Uris so the platform - * can extend temporary permission for the receiving app to access - * the resource. + * Detect when the calling application exposes a {@code file://} {@link android.net.Uri} + * to another app. + * + * <p>This exposure is discouraged since the receiving app may not have access to the + * shared path. For example, the receiving app may not have requested the {@link + * android.Manifest.permission#READ_EXTERNAL_STORAGE} runtime permission, or the + * platform may be sharing the {@link android.net.Uri} across user profile boundaries. + * + * <p>Instead, apps should use {@code content://} Uris so the platform can extend + * temporary permission for the receiving app to access the resource. * * @see android.support.v4.content.FileProvider * @see Intent#FLAG_GRANT_READ_URI_PERMISSION @@ -833,34 +871,32 @@ public final class StrictMode { } /** - * Detect any network traffic from the calling app which is not - * wrapped in SSL/TLS. This can help you detect places that your app - * is inadvertently sending cleartext data across the network. - * <p> - * Using {@link #penaltyDeath()} or - * {@link #penaltyDeathOnCleartextNetwork()} will block further - * traffic on that socket to prevent accidental data leakage, in - * addition to crashing your process. - * <p> - * Using {@link #penaltyDropBox()} will log the raw contents of the - * packet that triggered the violation. - * <p> - * This inspects both IPv4/IPv6 and TCP/UDP network traffic, but it - * may be subject to false positives, such as when STARTTLS - * protocols or HTTP proxies are used. + * Detect any network traffic from the calling app which is not wrapped in SSL/TLS. This + * can help you detect places that your app is inadvertently sending cleartext data + * across the network. + * + * <p>Using {@link #penaltyDeath()} or {@link #penaltyDeathOnCleartextNetwork()} will + * block further traffic on that socket to prevent accidental data leakage, in addition + * to crashing your process. + * + * <p>Using {@link #penaltyDropBox()} will log the raw contents of the packet that + * triggered the violation. + * + * <p>This inspects both IPv4/IPv6 and TCP/UDP network traffic, but it may be subject to + * false positives, such as when STARTTLS protocols or HTTP proxies are used. */ public Builder detectCleartextNetwork() { return enable(DETECT_VM_CLEARTEXT_NETWORK); } /** - * Detect when the calling application sends a {@code content://} - * {@link android.net.Uri} to another app without setting - * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or - * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. - * <p> - * Forgetting to include one or more of these flags when sending an - * intent is typically an app bug. + * Detect when the calling application sends a {@code content://} {@link + * android.net.Uri} to another app without setting {@link + * Intent#FLAG_GRANT_READ_URI_PERMISSION} or {@link + * Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * <p>Forgetting to include one or more of these flags when sending an intent is + * typically an app bug. * * @see Intent#FLAG_GRANT_READ_URI_PERMISSION * @see Intent#FLAG_GRANT_WRITE_URI_PERMISSION @@ -870,12 +906,11 @@ public final class StrictMode { } /** - * Detect any sockets in the calling app which have not been tagged - * using {@link TrafficStats}. Tagging sockets can help you - * investigate network usage inside your app, such as a narrowing - * down heavy usage to a specific library or component. - * <p> - * This currently does not detect sockets created in native code. + * Detect any sockets in the calling app which have not been tagged using {@link + * TrafficStats}. Tagging sockets can help you investigate network usage inside your + * app, such as a narrowing down heavy usage to a specific library or component. + * + * <p>This currently does not detect sockets created in native code. * * @see TrafficStats#setThreadStatsTag(int) * @see TrafficStats#tagSocket(java.net.Socket) @@ -885,18 +920,22 @@ 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 dies. + * 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 + * dies. */ public Builder penaltyDeath() { return enable(PENALTY_DEATH); } /** - * Crashes the whole process when cleartext network traffic is - * detected. + * Crashes the whole process when cleartext network traffic is detected. * * @see #detectCleartextNetwork() */ @@ -905,8 +944,8 @@ public final class StrictMode { } /** - * Crashes the whole process when a {@code file://} - * {@link android.net.Uri} is exposed beyond this app. + * Crashes the whole process when a {@code file://} {@link android.net.Uri} is exposed + * beyond this app. * * @see #detectFileUriExposure() */ @@ -914,23 +953,33 @@ public final class StrictMode { return enable(PENALTY_DEATH_ON_FILE_URI_EXPOSURE); } - /** - * Log detected violations to the system log. - */ + /** Log detected violations to the system log. */ public Builder penaltyLog() { return enable(PENALTY_LOG); } /** - * Enable detected violations log a stacktrace and timing data - * to the {@link android.os.DropBoxManager DropBox} on policy - * violation. Intended mostly for platform integrators doing - * beta user field data collection. + * Enable detected violations log a stacktrace and timing data to the {@link + * android.os.DropBoxManager DropBox} on policy violation. Intended mostly for platform + * integrators doing beta user field data collection. */ public Builder penaltyDropBox() { 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; @@ -944,56 +993,65 @@ public final class StrictMode { /** * Construct the VmPolicy instance. * - * <p>Note: if no penalties are enabled before calling - * <code>build</code>, {@link #penaltyLog} is implicitly - * set. + * <p>Note: if no penalties are enabled before calling <code>build</code>, {@link + * #penaltyLog} is implicitly set. */ public VmPolicy build() { // If there are detection bits set but no violation bits // set, enable simple logging. - if (mMask != 0 && - (mMask & (PENALTY_DEATH | PENALTY_LOG | - PENALTY_DROPBOX | PENALTY_DIALOG)) == 0) { + if (mListener == null + && mMask != 0 + && (mMask + & (PENALTY_DEATH + | PENALTY_LOG + | PENALTY_DROPBOX + | PENALTY_DIALOG)) + == 0) { penaltyLog(); } - return new VmPolicy(mMask, - mClassInstanceLimit != null ? mClassInstanceLimit : EMPTY_CLASS_LIMIT_MAP); + return new VmPolicy( + mMask, + mClassInstanceLimit != null ? mClassInstanceLimit : EMPTY_CLASS_LIMIT_MAP, + mListener, + mExecutor); } } } /** - * Log of strict mode violation stack traces that have occurred - * during a Binder call, to be serialized back later to the caller - * via Parcel.writeNoException() (amusingly) where the caller can - * choose how to react. + * Log of strict mode violation stack traces that have occurred during a Binder call, to be + * serialized back later to the caller via Parcel.writeNoException() (amusingly) where the + * caller can choose how to react. */ private static final ThreadLocal<ArrayList<ViolationInfo>> gatheredViolations = new ThreadLocal<ArrayList<ViolationInfo>>() { - @Override protected ArrayList<ViolationInfo> initialValue() { - // Starts null to avoid unnecessary allocations when - // checking whether there are any violations or not in - // hasGatheredViolations() below. - return null; - } - }; + @Override + protected ArrayList<ViolationInfo> initialValue() { + // Starts null to avoid unnecessary allocations when + // checking whether there are any violations or not in + // hasGatheredViolations() below. + return null; + } + }; /** - * Sets the policy for what actions on the current thread should - * be detected, as well as the penalty if such actions occur. + * Sets the policy for what actions on the current thread should be detected, as well as the + * penalty if such actions occur. * - * <p>Internally this sets a thread-local variable which is - * propagated across cross-process IPC calls, meaning you can - * catch violations when a system service or another process - * accesses the disk or network on your behalf. + * <p>Internally this sets a thread-local variable which is propagated across cross-process IPC + * calls, meaning you can catch violations when a system service or another process accesses the + * disk or network on your behalf. * * @param policy the policy to put into place */ 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, @@ -1016,7 +1074,7 @@ public final class StrictMode { if (policy instanceof AndroidBlockGuardPolicy) { androidPolicy = (AndroidBlockGuardPolicy) policy; } else { - androidPolicy = threadAndroidPolicy.get(); + androidPolicy = THREAD_ANDROID_POLICY.get(); BlockGuard.setThreadPolicy(androidPolicy); } androidPolicy.setPolicyMask(policyMask); @@ -1031,145 +1089,121 @@ public final class StrictMode { } /** - * @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. * * @return the bitmask of all the DETECT_* and PENALTY_* bits currently enabled - * * @hide */ public static int getThreadPolicyMask() { return BlockGuard.getThreadPolicy().getPolicyMask(); } - /** - * Returns the current thread's policy. - */ + /** Returns the current thread's policy. */ public static ThreadPolicy getThreadPolicy() { // TODO: this was a last minute Gingerbread API change (to // 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()); } /** - * A convenience wrapper that takes the current - * {@link ThreadPolicy} from {@link #getThreadPolicy}, modifies it - * to permit both disk reads & writes, and sets the new policy - * with {@link #setThreadPolicy}, returning the old policy so you - * can restore it at the end of a block. + * A convenience wrapper that takes the current {@link ThreadPolicy} from {@link + * #getThreadPolicy}, modifies it to permit both disk reads & writes, and sets the new + * policy with {@link #setThreadPolicy}, returning the old policy so you can restore it at the + * end of a block. * - * @return the old policy, to be passed to {@link #setThreadPolicy} to - * restore the policy at the end of a block + * @return the old policy, to be passed to {@link #setThreadPolicy} to restore the policy at the + * 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; } /** - * A convenience wrapper that takes the current - * {@link ThreadPolicy} from {@link #getThreadPolicy}, modifies it - * to permit disk reads, and sets the new policy - * with {@link #setThreadPolicy}, returning the old policy so you - * can restore it at the end of a block. + * A convenience wrapper that takes the current {@link ThreadPolicy} from {@link + * #getThreadPolicy}, modifies it to permit disk reads, and sets the new policy with {@link + * #setThreadPolicy}, returning the old policy so you can restore it at the end of a block. * - * @return the old policy, to be passed to setThreadPolicy to - * restore the policy. + * @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; + } + + private static VmPolicy allowVmViolations() { + VmPolicy oldPolicy = getVmPolicy(); + sVmPolicy = VmPolicy.LAX; + 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.")) { + /** + * 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; } } @@ -1177,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; + // Starting in N, we don't allow file:// Uri exposure + if (targetSdkVersion >= Build.VERSION_CODES.N) { + builder.detectFileUriExposure(); + builder.penaltyDeathOnFileUriExposure(); } - if (doFlashes) { - threadPolicyMask |= StrictMode.PENALTY_FLASH; - } - - 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()); } /** @@ -1260,31 +1294,42 @@ public final class StrictMode { * @hide */ public static void enableDeathOnFileUriExposure() { - sVmPolicyMask |= DETECT_VM_FILE_URI_EXPOSURE | PENALTY_DEATH_ON_FILE_URI_EXPOSURE; + sVmPolicy = + new VmPolicy( + sVmPolicy.mask + | DETECT_VM_FILE_URI_EXPOSURE + | PENALTY_DEATH_ON_FILE_URI_EXPOSURE, + sVmPolicy.classInstanceLimit, + sVmPolicy.mListener, + sVmPolicy.mCallbackExecutor); } /** - * Used by lame internal apps that haven't done the hard work to get - * themselves off file:// Uris yet. + * Used by lame internal apps that haven't done the hard work to get themselves off file:// Uris + * yet. * * @hide */ public static void disableDeathOnFileUriExposure() { - sVmPolicyMask &= ~(DETECT_VM_FILE_URI_EXPOSURE | PENALTY_DEATH_ON_FILE_URI_EXPOSURE); + sVmPolicy = + new VmPolicy( + sVmPolicy.mask + & ~(DETECT_VM_FILE_URI_EXPOSURE + | PENALTY_DEATH_ON_FILE_URI_EXPOSURE), + sVmPolicy.classInstanceLimit, + sVmPolicy.mListener, + sVmPolicy.mCallbackExecutor); } /** - * Parses the BlockGuard policy mask out from the Exception's - * getMessage() String value. Kinda gross, but least - * invasive. :/ + * Parses the BlockGuard policy mask out from the Exception's getMessage() String value. Kinda + * gross, but least invasive. :/ * - * Input is of the following forms: - * "policy=137 violation=64" - * "policy=137 violation=64 msg=Arbitrary text" + * <p>Input is of the following forms: "policy=137 violation=64" "policy=137 violation=64 + * msg=Arbitrary text" * - * Returns 0 on failure, which is a valid policy, but not a - * valid policy during a violation (else there must've been - * some policy in effect to violate). + * <p>Returns 0 on failure, which is a valid policy, but not a valid policy during a violation + * (else there must've been some policy in effect to violate). */ private static int parsePolicyFromMessage(String message) { if (message == null || !message.startsWith("policy=")) { @@ -1302,51 +1347,30 @@ public final class StrictMode { } } - /** - * Like parsePolicyFromMessage(), but returns the violation. - */ - private static int parseViolationFromMessage(String message) { - if (message == null) { - return 0; - } - 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 { - return Integer.parseInt(violationString); - } catch (NumberFormatException e) { - return 0; - } - } - private static final ThreadLocal<ArrayList<ViolationInfo>> violationsBeingTimed = new ThreadLocal<ArrayList<ViolationInfo>>() { - @Override protected ArrayList<ViolationInfo> initialValue() { - return new ArrayList<ViolationInfo>(); - } - }; + @Override + protected ArrayList<ViolationInfo> initialValue() { + return new ArrayList<ViolationInfo>(); + } + }; // Note: only access this once verifying the thread has a Looper. - private static final ThreadLocal<Handler> threadHandler = new ThreadLocal<Handler>() { - @Override protected Handler initialValue() { - return new Handler(); - } - }; + private static final ThreadLocal<Handler> THREAD_HANDLER = + new ThreadLocal<Handler>() { + @Override + protected Handler initialValue() { + return new Handler(); + } + }; - private static final ThreadLocal<AndroidBlockGuardPolicy> - threadAndroidPolicy = new ThreadLocal<AndroidBlockGuardPolicy>() { - @Override - protected AndroidBlockGuardPolicy initialValue() { - return new AndroidBlockGuardPolicy(0); - } - }; + private static final ThreadLocal<AndroidBlockGuardPolicy> THREAD_ANDROID_POLICY = + new ThreadLocal<AndroidBlockGuardPolicy>() { + @Override + protected AndroidBlockGuardPolicy initialValue() { + return new AndroidBlockGuardPolicy(0); + } + }; private static boolean tooManyViolationsThisLoop() { return violationsBeingTimed.get().size() >= MAX_OFFENSES_PER_LOOP; @@ -1382,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: @@ -1395,9 +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: @@ -1408,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; @@ -1422,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: @@ -1436,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: @@ -1452,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) { @@ -1466,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); } @@ -1496,10 +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) { - info.durationMillis = -1; // unknown (redundant, already set) - handleViolation(info); + if (looper == null || (info.mPolicy & THREAD_PENALTY_MASK) == PENALTY_DEATH) { + info.durationMillis = -1; // unknown (redundant, already set) + onThreadPolicyViolation(info); return; } @@ -1516,8 +1525,8 @@ public final class StrictMode { return; } - final IWindowManager windowManager = (info.policy & PENALTY_FLASH) != 0 ? - sWindowManager.get() : null; + final IWindowManager windowManager = + info.penaltyEnabled(PENALTY_FLASH) ? sWindowManager.get() : null; if (windowManager != null) { try { windowManager.showStrictModeViolation(true); @@ -1534,31 +1543,32 @@ public final class StrictMode { // throttled back to 60fps via SurfaceFlinger/View // invalidates, _not_ by posting frame updates every 16 // milliseconds. - threadHandler.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) { - } - } - - 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(); - } - }); + THREAD_HANDLER + .get() + .postAtFrontOfQueue( + () -> { + 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); + onThreadPolicyViolation(v); + } + records.clear(); + }); } // Note: It's possible (even quite likely) that the @@ -1566,22 +1576,17 @@ 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.crashInfo == null || info.crashInfo.stackTrace == null) { - 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) { - if (info.crashInfo.stackTrace.equals(previous.crashInfo.stackTrace)) { + if (info.getStackTrace().equals(previous.getStackTrace())) { // Duplicate. Don't log. return; } @@ -1599,47 +1604,39 @@ public final class StrictMode { lastViolationTime = vtime; } } else { - mLastViolationTime = new ArrayMap<Integer, Long>(1); + mLastViolationTime = new ArrayMap<>(1); } long now = SystemClock.uptimeMillis(); mLastViolationTime.put(crashFingerprint, now); - long timeSinceLastViolationMillis = lastViolationTime == 0 ? - Long.MAX_VALUE : (now - lastViolationTime); + long timeSinceLastViolationMillis = + lastViolationTime == 0 ? Long.MAX_VALUE : (now - lastViolationTime); - if ((info.policy & PENALTY_LOG) != 0 && sListener != null) { - sListener.onViolation(info.crashInfo.stackTrace); - } - if ((info.policy & PENALTY_LOG) != 0 && - timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { - if (info.durationMillis != -1) { - Log.d(TAG, "StrictMode policy violation; ~duration=" + - info.durationMillis + " ms: " + info.crashInfo.stackTrace); - } else { - Log.d(TAG, "StrictMode policy violation: " + info.crashInfo.stackTrace); - } + 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 && - timeSinceLastViolationMillis > MIN_DIALOG_INTERVAL_MS) { + 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) { - int violationBit = parseViolationFromMessage(info.crashInfo.exceptionMessage); - violationMaskSubset |= violationBit; - final int savedPolicyMask = getThreadPolicyMask(); + violationMaskSubset |= info.getViolationBit(); - 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 @@ -1648,51 +1645,43 @@ 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) { - int violationBit = parseViolationFromMessage(info.crashInfo.exceptionMessage); - throw new StrictModeViolation(info.policy, violationBit, 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 cases we - * don't need to do a synchronous call to the ActivityManager. - * This is used by both per-thread and vm-wide violations when - * applicable. + * 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 + * cases we don't need to do a synchronous call to the ActivityManager. This is used by both + * per-thread and vm-wide violations when applicable. */ private static void dropboxViolationAsync( final int violationMaskSubset, final ViolationInfo info) { @@ -1706,57 +1695,61 @@ 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); + } + } catch (RemoteException e) { + if (e instanceof DeadObjectException) { + // System process is dead; ignore + } else { + Log.e(TAG, "RemoteException handling StrictMode violation", e); } - }.start(); + } finally { + setThreadPolicyMask(oldMask); + } } private static class AndroidCloseGuardReporter implements CloseGuard.Reporter { public void report(String message, Throwable allocationSite) { - onVmPolicyViolation(message, allocationSite); + onVmPolicyViolation(new LeakedClosableViolation(message, allocationSite)); } } - /** - * Called from Parcel.writeNoException() - */ + /** Called from Parcel.writeNoException() */ /* package */ static boolean hasGatheredViolations() { return gatheredViolations.get() != null; } /** - * Called from Parcel.writeException(), so we drop this memory and - * don't incorrectly attribute it to the wrong caller on the next - * Binder call on this thread. + * Called from Parcel.writeException(), so we drop this memory and don't incorrectly attribute + * it to the wrong caller on the next Binder call on this thread. */ /* package */ static void clearGatheredViolations() { gatheredViolations.set(null); } - /** - * @hide - */ + /** @hide */ public static void conditionallyCheckInstanceCounts() { VmPolicy policy = getVmPolicy(); int policySize = policy.classInstanceLimit.size(); @@ -1777,14 +1770,13 @@ 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.getMessage(), tr); + onVmPolicyViolation(new InstanceCountViolation(klass, instances, limit)); } } } private static long sLastInstanceCountCheckMillis = 0; - private static boolean sIsIdlerRegistered = false; // guarded by StrictMode.class + private static boolean sIsIdlerRegistered = false; // guarded by StrictMode.class private static final MessageQueue.IdleHandler sProcessIdleHandler = new MessageQueue.IdleHandler() { public boolean queueIdle() { @@ -1798,23 +1790,21 @@ public final class StrictMode { }; /** - * Sets the policy for what actions in the VM process (on any - * thread) should be detected, as well as the penalty if such - * actions occur. + * Sets the policy for what actions in the VM process (on any thread) should be detected, as + * well as the penalty if such actions occur. * * @param policy the policy to put into place */ public static void setVmPolicy(final VmPolicy policy) { synchronized (StrictMode.class) { sVmPolicy = policy; - sVmPolicyMask = policy.mask; setCloseGuardEnabled(vmClosableObjectLeaksEnabled()); Looper looper = Looper.getMainLooper(); if (looper != null) { MessageQueue mq = looper.mQueue; - if (policy.classInstanceLimit.size() == 0 || - (sVmPolicyMask & VM_PENALTY_MASK) == 0) { + if (policy.classInstanceLimit.size() == 0 + || (sVmPolicy.mask & VM_PENALTY_MASK) == 0) { mq.removeIdleHandler(sProcessIdleHandler); sIsIdlerRegistered = false; } else if (!sIsIdlerRegistered) { @@ -1824,17 +1814,18 @@ public final class StrictMode { } int networkPolicy = NETWORK_POLICY_ACCEPT; - if ((sVmPolicyMask & DETECT_VM_CLEARTEXT_NETWORK) != 0) { - if ((sVmPolicyMask & PENALTY_DEATH) != 0 - || (sVmPolicyMask & PENALTY_DEATH_ON_CLEARTEXT_NETWORK) != 0) { + if ((sVmPolicy.mask & DETECT_VM_CLEARTEXT_NETWORK) != 0) { + if ((sVmPolicy.mask & PENALTY_DEATH) != 0 + || (sVmPolicy.mask & PENALTY_DEATH_ON_CLEARTEXT_NETWORK) != 0) { networkPolicy = NETWORK_POLICY_REJECT; } else { networkPolicy = NETWORK_POLICY_LOG; } } - final INetworkManagementService netd = INetworkManagementService.Stub.asInterface( - ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); + final INetworkManagementService netd = + INetworkManagementService.Stub.asInterface( + ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); if (netd != null) { try { netd.setUidCleartextNetworkPolicy(android.os.Process.myUid(), networkPolicy); @@ -1846,9 +1837,7 @@ public final class StrictMode { } } - /** - * Gets the current VM policy. - */ + /** Gets the current VM policy. */ public static VmPolicy getVmPolicy() { synchronized (StrictMode.class) { return sVmPolicy; @@ -1858,124 +1847,90 @@ public final class StrictMode { /** * Enable the recommended StrictMode defaults, with violations just being logged. * - * <p>This catches disk and network access on the main thread, as - * well as leaked SQLite cursors and unclosed resources. This is - * simply a wrapper around {@link #setVmPolicy} and {@link + * <p>This catches disk and network access on the main thread, as well as leaked SQLite cursors + * and unclosed resources. This is simply a wrapper around {@link #setVmPolicy} and {@link * #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 - */ + /** @hide */ public static boolean vmSqliteObjectLeaksEnabled() { - return (sVmPolicyMask & DETECT_VM_CURSOR_LEAKS) != 0; + return (sVmPolicy.mask & DETECT_VM_CURSOR_LEAKS) != 0; } - /** - * @hide - */ + /** @hide */ public static boolean vmClosableObjectLeaksEnabled() { - return (sVmPolicyMask & DETECT_VM_CLOSABLE_LEAKS) != 0; + return (sVmPolicy.mask & DETECT_VM_CLOSABLE_LEAKS) != 0; } - /** - * @hide - */ + /** @hide */ public static boolean vmRegistrationLeaksEnabled() { - return (sVmPolicyMask & DETECT_VM_REGISTRATION_LEAKS) != 0; + return (sVmPolicy.mask & DETECT_VM_REGISTRATION_LEAKS) != 0; } - /** - * @hide - */ + /** @hide */ public static boolean vmFileUriExposureEnabled() { - return (sVmPolicyMask & DETECT_VM_FILE_URI_EXPOSURE) != 0; + return (sVmPolicy.mask & DETECT_VM_FILE_URI_EXPOSURE) != 0; } - /** - * @hide - */ + /** @hide */ public static boolean vmCleartextNetworkEnabled() { - return (sVmPolicyMask & DETECT_VM_CLEARTEXT_NETWORK) != 0; + return (sVmPolicy.mask & DETECT_VM_CLEARTEXT_NETWORK) != 0; } - /** - * @hide - */ + /** @hide */ public static boolean vmContentUriWithoutPermissionEnabled() { - return (sVmPolicyMask & DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION) != 0; + return (sVmPolicy.mask & DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION) != 0; } - /** - * @hide - */ + /** @hide */ public static boolean vmUntaggedSocketEnabled() { - return (sVmPolicyMask & DETECT_VM_UNTAGGED_SOCKET) != 0; + return (sVmPolicy.mask & DETECT_VM_UNTAGGED_SOCKET) != 0; } - /** - * @hide - */ + /** @hide */ public static void onSqliteObjectLeaked(String message, Throwable originStack) { - onVmPolicyViolation(message, originStack); + onVmPolicyViolation(new SqliteObjectLeakedViolation(message, originStack)); } - /** - * @hide - */ + /** @hide */ public static void onWebViewMethodCalledOnWrongThread(Throwable originStack) { - onVmPolicyViolation(null, originStack); + onVmPolicyViolation(new WebViewMethodCalledOnWrongThreadViolation(originStack)); } - /** - * @hide - */ + /** @hide */ public static void onIntentReceiverLeaked(Throwable originStack) { - onVmPolicyViolation(null, originStack); + onVmPolicyViolation(new IntentReceiverLeakedViolation(originStack)); } - /** - * @hide - */ + /** @hide */ public static void onServiceConnectionLeaked(Throwable originStack) { - onVmPolicyViolation(null, originStack); + onVmPolicyViolation(new ServiceConnectionLeakedViolation(originStack)); } - /** - * @hide - */ + /** @hide */ public static void onFileUriExposed(Uri uri, String location) { final String message = uri + " exposed beyond app through " + location; - if ((sVmPolicyMask & PENALTY_DEATH_ON_FILE_URI_EXPOSURE) != 0) { + if ((sVmPolicy.mask & PENALTY_DEATH_ON_FILE_URI_EXPOSURE) != 0) { throw new FileUriExposedException(message); } else { - onVmPolicyViolation(null, new Throwable(message)); + onVmPolicyViolation(new FileUriExposedViolation(message)); } } - /** - * @hide - */ + /** @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(null, new Throwable(message)); + onVmPolicyViolation(new ContentUriWithoutPermissionViolation(uri, location)); } - /** - * @hide - */ + /** @hide */ + public static final String CLEARTEXT_DETECTED_MSG = + "Detected cleartext network traffic from UID "; + + /** @hide */ public static void onCleartextNetworkDetected(byte[] firstPacket) { byte[] rawAddr = null; if (firstPacket != null) { @@ -1991,47 +1946,37 @@ public final class StrictMode { } final int uid = android.os.Process.myUid(); - String msg = "Detected cleartext network traffic from UID " + uid; + String msg = CLEARTEXT_DETECTED_MSG + uid; if (rawAddr != null) { try { - msg = "Detected cleartext network traffic from UID " + uid + " to " - + InetAddress.getByAddress(rawAddr); + msg += " to " + InetAddress.getByAddress(rawAddr); } catch (UnknownHostException ignored) { } } - - final boolean forceDeath = (sVmPolicyMask & PENALTY_DEATH_ON_CLEARTEXT_NETWORK) != 0; - onVmPolicyViolation(HexDump.dumpHexString(firstPacket).trim(), new Throwable(msg), - forceDeath); + msg += HexDump.dumpHexString(firstPacket).trim() + " "; + final boolean forceDeath = (sVmPolicy.mask & PENALTY_DEATH_ON_CLEARTEXT_NETWORK) != 0; + onVmPolicyViolation(new CleartextNetworkViolation(msg), forceDeath); } - /** - * @hide - */ + /** @hide */ public static void onUntaggedSocket() { - onVmPolicyViolation(null, new Throwable("Untagged socket detected; use" - + " TrafficStats.setThreadSocketTag() to track all network usage")); + 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(String message, Throwable originStack) { - onVmPolicyViolation(message, originStack, false); + /** @hide */ + public static void onVmPolicyViolation(Violation originStack) { + onVmPolicyViolation(originStack, false); } - /** - * @hide - */ - public static void onVmPolicyViolation(String message, Throwable originStack, - boolean forceDeath) { - final boolean penaltyDropbox = (sVmPolicyMask & PENALTY_DROPBOX) != 0; - final boolean penaltyDeath = ((sVmPolicyMask & PENALTY_DEATH) != 0) || forceDeath; - final boolean penaltyLog = (sVmPolicyMask & PENALTY_LOG) != 0; - final ViolationInfo info = new ViolationInfo(message, originStack, sVmPolicyMask); + /** @hide */ + 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(violation, sVmPolicy.mask); // Erase stuff not relevant for process-wide violations info.numAnimationsRunning = 0; @@ -2040,61 +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 && sListener != null) { - sListener.onViolation(originStack.toString()); - } - if (penaltyLog && timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { - Log.e(TAG, message, originStack); + if (timeSinceLastViolationMillis <= MIN_VM_INTERVAL_MS) { + // Rate limit all penalties. + return; } - int violationMaskSubset = PENALTY_DROPBOX | (ALL_VM_DETECT_BITS & sVmPolicyMask); - - 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 (penaltyLog && sLogger != null && timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { + sLogger.log(info); } - 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); + int violationMaskSubset = PENALTY_DROPBOX | (ALL_VM_DETECT_BITS & sVmPolicy.mask); + + 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); } } @@ -2103,11 +2023,29 @@ 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() - */ + /** Called from Parcel.writeNoException() */ /* package */ static void writeGatheredViolationsToParcel(Parcel p) { ArrayList<ViolationInfo> violations = gatheredViolations.get(); if (violations == null) { @@ -2125,28 +2063,19 @@ 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. + * 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) { - // Our own stack trace to append - StringWriter sw = new StringWriter(); - sw.append("# via Binder call with stack:\n"); - PrintWriter pw = new FastPrintWriter(sw, false, 256); - new LogStackTrace().printStackTrace(pw); - pw.flush(); - String ourStack = sw.toString(); - + Throwable localCallSite = new Throwable(); final int policyMask = getThreadPolicyMask(); final boolean currentlyGathering = (policyMask & PENALTY_GATHER) != 0; final int size = p.readInt(); for (int i = 0; i < size; i++) { final ViolationInfo info = new ViolationInfo(p, !currentlyGathering); - info.crashInfo.appendStackTrace(ourStack); + info.addLocalStack(localCallSite); BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); if (policy instanceof AndroidBlockGuardPolicy) { ((AndroidBlockGuardPolicy) policy).handleViolationWithTimingAttempt(info); @@ -2155,22 +2084,20 @@ public final class StrictMode { } /** - * Called from android_util_Binder.cpp's - * android_os_Parcel_enforceInterface when an incoming Binder call - * requires changing the StrictMode policy mask. The role of this - * function is to ask Binder for its current (native) thread-local - * policy value and synchronize it to libcore's (Java) - * thread-local policy value. + * Called from android_util_Binder.cpp's android_os_Parcel_enforceInterface when an incoming + * Binder call requires changing the StrictMode policy mask. The role of this function is to ask + * Binder for its current (native) thread-local policy value and synchronize it to libcore's + * (Java) thread-local policy value. */ private static void onBinderStrictModePolicyChange(int newPolicy) { setBlockGuardPolicy(newPolicy); } /** - * A tracked, critical time span. (e.g. during an animation.) + * A tracked, critical time span. (e.g. during an animation.) * - * The object itself is a linked list node, to avoid any allocations - * during rapid span entries and exits. + * <p>The object itself is a linked list node, to avoid any allocations during rapid span + * entries and exits. * * @hide */ @@ -2178,7 +2105,7 @@ public final class StrictMode { private String mName; private long mCreateMillis; private Span mNext; - private Span mPrev; // not used when in freeList, only active + private Span mPrev; // not used when in freeList, only active private final ThreadSpanState mContainerState; Span(ThreadSpanState threadState) { @@ -2191,12 +2118,10 @@ public final class StrictMode { } /** - * To be called when the critical span is complete (i.e. the - * animation is done animating). This can be called on any - * thread (even a different one from where the animation was - * taking place), but that's only a defensive implementation - * measure. It really makes no sense for you to call this on - * thread other than that where you created it. + * To be called when the critical span is complete (i.e. the animation is done animating). + * This can be called on any thread (even a different one from where the animation was + * taking place), but that's only a defensive implementation measure. It really makes no + * sense for you to call this on thread other than that where you created it. * * @hide */ @@ -2240,53 +2165,52 @@ public final class StrictMode { } // The no-op span that's used in user builds. - private static final Span NO_OP_SPAN = new Span() { - public void finish() { - // Do nothing. - } - }; + private static final Span NO_OP_SPAN = + new Span() { + public void finish() { + // Do nothing. + } + }; /** * Linked lists of active spans and a freelist. * - * Locking notes: there's one of these structures per thread and - * all members of this structure (as well as the Span nodes under - * it) are guarded by the ThreadSpanState object instance. While - * in theory there'd be no locking required because it's all local - * per-thread, the finish() method above is defensive against - * people calling it on a different thread from where they created - * the Span, hence the locking. + * <p>Locking notes: there's one of these structures per thread and all members of this + * structure (as well as the Span nodes under it) are guarded by the ThreadSpanState object + * instance. While in theory there'd be no locking required because it's all local per-thread, + * the finish() method above is defensive against people calling it on a different thread from + * where they created the Span, hence the locking. */ private static class ThreadSpanState { - public Span mActiveHead; // doubly-linked list. + public Span mActiveHead; // doubly-linked list. public int mActiveSize; - public Span mFreeListHead; // singly-linked list. only changes at head. + public Span mFreeListHead; // singly-linked list. only changes at head. public int mFreeListSize; } private static final ThreadLocal<ThreadSpanState> sThisThreadSpanState = new ThreadLocal<ThreadSpanState>() { - @Override protected ThreadSpanState initialValue() { - return new ThreadSpanState(); - } - }; + @Override + protected ThreadSpanState initialValue() { + return new ThreadSpanState(); + } + }; - private static Singleton<IWindowManager> sWindowManager = new Singleton<IWindowManager>() { - protected IWindowManager create() { - return IWindowManager.Stub.asInterface(ServiceManager.getService("window")); - } - }; + private static Singleton<IWindowManager> sWindowManager = + new Singleton<IWindowManager>() { + protected IWindowManager create() { + return IWindowManager.Stub.asInterface(ServiceManager.getService("window")); + } + }; /** * Enter a named critical span (e.g. an animation) * - * <p>The name is an arbitary label (or tag) that will be applied - * to any strictmode violation that happens while this span is - * active. You must call finish() on the span when done. + * <p>The name is an arbitary label (or tag) that will be applied to any strictmode violation + * that happens while this span is active. You must call finish() on the span when done. * - * <p>This will never return null, but on devices without debugging - * enabled, this may return a dummy object on which the finish() - * method is a no-op. + * <p>This will never return null, but on devices without debugging enabled, this may return a + * dummy object on which the finish() method is a no-op. * * <p>TODO: add CloseGuard to this, verifying callers call finish. * @@ -2325,13 +2249,11 @@ public final class StrictMode { } /** - * For code to note that it's slow. This is a no-op unless the - * current thread's {@link android.os.StrictMode.ThreadPolicy} has - * {@link android.os.StrictMode.ThreadPolicy.Builder#detectCustomSlowCalls} - * enabled. + * For code to note that it's slow. This is a no-op unless the current thread's {@link + * android.os.StrictMode.ThreadPolicy} has {@link + * android.os.StrictMode.ThreadPolicy.Builder#detectCustomSlowCalls} enabled. * - * @param name a short string for the exception stack trace that's - * built if when this fires. + * @param name a short string for the exception stack trace that's built if when this fires. */ public static void noteSlowCall(String name) { BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); @@ -2343,14 +2265,11 @@ public final class StrictMode { } /** - * For code to note that a resource was obtained using a type other than - * its defined type. This is a no-op unless the current thread's - * {@link android.os.StrictMode.ThreadPolicy} has - * {@link android.os.StrictMode.ThreadPolicy.Builder#detectResourceMismatches()} - * enabled. + * For code to note that a resource was obtained using a type other than its defined type. This + * is a no-op unless the current thread's {@link android.os.StrictMode.ThreadPolicy} has {@link + * android.os.StrictMode.ThreadPolicy.Builder#detectResourceMismatches()} enabled. * - * @param tag an object for the exception stack trace that's - * built if when this fires. + * @param tag an object for the exception stack trace that's built if when this fires. * @hide */ public static void noteResourceMismatch(Object tag) { @@ -2362,58 +2281,50 @@ public final class StrictMode { ((AndroidBlockGuardPolicy) policy).onResourceMismatch(tag); } - /** - * @hide - */ + /** @hide */ public static void noteUnbufferedIO() { BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); if (!(policy instanceof AndroidBlockGuardPolicy)) { // StrictMode not enabled. return; } - ((AndroidBlockGuardPolicy) policy).onUnbufferedIO(); + policy.onUnbufferedIO(); } - /** - * @hide - */ + /** @hide */ public static void noteDiskRead() { BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); if (!(policy instanceof AndroidBlockGuardPolicy)) { // StrictMode not enabled. return; } - ((AndroidBlockGuardPolicy) policy).onReadFromDisk(); + policy.onReadFromDisk(); } - /** - * @hide - */ + /** @hide */ public static void noteDiskWrite() { BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); if (!(policy instanceof AndroidBlockGuardPolicy)) { // StrictMode not enabled. return; } - ((AndroidBlockGuardPolicy) policy).onWriteToDisk(); + policy.onWriteToDisk(); } - // Guarded by StrictMode.class - private static final HashMap<Class, Integer> sExpectedActivityInstanceCount = - new HashMap<Class, Integer>(); + @GuardedBy("StrictMode.class") + private static final HashMap<Class, Integer> sExpectedActivityInstanceCount = new HashMap<>(); /** - * Returns an object that is used to track instances of activites. - * The activity should store a reference to the tracker object in one of its fields. + * Returns an object that is used to track instances of activites. The activity should store a + * reference to the tracker object in one of its fields. + * * @hide */ public static Object trackActivity(Object instance) { return new InstanceTracker(instance); } - /** - * @hide - */ + /** @hide */ public static void incrementExpectedActivityCount(Class klass) { if (klass == null) { return; @@ -2430,9 +2341,7 @@ public final class StrictMode { } } - /** - * @hide - */ + /** @hide */ public static void decrementExpectedActivityCount(Class klass) { if (klass == null) { return; @@ -2477,94 +2386,63 @@ public final class StrictMode { long instances = VMDebug.countInstancesOfClass(klass, false); if (instances > limit) { - Throwable tr = new InstanceCountViolation(klass, instances, limit); - onVmPolicyViolation(tr.getMessage(), tr); + onVmPolicyViolation(new InstanceCountViolation(klass, instances, limit)); } } /** - * Parcelable that gets sent in Binder call headers back to callers - * to report violations that happened during a cross-process call. + * Parcelable that gets sent in Binder call headers back to callers to report violations that + * happened during a cross-process call. * * @hide */ - public static class ViolationInfo implements Parcelable { - public final String message; + @TestApi + public static final class ViolationInfo implements Parcelable { + /** Stack and violation details. */ + private final Violation mViolation; - /** - * Stack and other stuff info. - */ - public final ApplicationErrorReport.CrashInfo crashInfo; + /** Path leading to a violation that occurred across binder. */ + private final Deque<StackTraceElement[]> mBinderStack = new ArrayDeque<>(); - /** - * The strict mode policy mask at the time of violation. - */ - public final int policy; + /** Memoized stack trace of full violation. */ + @Nullable private String mStackTrace; - /** - * The wall time duration of the violation, when known. -1 when - * not known. - */ + /** The strict mode policy mask at the time of violation. */ + private final int mPolicy; + + /** The wall time duration of the violation, when known. -1 when not known. */ public int durationMillis = -1; - /** - * The number of animations currently running. - */ + /** The number of animations currently running. */ public int numAnimationsRunning = 0; - /** - * List of tags from active Span instances during this - * violation, or null for none. - */ + /** List of tags from active Span instances during this violation, or null for none. */ public String[] tags; /** - * Which violation number this was (1-based) since the last Looper loop, - * from the perspective of the root caller (if it crossed any processes - * via Binder calls). The value is 0 if the root caller wasn't on a Looper - * thread. + * Which violation number this was (1-based) since the last Looper loop, from the + * perspective of the root caller (if it crossed any processes via Binder calls). The value + * is 0 if the root caller wasn't on a Looper thread. */ public int violationNumThisLoop; - /** - * The time (in terms of SystemClock.uptimeMillis()) that the - * violation occurred. - */ + /** The time (in terms of SystemClock.uptimeMillis()) that the violation occurred. */ public long violationUptimeMillis; /** - * The action of the Intent being broadcast to somebody's onReceive - * on this thread right now, or null. + * The action of the Intent being broadcast to somebody's onReceive on this thread right + * now, or null. */ public String broadcastIntentAction; - /** - * If this is a instance count violation, the number of instances in memory, - * else -1. - */ + /** 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() { - message = null; - crashInfo = null; - policy = 0; - } - - public ViolationInfo(Throwable tr, int policy) { - this(null, tr, policy); - } - - /** - * Create an instance of ViolationInfo initialized from an exception. - */ - public ViolationInfo(String message, Throwable tr, int policy) { - this.message = message; - crashInfo = new ApplicationErrorReport.CrashInfo(tr); + /** Create an instance of ViolationInfo initialized from an exception. */ + 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) { @@ -2572,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; @@ -2592,11 +2470,107 @@ public final class StrictMode { } } + /** Equivalent output to {@link ApplicationErrorReport.CrashInfo#stackTrace}. */ + public String getStackTrace() { + if (mStackTrace == null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new FastPrintWriter(sw, false, 256); + mViolation.printStackTrace(pw); + for (StackTraceElement[] traces : mBinderStack) { + pw.append("# via Binder call with stack:\n"); + for (StackTraceElement traceElement : traces) { + pw.append("\tat "); + pw.append(traceElement.toString()); + pw.append('\n'); + } + } + pw.flush(); + pw.close(); + mStackTrace = sw.toString(); + } + return mStackTrace; + } + + /** + * Optional message describing this violation. + * + * @hide + */ + @TestApi + public String getViolationDetails() { + return mViolation.getMessage(); + } + + /** + * Policy mask at time of violation. + * + * @hide + */ + @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. We + * only preserve the stack trace elements. + * + * @hide + */ + void addLocalStack(Throwable t) { + mBinderStack.addFirst(t.getStackTrace()); + } + + /** + * Retrieve the type of StrictMode violation. + * + * @hide + */ + @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 (crashInfo != null) { - result = 37 * result + crashInfo.stackTrace.hashCode(); + if (mViolation != null) { + result = 37 * result + mViolation.hashCode(); } if (numAnimationsRunning != 0) { result *= 37; @@ -2612,9 +2586,7 @@ public final class StrictMode { return result; } - /** - * Create an instance of ViolationInfo initialized from a Parcel. - */ + /** Create an instance of ViolationInfo initialized from a Parcel. */ public ViolationInfo(Parcel in) { this(in, false); } @@ -2622,21 +2594,30 @@ public final class StrictMode { /** * Create an instance of ViolationInfo initialized from a Parcel. * - * @param unsetGatheringBit if true, the caller is the root caller - * and the gathering penalty should be removed. + * @param unsetGatheringBit if true, the caller is the root caller and the gathering penalty + * should be removed. */ public ViolationInfo(Parcel in, boolean unsetGatheringBit) { - message = in.readString(); - if (in.readInt() != 0) { - crashInfo = new ApplicationErrorReport.CrashInfo(in); - } else { - crashInfo = null; + mViolation = (Violation) in.readSerializable(); + int binderStackSize = in.readInt(); + for (int i = 0; i < binderStackSize; i++) { + 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(); @@ -2647,20 +2628,22 @@ public final class StrictMode { tags = in.readStringArray(); } - /** - * Save a ViolationInfo instance to a parcel. - */ + /** Save a ViolationInfo instance to a parcel. */ @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(message); - if (crashInfo != null) { - dest.writeInt(1); - crashInfo.writeToParcel(dest, flags); - } else { - dest.writeInt(0); + dest.writeSerializable(mViolation); + dest.writeInt(mBinderStack.size()); + 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); @@ -2668,28 +2651,32 @@ public final class StrictMode { dest.writeLong(numInstances); dest.writeString(broadcastIntentAction); dest.writeStringArray(tags); - int total = dest.dataPosition()-start; - if (Binder.CHECK_PARCEL_SIZE && total > 10*1024) { - Slog.d(TAG, "VIO: policy=" + policy + " dur=" + durationMillis - + " numLoop=" + violationNumThisLoop - + " anim=" + numAnimationsRunning - + " uptime=" + violationUptimeMillis - + " numInst=" + numInstances); + int total = dest.dataPosition() - start; + if (Binder.CHECK_PARCEL_SIZE && total > 10 * 1024) { + Slog.d( + TAG, + "VIO: policy=" + + mPolicy + + " dur=" + + durationMillis + + " numLoop=" + + violationNumThisLoop + + " anim=" + + numAnimationsRunning + + " uptime=" + + violationUptimeMillis + + " numInst=" + + numInstances); Slog.d(TAG, "VIO: action=" + broadcastIntentAction); Slog.d(TAG, "VIO: tags=" + Arrays.toString(tags)); - Slog.d(TAG, "VIO: TOTAL BYTES WRITTEN: " + (dest.dataPosition()-start)); + Slog.d(TAG, "VIO: TOTAL BYTES WRITTEN: " + (dest.dataPosition() - start)); } } - - /** - * Dump a ViolationInfo instance to a Printer. - */ + /** Dump a ViolationInfo instance to a Printer. */ public void dump(Printer pw, String prefix) { - if (crashInfo != null) { - crashInfo.dump(pw, prefix); - } - pw.println(prefix + "policy: " + policy); + pw.println(prefix + "stackTrace: " + getStackTrace()); + pw.println(prefix + "policy: " + mPolicy); if (durationMillis != -1) { pw.println(prefix + "durationMillis: " + durationMillis); } @@ -2733,29 +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 { - final Class mClass; - final long mInstances; - 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); - mClass = klass; - mInstances = instances; - mLimit = limit; - } - } - private static final class InstanceTracker { private static final HashMap<Class<?>, Integer> sInstanceCounts = new HashMap<Class<?>, Integer>(); diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java index b3d76d73ac34..c52c22d60ade 100644 --- a/core/java/android/os/SystemClock.java +++ b/core/java/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/core/java/android/os/Temperature.java b/core/java/android/os/Temperature.java index 3e48493a7a81..8767731e748a 100644 --- a/core/java/android/os/Temperature.java +++ b/core/java/android/os/Temperature.java @@ -30,8 +30,8 @@ public class Temperature implements Parcelable { private int mType; public Temperature() { - mType = Integer.MIN_VALUE; - mValue = HardwarePropertiesManager.UNDEFINED_TEMPERATURE; + this(HardwarePropertiesManager.UNDEFINED_TEMPERATURE, + Integer.MIN_VALUE); } public Temperature(float value, int type) { diff --git a/core/java/android/os/UEventObserver.java b/core/java/android/os/UEventObserver.java index 5c80ca6559ae..69a3922585ce 100644 --- a/core/java/android/os/UEventObserver.java +++ b/core/java/android/os/UEventObserver.java @@ -108,7 +108,7 @@ public abstract class UEventObserver { * UEventObserver after this call. Repeated calls have no effect. */ public final void stopObserving() { - final UEventThread t = getThread(); + final UEventThread t = peekThread(); if (t != null) { t.removeObserver(this); } diff --git a/core/java/android/os/UpdateEngine.java b/core/java/android/os/UpdateEngine.java index ee0b6230fade..c6149bed9d6d 100644 --- a/core/java/android/os/UpdateEngine.java +++ b/core/java/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/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 794b3baf1932..61dd462f2e07 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/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>. * @@ -319,9 +331,28 @@ public class UserManager { public static final String DISALLOW_CONFIG_VPN = "no_config_vpn"; /** + * Specifies if date, time and timezone configuring is disallowed. + * + * <p>When restriction is set by device owners, it applies globally - i.e., it disables date, + * time and timezone setting on the entire device and all users will be affected. When it's set + * by profile owners, it's only applied to the managed user. + * <p>The default value is <code>false</code>. + * + * <p>This user restriction has no effect on managed profiles. + * <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_DATE_TIME = "no_config_date_time"; + + /** * Specifies if a user is disallowed from configuring Tethering * & portable hotspots. This can only be set by device owners and profile owners on the * primary user. The default value is <code>false</code>. + * <p>In Android 9.0 or higher, if tethering is enabled when this restriction is set, + * tethering will be automatically turned off. * * <p>Key for user restrictions. * <p>Type: Boolean @@ -495,8 +526,11 @@ public class UserManager { /** * Specifies if a user is disallowed from adjusting the master volume. If set, the master volume - * will be muted. This can be set by device owners and profile owners. The default value is - * <code>false</code>. + * will be muted. This can be set by device owners from API 21 and profile owners from API 24. + * The default value is <code>false</code>. + * + * <p>When the restriction is set by profile owners, then it only applies to relevant + * profiles. * * <p>This restriction has no effect on managed profiles. * <p>Key for user restrictions. @@ -569,6 +603,25 @@ public class UserManager { public static final String DISALLOW_CREATE_WINDOWS = "no_create_windows"; /** + * Specifies that system error dialogs for crashed or unresponsive apps should not be shown. + * In this case, the system will force-stop the app as if the user chooses the "close app" + * option on the UI. No feedback report will be collected as there is no way for the user to + * provide explicit consent. + * + * When this user restriction is set by device owners, it's applied to all users; when it's set + * by profile owners, it's only applied to the relevant profiles. + * The default value is <code>false</code>. + * + * <p>This user restriction has no effect on managed profiles. + * <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_SYSTEM_ERROR_DIALOGS = "no_system_error_dialogs"; + + /** * Specifies if what is copied in the clipboard of this profile can * be pasted in related profiles. Does not restrict if the clipboard of related profiles can be * pasted in this profile. @@ -751,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. * @@ -876,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() { @@ -886,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; } /** @@ -981,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) { @@ -1007,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 @@ -1026,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. @@ -2022,7 +2115,7 @@ public class UserManager { */ public void setQuietModeEnabled(@UserIdInt int userHandle, boolean enableQuietMode) { try { - mService.setQuietModeEnabled(userHandle, enableQuietMode); + mService.setQuietModeEnabled(userHandle, enableQuietMode, null); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } @@ -2049,10 +2142,14 @@ public class UserManager { * unlocking. If the user is already unlocked, we call through to {@link #setQuietModeEnabled} * directly. * - * @return true if the quiet mode was disabled immediately + * @param userHandle The user that is going to disable quiet mode. + * @param target The target to launch when the user is unlocked. + * @return {@code true} if quiet mode is disabled without showing confirm credentials screen, + * {@code false} otherwise. * @hide */ - public boolean trySetQuietModeDisabled(@UserIdInt int userHandle, IntentSender target) { + public boolean trySetQuietModeDisabled( + @UserIdInt int userHandle, @Nullable IntentSender target) { try { return mService.trySetQuietModeDisabled(userHandle, target); } catch (RemoteException re) { @@ -2257,6 +2354,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/core/java/android/os/UserManagerInternal.java b/core/java/android/os/UserManagerInternal.java index 17f00c24988d..9369eebfd984 100644 --- a/core/java/android/os/UserManagerInternal.java +++ b/core/java/android/os/UserManagerInternal.java @@ -154,11 +154,21 @@ public abstract class UserManagerInternal { public abstract boolean isUserUnlocked(int userId); /** - * Return whether the given user is running + * Returns whether the given user is running */ public abstract boolean isUserRunning(int userId); /** + * Returns whether the given user is initialized + */ + public abstract boolean isUserInitialized(int userId); + + /** + * Returns whether the given user exists + */ + public abstract boolean exists(int userId); + + /** * Set user's running state */ public abstract void setUserState(int userId, int userState); diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl index 50855bb349d9..3b53260e5dd7 100644 --- a/core/java/android/os/storage/IStorageManager.aidl +++ b/core/java/android/os/storage/IStorageManager.aidl @@ -17,6 +17,7 @@ package android.os.storage; import android.content.pm.IPackageMoveObserver; +import android.os.IVoldTaskListener; import android.os.ParcelFileDescriptor; import android.os.storage.DiskInfo; import android.os.storage.IStorageEventListener; @@ -45,103 +46,11 @@ interface IStorageManager { */ void unregisterListener(IStorageEventListener listener) = 1; /** - * Returns true if a USB mass storage host is connected - */ - boolean isUsbMassStorageConnected() = 2; - /** - * Enables / disables USB mass storage. The caller should check actual - * status of enabling/disabling USB mass storage via StorageEventListener. - */ - void setUsbMassStorageEnabled(boolean enable) = 3; - /** - * Returns true if a USB mass storage host is enabled (media is shared) - */ - boolean isUsbMassStorageEnabled() = 4; - /** - * Mount external storage at given mount point. Returns an int consistent - * with StorageResultCode - */ - int mountVolume(in String mountPoint) = 5; - /** - * Safely unmount external storage at given mount point. The unmount is an - * asynchronous operation. Applications should register StorageEventListener - * for storage related status changes. - * @param mountPoint the mount point - * @param force whether or not to forcefully unmount it (e.g. even if programs are using this - * data currently) - * @param removeEncryption whether or not encryption mapping should be removed from the volume. - * This value implies {@code force}. - */ - void unmountVolume(in String mountPoint, boolean force, boolean removeEncryption) = 6; - /** - * Format external storage given a mount point. Returns an int consistent - * with StorageResultCode - */ - int formatVolume(in String mountPoint) = 7; - /** - * Returns an array of pids with open files on the specified path. - */ - int[] getStorageUsers(in String path) = 8; - /** - * Gets the state of a volume via its mountpoint. - */ - String getVolumeState(in String mountPoint) = 9; - /* - * Creates a secure container with the specified parameters. Returns an int - * consistent with StorageResultCode - */ - int createSecureContainer(in String id, int sizeMb, in String fstype, in String key, - int ownerUid, boolean external) = 10; - /* - * Finalize a container which has just been created and populated. After - * finalization, the container is immutable. Returns an int consistent with - * StorageResultCode - */ - int finalizeSecureContainer(in String id) = 11; - /* - * Destroy a secure container, and free up all resources associated with it. - * NOTE: Ensure all references are released prior to deleting. Returns an - * int consistent with StorageResultCode - */ - int destroySecureContainer(in String id, boolean force) = 12; - /* - * Mount a secure container with the specified key and owner UID. Returns an - * int consistent with StorageResultCode - */ - int mountSecureContainer(in String id, in String key, int ownerUid, boolean readOnly) = 13; - /* - * Unount a secure container. Returns an int consistent with - * StorageResultCode - */ - int unmountSecureContainer(in String id, boolean force) = 14; - /* - * Returns true if the specified container is mounted - */ - boolean isSecureContainerMounted(in String id) = 15; - /* - * Rename an unmounted secure container. Returns an int consistent with - * StorageResultCode - */ - int renameSecureContainer(in String oldId, in String newId) = 16; - /* - * Returns the filesystem path of a mounted secure container. - */ - String getSecureContainerPath(in String id) = 17; - /** - * Gets an Array of currently known secure container IDs - */ - String[] getSecureContainerList() = 18; - /** * Shuts down the StorageManagerService and gracefully unmounts all external media. * Invokes call back once the shutdown is complete. */ void shutdown(IStorageShutdownObserver observer) = 19; /** - * Call into StorageManagerService by PackageManager to notify that its done - * processing the media status update request. - */ - void finishMediaUpdate() = 20; - /** * Mounts an Opaque Binary Blob (OBB) with the specified decryption key and * only allows the calling process's UID access to the contents. * StorageManagerService will call back to the supplied IObbActionListener to inform @@ -166,10 +75,6 @@ interface IStorageManager { */ String getMountedObbPath(in String rawPath) = 24; /** - * Returns whether or not the external storage is emulated. - */ - boolean isExternalStorageEmulated() = 25; - /** * Decrypts any encrypted volumes. */ int decryptStorage(in String password) = 26; @@ -186,14 +91,6 @@ interface IStorageManager { */ StorageVolume[] getVolumeList(int uid, in String packageName, int flags) = 29; /** - * Gets the path on the filesystem for the ASEC container itself. - * - * @param cid ASEC container ID - * @return path to filesystem or {@code null} if it's not found - * @throws RemoteException - */ - String getSecureContainerFilesystemPath(in String cid) = 30; - /** * Determines the encryption state of the volume. * @return a numerical value. See {@code ENCRYPTION_STATE_*} for possible * values. @@ -208,11 +105,6 @@ interface IStorageManager { * may only be called by the system process. */ int verifyEncryptionPassword(in String password) = 32; - /* - * Fix permissions in a container which has just been created and populated. - * Returns an int consistent with StorageResultCode - */ - int fixPermissionsSecureContainer(in String id, int gid, in String filename) = 33; /** * Ensure that all directories along given path exist, creating parent * directories as needed. Validates that given path is absolute and that it @@ -220,7 +112,7 @@ interface IStorageManager { * path belongs to a volume managed by vold, and that path is either * external storage data or OBB directory belonging to calling app. */ - int mkdirs(in String callingPkg, in String path) = 34; + void mkdirs(in String callingPkg, in String path) = 34; /** * Determines the type of the encryption password * @return PasswordType @@ -247,7 +139,6 @@ interface IStorageManager { * @return contents of field */ String getField(in String field) = 39; - int resizeSecureContainer(in String id, int sizeMb, in String key) = 40; /** * Report the time of the last maintenance operation such as fstrim. * @return Timestamp of the last maintenance operation, in the @@ -260,7 +151,6 @@ interface IStorageManager { * @throws RemoteException */ void runMaintenance() = 42; - void waitForAsecScan() = 43; DiskInfo[] getDisks() = 44; VolumeInfo[] getVolumes(int flags) = 45; VolumeRecord[] getVolumeRecords(int flags) = 46; @@ -276,7 +166,7 @@ interface IStorageManager { void forgetAllVolumes() = 56; String getPrimaryStorageUuid() = 57; void setPrimaryStorageUuid(in String volumeUuid, IPackageMoveObserver callback) = 58; - long benchmark(in String volId) = 59; + void benchmark(in String volId, IVoldTaskListener listener) = 59; void setDebugFlags(int flags, int mask) = 60; void createUserKey(int userId, int serialNumber, boolean ephemeral) = 61; void destroyUserKey(int userId) = 62; @@ -288,7 +178,7 @@ interface IStorageManager { boolean isConvertibleToFBE() = 68; void addUserKeyAuth(int userId, int serialNumber, in byte[] token, in byte[] secret) = 70; void fixateNewestUserKeyAuth(int userId) = 71; - void fstrim(int flags) = 72; + void fstrim(int flags, IVoldTaskListener listener) = 72; AppFuseMount mountProxyFileDescriptorBridge() = 73; ParcelFileDescriptor openProxyFileDescriptor(int mountPointId, int fileId, int mode) = 74; long getCacheQuotaBytes(String volumeUuid, int uid) = 75; @@ -296,4 +186,6 @@ interface IStorageManager { long getAllocatableBytes(String volumeUuid, int flags, String callingPackage) = 77; void allocateBytes(String volumeUuid, long bytes, int flags, String callingPackage) = 78; void secdiscard(in String path) = 79; + void runIdleMaintenance() = 80; + void abortIdleMaintenance() = 81; } diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 8533c7efad84..4796712f2d98 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -41,10 +41,13 @@ import android.os.Binder; 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; @@ -86,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; /** @@ -219,27 +224,31 @@ public class StorageManager { public static final int FLAG_INCLUDE_INVISIBLE = 1 << 10; /** {@hide} */ - public static final int FSTRIM_FLAG_DEEP = 1 << 0; - /** {@hide} */ - public static final int FSTRIM_FLAG_BENCHMARK = 1 << 1; + public static final int FSTRIM_FLAG_DEEP = IVold.FSTRIM_FLAG_DEEP_TRIM; /** @hide The volume is not encrypted. */ - public static final int ENCRYPTION_STATE_NONE = 1; + public static final int ENCRYPTION_STATE_NONE = + IVold.ENCRYPTION_STATE_NONE; /** @hide The volume has been encrypted succesfully. */ - public static final int ENCRYPTION_STATE_OK = 0; + public static final int ENCRYPTION_STATE_OK = + IVold.ENCRYPTION_STATE_OK; - /** @hide The volume is in a bad state.*/ - public static final int ENCRYPTION_STATE_ERROR_UNKNOWN = -1; + /** @hide The volume is in a bad state. */ + public static final int ENCRYPTION_STATE_ERROR_UNKNOWN = + IVold.ENCRYPTION_STATE_ERROR_UNKNOWN; /** @hide Encryption is incomplete */ - public static final int ENCRYPTION_STATE_ERROR_INCOMPLETE = -2; + public static final int ENCRYPTION_STATE_ERROR_INCOMPLETE = + IVold.ENCRYPTION_STATE_ERROR_INCOMPLETE; /** @hide Encryption is incomplete and irrecoverable */ - public static final int ENCRYPTION_STATE_ERROR_INCONSISTENT = -3; + public static final int ENCRYPTION_STATE_ERROR_INCONSISTENT = + IVold.ENCRYPTION_STATE_ERROR_INCONSISTENT; /** @hide Underlying data is corrupt */ - public static final int ENCRYPTION_STATE_ERROR_CORRUPT = -4; + public static final int ENCRYPTION_STATE_ERROR_CORRUPT = + IVold.ENCRYPTION_STATE_ERROR_CORRUPT; private static volatile IStorageManager sStorageManager = null; @@ -879,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 { - return mStorageManager.benchmark(volId); + // 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 { + mStorageManager.benchmark(volId, listener); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1091,6 +1123,15 @@ public class StorageManager { return FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace()); } + /** {@hide} */ + public void mkdirs(File file) { + try { + mStorageManager.mkdirs(mContext.getOpPackageName(), file.getAbsolutePath()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** @removed */ public @NonNull StorageVolume[] getVolumeList() { return getVolumeList(mContext.getUserId(), 0); @@ -1973,13 +2014,13 @@ public class StorageManager { /// Consts to match the password types in cryptfs.h /** @hide */ - public static final int CRYPT_TYPE_PASSWORD = 0; + public static final int CRYPT_TYPE_PASSWORD = IVold.PASSWORD_TYPE_PASSWORD; /** @hide */ - public static final int CRYPT_TYPE_DEFAULT = 1; + public static final int CRYPT_TYPE_DEFAULT = IVold.PASSWORD_TYPE_DEFAULT; /** @hide */ - public static final int CRYPT_TYPE_PATTERN = 2; + public static final int CRYPT_TYPE_PATTERN = IVold.PASSWORD_TYPE_PATTERN; /** @hide */ - public static final int CRYPT_TYPE_PIN = 3; + public static final int CRYPT_TYPE_PIN = IVold.PASSWORD_TYPE_PIN; // Constants for the data available via StorageManagerService.getField. /** @hide */ diff --git a/core/java/android/os/storage/VolumeInfo.java b/core/java/android/os/storage/VolumeInfo.java index a21e05e314b2..76f79f13d9a7 100644 --- a/core/java/android/os/storage/VolumeInfo.java +++ b/core/java/android/os/storage/VolumeInfo.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Environment; +import android.os.IVold; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; @@ -75,24 +76,24 @@ public class VolumeInfo implements Parcelable { /** Real volume representing internal emulated storage */ public static final String ID_EMULATED_INTERNAL = "emulated"; - public static final int TYPE_PUBLIC = 0; - public static final int TYPE_PRIVATE = 1; - public static final int TYPE_EMULATED = 2; - public static final int TYPE_ASEC = 3; - public static final int TYPE_OBB = 4; - - public static final int STATE_UNMOUNTED = 0; - public static final int STATE_CHECKING = 1; - public static final int STATE_MOUNTED = 2; - public static final int STATE_MOUNTED_READ_ONLY = 3; - public static final int STATE_FORMATTING = 4; - public static final int STATE_EJECTING = 5; - public static final int STATE_UNMOUNTABLE = 6; - public static final int STATE_REMOVED = 7; - public static final int STATE_BAD_REMOVAL = 8; - - public static final int MOUNT_FLAG_PRIMARY = 1 << 0; - public static final int MOUNT_FLAG_VISIBLE = 1 << 1; + public static final int TYPE_PUBLIC = IVold.VOLUME_TYPE_PUBLIC; + public static final int TYPE_PRIVATE = IVold.VOLUME_TYPE_PRIVATE; + public static final int TYPE_EMULATED = IVold.VOLUME_TYPE_EMULATED; + public static final int TYPE_ASEC = IVold.VOLUME_TYPE_ASEC; + public static final int TYPE_OBB = IVold.VOLUME_TYPE_OBB; + + public static final int STATE_UNMOUNTED = IVold.VOLUME_STATE_UNMOUNTED; + public static final int STATE_CHECKING = IVold.VOLUME_STATE_CHECKING; + public static final int STATE_MOUNTED = IVold.VOLUME_STATE_MOUNTED; + public static final int STATE_MOUNTED_READ_ONLY = IVold.VOLUME_STATE_MOUNTED_READ_ONLY; + public static final int STATE_FORMATTING = IVold.VOLUME_STATE_FORMATTING; + public static final int STATE_EJECTING = IVold.VOLUME_STATE_EJECTING; + public static final int STATE_UNMOUNTABLE = IVold.VOLUME_STATE_UNMOUNTABLE; + public static final int STATE_REMOVED = IVold.VOLUME_STATE_REMOVED; + public static final int STATE_BAD_REMOVAL = IVold.VOLUME_STATE_BAD_REMOVAL; + + public static final int MOUNT_FLAG_PRIMARY = IVold.MOUNT_FLAG_PRIMARY; + public static final int MOUNT_FLAG_VISIBLE = IVold.MOUNT_FLAG_VISIBLE; private static SparseArray<String> sStateToEnvironment = new SparseArray<>(); private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>(); diff --git a/core/java/android/os/strictmode/CleartextNetworkViolation.java b/core/java/android/os/strictmode/CleartextNetworkViolation.java new file mode 100644 index 000000000000..6a0d381d9b14 --- /dev/null +++ b/core/java/android/os/strictmode/CleartextNetworkViolation.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 CleartextNetworkViolation extends Violation { + /** @hide */ + public CleartextNetworkViolation(String msg) { + super(msg); + } +} diff --git a/core/java/android/os/strictmode/ContentUriWithoutPermissionViolation.java b/core/java/android/os/strictmode/ContentUriWithoutPermissionViolation.java new file mode 100644 index 000000000000..e78dc79d9628 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/CustomViolation.java b/core/java/android/os/strictmode/CustomViolation.java new file mode 100644 index 000000000000..d4ad06715004 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/DiskReadViolation.java b/core/java/android/os/strictmode/DiskReadViolation.java new file mode 100644 index 000000000000..fad32dbf89ed --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/DiskWriteViolation.java b/core/java/android/os/strictmode/DiskWriteViolation.java new file mode 100644 index 000000000000..cb9ca3815fe1 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/FileUriExposedViolation.java b/core/java/android/os/strictmode/FileUriExposedViolation.java new file mode 100644 index 000000000000..e3e6f8334a98 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/InstanceCountViolation.java b/core/java/android/os/strictmode/InstanceCountViolation.java new file mode 100644 index 000000000000..9ee2c8e5dc26 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/IntentReceiverLeakedViolation.java b/core/java/android/os/strictmode/IntentReceiverLeakedViolation.java new file mode 100644 index 000000000000..f416c94034c3 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/LeakedClosableViolation.java b/core/java/android/os/strictmode/LeakedClosableViolation.java new file mode 100644 index 000000000000..c795a6b89ec0 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/NetworkViolation.java b/core/java/android/os/strictmode/NetworkViolation.java new file mode 100644 index 000000000000..abcf009dd98b --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/ResourceMismatchViolation.java b/core/java/android/os/strictmode/ResourceMismatchViolation.java new file mode 100644 index 000000000000..97c449938ede --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/ServiceConnectionLeakedViolation.java b/core/java/android/os/strictmode/ServiceConnectionLeakedViolation.java new file mode 100644 index 000000000000..2d6b58f031c8 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/SqliteObjectLeakedViolation.java b/core/java/android/os/strictmode/SqliteObjectLeakedViolation.java new file mode 100644 index 000000000000..020022070475 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/UnbufferedIoViolation.java b/core/java/android/os/strictmode/UnbufferedIoViolation.java new file mode 100644 index 000000000000..a5c326d1b98e --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/UntaggedSocketViolation.java b/core/java/android/os/strictmode/UntaggedSocketViolation.java new file mode 100644 index 000000000000..836a8b9dc633 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/Violation.java b/core/java/android/os/strictmode/Violation.java new file mode 100644 index 000000000000..31c7d584fd65 --- /dev/null +++ b/core/java/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/core/java/android/os/strictmode/WebViewMethodCalledOnWrongThreadViolation.java b/core/java/android/os/strictmode/WebViewMethodCalledOnWrongThreadViolation.java new file mode 100644 index 000000000000..c328d1470b53 --- /dev/null +++ b/core/java/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/core/java/android/preference/PreferenceFragment.java b/core/java/android/preference/PreferenceFragment.java index 73fa01e59201..4c556efaecd0 100644 --- a/core/java/android/preference/PreferenceFragment.java +++ b/core/java/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/core/java/android/print/PrintAttributes.java b/core/java/android/print/PrintAttributes.java index 17942aea960a..ce5b11ee33f1 100644 --- a/core/java/android/print/PrintAttributes.java +++ b/core/java/android/print/PrintAttributes.java @@ -26,6 +26,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources.NotFoundException; import android.os.Parcel; import android.os.Parcelable; +import android.service.print.PrintAttributesProto; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -54,9 +55,9 @@ public final class PrintAttributes implements Parcelable { @interface ColorMode { } /** Color mode: Monochrome color scheme, for example one color is used. */ - public static final int COLOR_MODE_MONOCHROME = 1 << 0; + public static final int COLOR_MODE_MONOCHROME = PrintAttributesProto.COLOR_MODE_MONOCHROME; /** Color mode: Color color scheme, for example many colors are used. */ - public static final int COLOR_MODE_COLOR = 1 << 1; + public static final int COLOR_MODE_COLOR = PrintAttributesProto.COLOR_MODE_COLOR; private static final int VALID_COLOR_MODES = COLOR_MODE_MONOCHROME | COLOR_MODE_COLOR; @@ -69,11 +70,11 @@ public final class PrintAttributes implements Parcelable { @interface DuplexMode { } /** Duplex mode: No duplexing. */ - public static final int DUPLEX_MODE_NONE = 1 << 0; + public static final int DUPLEX_MODE_NONE = PrintAttributesProto.DUPLEX_MODE_NONE; /** Duplex mode: Pages are turned sideways along the long edge - like a book. */ - public static final int DUPLEX_MODE_LONG_EDGE = 1 << 1; + public static final int DUPLEX_MODE_LONG_EDGE = PrintAttributesProto.DUPLEX_MODE_LONG_EDGE; /** Duplex mode: Pages are turned upwards along the short edge - like a notpad. */ - public static final int DUPLEX_MODE_SHORT_EDGE = 1 << 2; + public static final int DUPLEX_MODE_SHORT_EDGE = PrintAttributesProto.DUPLEX_MODE_SHORT_EDGE; private static final int VALID_DUPLEX_MODES = DUPLEX_MODE_NONE | DUPLEX_MODE_LONG_EDGE | DUPLEX_MODE_SHORT_EDGE; diff --git a/core/java/android/print/PrintJobInfo.java b/core/java/android/print/PrintJobInfo.java index 3d094f7d09f4..94686a8ee456 100644 --- a/core/java/android/print/PrintJobInfo.java +++ b/core/java/android/print/PrintJobInfo.java @@ -28,6 +28,7 @@ import android.content.res.Resources; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.service.print.PrintJobInfoProto; import com.android.internal.util.Preconditions; @@ -88,7 +89,7 @@ public final class PrintJobInfo implements Parcelable { * Next valid states: {@link #STATE_QUEUED} * </p> */ - public static final int STATE_CREATED = 1; + public static final int STATE_CREATED = PrintJobInfoProto.STATE_CREATED; /** * Print job state: The print jobs is created, it is ready @@ -98,7 +99,7 @@ public final class PrintJobInfo implements Parcelable { * {@link #STATE_CANCELED} * </p> */ - public static final int STATE_QUEUED = 2; + public static final int STATE_QUEUED = PrintJobInfoProto.STATE_QUEUED; /** * Print job state: The print job is being printed. @@ -107,7 +108,7 @@ public final class PrintJobInfo implements Parcelable { * {@link #STATE_CANCELED}, {@link #STATE_BLOCKED} * </p> */ - public static final int STATE_STARTED = 3; + public static final int STATE_STARTED = PrintJobInfoProto.STATE_STARTED; /** * Print job state: The print job is blocked. @@ -116,7 +117,7 @@ public final class PrintJobInfo implements Parcelable { * {@link #STATE_STARTED} * </p> */ - public static final int STATE_BLOCKED = 4; + public static final int STATE_BLOCKED = PrintJobInfoProto.STATE_BLOCKED; /** * Print job state: The print job is successfully printed. @@ -125,7 +126,7 @@ public final class PrintJobInfo implements Parcelable { * Next valid states: None * </p> */ - public static final int STATE_COMPLETED = 5; + public static final int STATE_COMPLETED = PrintJobInfoProto.STATE_COMPLETED; /** * Print job state: The print job was printing but printing failed. @@ -133,7 +134,7 @@ public final class PrintJobInfo implements Parcelable { * Next valid states: {@link #STATE_CANCELED}, {@link #STATE_STARTED} * </p> */ - public static final int STATE_FAILED = 6; + public static final int STATE_FAILED = PrintJobInfoProto.STATE_FAILED; /** * Print job state: The print job is canceled. @@ -142,7 +143,7 @@ public final class PrintJobInfo implements Parcelable { * Next valid states: None * </p> */ - public static final int STATE_CANCELED = 7; + public static final int STATE_CANCELED = PrintJobInfoProto.STATE_CANCELED; /** The unique print job id. */ private PrintJobId mId; diff --git a/core/java/android/print/PrinterInfo.java b/core/java/android/print/PrinterInfo.java index f99b185229bc..88feab7fc2ca 100644 --- a/core/java/android/print/PrinterInfo.java +++ b/core/java/android/print/PrinterInfo.java @@ -31,6 +31,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Parcel; import android.os.Parcelable; +import android.service.print.PrinterInfoProto; import android.text.TextUtils; import com.android.internal.util.Preconditions; @@ -59,13 +60,13 @@ public final class PrinterInfo implements Parcelable { public @interface Status { } /** Printer status: the printer is idle and ready to print. */ - public static final int STATUS_IDLE = 1; + public static final int STATUS_IDLE = PrinterInfoProto.STATUS_IDLE; /** Printer status: the printer is busy printing. */ - public static final int STATUS_BUSY = 2; + public static final int STATUS_BUSY = PrinterInfoProto.STATUS_BUSY; /** Printer status: the printer is not available. */ - public static final int STATUS_UNAVAILABLE = 3; + public static final int STATUS_UNAVAILABLE = PrinterInfoProto.STATUS_UNAVAILABLE; private final @NonNull PrinterId mId; diff --git a/core/java/android/provider/AlarmClock.java b/core/java/android/provider/AlarmClock.java index f9030124cc63..21694575631a 100644 --- a/core/java/android/provider/AlarmClock.java +++ b/core/java/android/provider/AlarmClock.java @@ -158,7 +158,6 @@ public final class AlarmClock { * <p> * Dismiss all currently expired timers. If there are no expired timers, then this is a no-op. * </p> - * @hide */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_DISMISS_TIMER = "android.intent.action.DISMISS_TIMER"; diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index cc1c0677441e..ec5b1c668a27 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -22,6 +22,7 @@ import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; import android.app.Activity; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; @@ -42,10 +43,12 @@ import android.database.DatabaseUtils; import android.graphics.Rect; import android.net.Uri; import android.os.RemoteException; +import android.telecom.PhoneAccountHandle; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Pair; import android.view.View; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -4237,6 +4240,25 @@ public final class ContactsContract { * current carrier. An allowed bitmask of {@link #CARRIER_PRESENCE}. */ public static final int CARRIER_PRESENCE_VT_CAPABLE = 0x01; + + /** + * The flattened {@link android.content.ComponentName} of a {@link + * android.telecom.PhoneAccountHandle} that is the preferred {@code PhoneAccountHandle} to + * call the contact with. Used by {@link CommonDataKinds.Phone}. + * + * @see PhoneAccountHandle#getComponentName() + * @see ComponentName#flattenToString() + */ + String PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME = "preferred_phone_account_component_name"; + + /** + * The ID of a {@link + * android.telecom.PhoneAccountHandle} that is the preferred {@code PhoneAccountHandle} to + * call the contact with. Used by {@link CommonDataKinds.Phone}. + * + * @see PhoneAccountHandle#getId() () + */ + String PREFERRED_PHONE_ACCOUNT_ID = "preferred_phone_account_id"; } /** diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index ad4ec7248a81..99fcdad4fc94 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -188,9 +188,6 @@ public final class DocumentsContract { /** {@hide} */ public static final String METADATA_EXIF = "android:documentExif"; - /** {@hide} */ - public static final String EXTRA_METADATA_TAGS = "android:documentMetadataTags"; - /** * Constants related to a document, including {@link Cursor} column names * and flags. @@ -1397,38 +1394,42 @@ public final class DocumentsContract { /** * Returns metadata associated with the document. The type of metadata returned - * is specific to the document type. For example image files will largely return EXIF - * metadata. - * - * <p>The returned {@link Bundle} will contain zero or more entries. - * <p>Each entry represents a specific type of metadata. + * is specific to the document type. For example the data returned for an image + * file will likely consist primarily or soley of EXIF metadata. * - * <p>if tags == null, then a list of default tags will be used. + * <p>The returned {@link Bundle} will contain zero or more entries depending + * on the type of data supported by the document provider. * - * @param documentUri a Document URI - * @param tags an array of keys to choose which data are added to the Bundle. If the Document - * is a JPG or ExifInterface compatible, send keys from {@link ExifInterface}. - * If tags are null, a set of default tags will be used. If the tags don't - * match with any relevant data, they will not be added to the Bundle. - * @return a Bundle of Bundles. If metadata exists within the Bundle, there will also - * be a String under DocumentsContract.METADATA_TYPES that will return a String[] of the - * types of metadata gathered. + * <ol> + * <li>A {@link DocumentsContract.METADATA_TYPES} containing a {@code String[]} value. + * The string array identifies the type or types of metadata returned. Each + * value in the can be used to access a {@link Bundle} of data + * containing that type of data. + * <li>An entry each for each type of returned metadata. Each set of metadata is + * itself represented as a bundle and accessible via a string key naming + * the type of data. + * </ol> * - * <pre><code> - * Bundle metadata = DocumentsContract.getDocumentMetadata(resolver, imageDocUri, tags); - * int imageLength = metadata.getInt(ExifInterface.TAG_IMAGE_LENGTH); + * <p>Example: + * <p><pre><code> + * Bundle metadata = DocumentsContract.getDocumentMetadata(client, imageDocUri, tags); + * if (metadata.containsKey(DocumentsContract.METADATA_EXIF)) { + * Bundle exif = metadata.getBundle(DocumentsContract.METADATA_EXIF); + * int imageLength = exif.getInt(ExifInterface.TAG_IMAGE_LENGTH); + * } * </code></pre> * + * @param documentUri a Document URI + * @return a Bundle of Bundles. * {@hide} */ - public static Bundle getDocumentMetadata(ContentResolver resolver, Uri documentUri, - @Nullable String[] tags) + public static Bundle getDocumentMetadata(ContentResolver resolver, Uri documentUri) throws FileNotFoundException { final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( documentUri.getAuthority()); try { - return getDocumentMetadata(client, documentUri, tags); + return getDocumentMetadata(client, documentUri); } catch (Exception e) { Log.w(TAG, "Failed to get document metadata"); rethrowIfNecessary(resolver, e); @@ -1440,35 +1441,39 @@ public final class DocumentsContract { /** * Returns metadata associated with the document. The type of metadata returned - * is specific to the document type. For example image files will largely return EXIF - * metadata. - * - * <p>The returned {@link Bundle} will contain zero or more entries. - * <p>Each entry represents a specific type of metadata. + * is specific to the document type. For example the data returned for an image + * file will likely consist primarily or soley of EXIF metadata. * - * <p>if tags == null, then a list of default tags will be used. + * <p>The returned {@link Bundle} will contain zero or more entries depending + * on the type of data supported by the document provider. * - * @param documentUri a Document URI - * @param tags an array of keys to choose which data are added to the Bundle. If the Document - * is a JPG or ExifInterface compatible, send keys from {@link ExifInterface}. - * If tags are null, a set of default tags will be used. If the tags don't - * match with any relevant data, they will not be added to the Bundle. - * @return a Bundle of Bundles. If metadata exists within the Bundle, there will also - * be a String under DocumentsContract.METADATA_TYPES that will return a String[] of the - * types of metadata gathered. + * <ol> + * <li>A {@link DocumentsContract.METADATA_TYPES} containing a {@code String[]} value. + * The string array identifies the type or types of metadata returned. Each + * value in the can be used to access a {@link Bundle} of data + * containing that type of data. + * <li>An entry each for each type of returned metadata. Each set of metadata is + * itself represented as a bundle and accessible via a string key naming + * the type of data. + * </ol> * - * <pre><code> + * <p>Example: + * <p><pre><code> * Bundle metadata = DocumentsContract.getDocumentMetadata(client, imageDocUri, tags); - * int imageLength = metadata.getInt(ExifInterface.TAG_IMAGE_LENGTH); + * if (metadata.containsKey(DocumentsContract.METADATA_EXIF)) { + * Bundle exif = metadata.getBundle(DocumentsContract.METADATA_EXIF); + * int imageLength = exif.getInt(ExifInterface.TAG_IMAGE_LENGTH); + * } * </code></pre> * + * @param documentUri a Document URI + * @return a Bundle of Bundles. * {@hide} */ - public static Bundle getDocumentMetadata(ContentProviderClient client, - Uri documentUri, @Nullable String[] tags) throws RemoteException { + public static Bundle getDocumentMetadata( + ContentProviderClient client, Uri documentUri) throws RemoteException { final Bundle in = new Bundle(); in.putParcelable(EXTRA_URI, documentUri); - in.putStringArray(EXTRA_METADATA_TAGS, tags); final Bundle out = client.call(METHOD_GET_DOCUMENT_METADATA, null, in); diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 4bdcdb097df6..81b1921dd809 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -627,7 +627,7 @@ public abstract class DocumentsProvider extends ContentProvider { } /** {@hide} */ - public @Nullable Bundle getDocumentMetadata(String documentId, @Nullable String[] tags) + public @Nullable Bundle getDocumentMetadata(String documentId) throws FileNotFoundException { throw new UnsupportedOperationException("Metadata not supported"); } @@ -685,7 +685,9 @@ public abstract class DocumentsProvider extends ContentProvider { * @see ParcelFileDescriptor#parseMode(String) */ public abstract ParcelFileDescriptor openDocument( - String documentId, String mode, CancellationSignal signal) throws FileNotFoundException; + String documentId, + String mode, + @Nullable CancellationSignal signal) throws FileNotFoundException; /** * Open and return a thumbnail of the requested document. @@ -1142,8 +1144,7 @@ public abstract class DocumentsProvider extends ContentProvider { out.putParcelable(DocumentsContract.EXTRA_RESULT, path); } else if (METHOD_GET_DOCUMENT_METADATA.equals(method)) { - return getDocumentMetadata( - documentId, extras.getStringArray(DocumentsContract.EXTRA_METADATA_TAGS)); + return getDocumentMetadata(documentId); } else { throw new UnsupportedOperationException("Method not supported " + method); } diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 13e1e26b51c3..32d68cd9f869 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/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/core/java/android/provider/MetadataReader.java b/core/java/android/provider/MetadataReader.java index 2d1fca029c1c..4f3a7d68d40c 100644 --- a/core/java/android/provider/MetadataReader.java +++ b/core/java/android/provider/MetadataReader.java @@ -36,27 +36,33 @@ import java.util.Map; */ public final class MetadataReader { - private MetadataReader() { - } + private MetadataReader() {} private static final String[] DEFAULT_EXIF_TAGS = { - ExifInterface.TAG_IMAGE_WIDTH, - ExifInterface.TAG_IMAGE_LENGTH, + ExifInterface.TAG_APERTURE, + ExifInterface.TAG_COPYRIGHT, ExifInterface.TAG_DATETIME, + ExifInterface.TAG_EXPOSURE_TIME, + ExifInterface.TAG_FOCAL_LENGTH, + ExifInterface.TAG_F_NUMBER, ExifInterface.TAG_GPS_LATITUDE, + ExifInterface.TAG_GPS_LATITUDE_REF, ExifInterface.TAG_GPS_LONGITUDE, + ExifInterface.TAG_GPS_LONGITUDE_REF, + ExifInterface.TAG_IMAGE_LENGTH, + ExifInterface.TAG_IMAGE_WIDTH, + ExifInterface.TAG_ISO_SPEED_RATINGS, ExifInterface.TAG_MAKE, ExifInterface.TAG_MODEL, - ExifInterface.TAG_APERTURE, - ExifInterface.TAG_SHUTTER_SPEED_VALUE + ExifInterface.TAG_ORIENTATION, + ExifInterface.TAG_SHUTTER_SPEED_VALUE, }; - private static final Map<String, Integer> TYPE_MAPPING = new HashMap<>(); - private static final String[] ALL_KNOWN_EXIF_KEYS; private static final int TYPE_INT = 0; private static final int TYPE_DOUBLE = 1; private static final int TYPE_STRING = 2; + private static final Map<String, Integer> TYPE_MAPPING = new HashMap<>(); static { // TODO: Move this over to ExifInterface.java // Since each ExifInterface item has a type, and there's currently no way to get the type @@ -166,9 +172,9 @@ public final class MetadataReader { TYPE_MAPPING.put(ExifInterface.TAG_GPS_DIFFERENTIAL, TYPE_INT); TYPE_MAPPING.put(ExifInterface.TAG_GPS_IMG_DIRECTION, TYPE_DOUBLE); TYPE_MAPPING.put(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, TYPE_STRING); - TYPE_MAPPING.put(ExifInterface.TAG_GPS_LATITUDE, TYPE_DOUBLE); + TYPE_MAPPING.put(ExifInterface.TAG_GPS_LATITUDE, TYPE_STRING); TYPE_MAPPING.put(ExifInterface.TAG_GPS_LATITUDE_REF, TYPE_STRING); - TYPE_MAPPING.put(ExifInterface.TAG_GPS_LONGITUDE, TYPE_DOUBLE); + TYPE_MAPPING.put(ExifInterface.TAG_GPS_LONGITUDE, TYPE_STRING); TYPE_MAPPING.put(ExifInterface.TAG_GPS_LONGITUDE_REF, TYPE_STRING); TYPE_MAPPING.put(ExifInterface.TAG_GPS_MAP_DATUM, TYPE_STRING); TYPE_MAPPING.put(ExifInterface.TAG_GPS_MEASURE_MODE, TYPE_STRING); @@ -196,11 +202,19 @@ public final class MetadataReader { TYPE_MAPPING.put(ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, TYPE_INT); TYPE_MAPPING.put(ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, TYPE_INT); TYPE_MAPPING.put(ExifInterface.TAG_RW2_ISO, TYPE_INT); - ALL_KNOWN_EXIF_KEYS = TYPE_MAPPING.keySet().toArray(new String[TYPE_MAPPING.size()]); } private static final String JPG_MIME_TYPE = "image/jpg"; private static final String JPEG_MIME_TYPE = "image/jpeg"; + + /** + * Returns true if caller can generally expect to get metadata results + * for the supplied mimetype. + */ + public static boolean isSupportedMimeType(String mimeType) { + return JPG_MIME_TYPE.equals(mimeType) || JPEG_MIME_TYPE.equals(mimeType); + } + /** * Generic metadata retrieval method that can retrieve any available metadata from a given doc * Currently only functions for exifdata @@ -209,25 +223,14 @@ public final class MetadataReader { * @param stream InputStream containing a file * @param mimeType type of the given file * @param tags a variable amount of keys to differentiate which tags the user wants - * if null, returns a default set of data from the following keys: - * Exif data: - * ExifInterface.TAG_IMAGE_WIDTH, - * ExifInterface.TAG_IMAGE_LENGTH, - * ExifInterface.TAG_DATETIME, - * ExifInterface.TAG_GPS_LATITUDE, - * ExifInterface.TAG_GPS_LONGITUDE, - * ExifInterface.TAG_MAKE, - * ExifInterface.TAG_MODEL, - * ExifInterface.TAG_APERTURE, - * ExifInterface.TAG_SHUTTER_SPEED_VALUE + * if null, returns a default set of data. See {@link DEFAULT_EXIF_TAGS}. * @throws IOException when the file doesn't exist */ public static void getMetadata(Bundle metadata, InputStream stream, String mimeType, @Nullable String[] tags) throws IOException { - List<String> metadataTypes = new ArrayList(); - if (mimeType.equals(JPG_MIME_TYPE) || mimeType.equals(JPEG_MIME_TYPE)) { - ExifInterface exifInterface = new ExifInterface(stream); - Bundle exifData = getExifData(exifInterface, tags); + List<String> metadataTypes = new ArrayList<>(); + if (isSupportedMimeType(mimeType)) { + Bundle exifData = getExifData(stream, tags); if (exifData.size() > 0) { metadata.putBundle(DocumentsContract.METADATA_EXIF, exifData); metadataTypes.add(DocumentsContract.METADATA_EXIF); @@ -242,41 +245,33 @@ public final class MetadataReader { /** * Helper method that is called if getMetadata is called for an image mimeType. * - * @param exif the bundle to which we add exif data. - * @param exifInterface an ExifInterface for an image + * @param stream the input stream from which to extra data. * @param tags a list of ExifInterface tags that are used to retrieve data. - * if null, returns a default set of data from the following keys: - * ExifInterface.TAG_IMAGE_WIDTH, - * ExifInterface.TAG_IMAGE_LENGTH, - * ExifInterface.TAG_DATETIME, - * ExifInterface.TAG_GPS_LATITUDE, - * ExifInterface.TAG_GPS_LONGITUDE, - * ExifInterface.TAG_MAKE, - * ExifInterface.TAG_MODEL, - * ExifInterface.TAG_APERTURE, - * ExifInterface.TAG_SHUTTER_SPEED_VALUE + * if null, returns a default set of data. See {@link DEFAULT_EXIF_TAGS}. */ - private static Bundle getExifData(ExifInterface exifInterface, @Nullable String[] tags) + private static Bundle getExifData(InputStream stream, @Nullable String[] tags) throws IOException { if (tags == null) { tags = DEFAULT_EXIF_TAGS; } + + ExifInterface exifInterface = new ExifInterface(stream); Bundle exif = new Bundle(); - for (int i = 0; i < tags.length; i++) { - if (TYPE_MAPPING.get(tags[i]).equals(TYPE_INT)) { - int data = exifInterface.getAttributeInt(tags[i], Integer.MIN_VALUE); + for (String tag : tags) { + if (TYPE_MAPPING.get(tag).equals(TYPE_INT)) { + int data = exifInterface.getAttributeInt(tag, Integer.MIN_VALUE); if (data != Integer.MIN_VALUE) { - exif.putInt(tags[i], data); + exif.putInt(tag, data); } - } else if (TYPE_MAPPING.get(tags[i]).equals(TYPE_DOUBLE)) { - double data = exifInterface.getAttributeDouble(tags[i], Double.MIN_VALUE); + } else if (TYPE_MAPPING.get(tag).equals(TYPE_DOUBLE)) { + double data = exifInterface.getAttributeDouble(tag, Double.MIN_VALUE); if (data != Double.MIN_VALUE) { - exif.putDouble(tags[i], data); + exif.putDouble(tag, data); } - } else if (TYPE_MAPPING.get(tags[i]).equals(TYPE_STRING)) { - String data = exifInterface.getAttribute(tags[i]); + } else if (TYPE_MAPPING.get(tag).equals(TYPE_STRING)) { + String data = exifInterface.getAttribute(tag); if (data != null) { - exif.putString(tags[i], data); + exif.putString(tag, data); } } } diff --git a/core/java/android/provider/SearchIndexableData.java b/core/java/android/provider/SearchIndexableData.java index 5e0a76de8d27..a60be5363d62 100644 --- a/core/java/android/provider/SearchIndexableData.java +++ b/core/java/android/provider/SearchIndexableData.java @@ -56,6 +56,8 @@ public abstract class SearchIndexableData { /** * The key for the data. This is application specific. Should be unique per data as the data * should be able to be retrieved by the key. + * <p/> + * This is required for indexing to work. */ public String key; diff --git a/core/java/android/provider/SearchIndexablesContract.java b/core/java/android/provider/SearchIndexablesContract.java index ff8b9dd77068..adf437cedd90 100644 --- a/core/java/android/provider/SearchIndexablesContract.java +++ b/core/java/android/provider/SearchIndexablesContract.java @@ -62,11 +62,25 @@ public class SearchIndexablesContract { public static final String NON_INDEXABLES_KEYS = "non_indexables_key"; /** + * Site map pairs data key + * + * @hide + */ + public static final String SITE_MAP_PAIRS_KEYS = "site_map_pairs"; + + /** * ContentProvider path for non indexable data keys. */ public static final String NON_INDEXABLES_KEYS_PATH = SETTINGS + "/" + NON_INDEXABLES_KEYS; /** + * ContentProvider path for sitemap keys. + * + * @hide + */ + public static final String SITE_MAP_PAIRS_PATH = SETTINGS + "/" + SITE_MAP_PAIRS_KEYS; + + /** * Indexable xml resources columns. */ public static final String[] INDEXABLES_XML_RES_COLUMNS = new String[] { @@ -113,6 +127,18 @@ public class SearchIndexablesContract { }; /** + * Columns for site map queries. + * + * @hide + */ + public static final String[] SITE_MAP_COLUMNS = new String[] { + SiteMapColumns.PARENT_CLASS, + SiteMapColumns.PARENT_TITLE, + SiteMapColumns.CHILD_CLASS, + SiteMapColumns.CHILD_TITLE, + }; + + /** * Indexable raw data columns indices. */ public static final int COLUMN_INDEX_RAW_RANK = 0; @@ -169,6 +195,16 @@ public class SearchIndexablesContract { } /** + * @hide + */ + public static final class SiteMapColumns { + public static final String PARENT_CLASS = "parent_class"; + public static final String CHILD_CLASS = "child_class"; + public static final String PARENT_TITLE = "parent_title"; + public static final String CHILD_TITLE = "child_title"; + } + + /** * Constants related to a {@link SearchIndexableData}. * * This is the raw data that is stored into an Index. This is related to diff --git a/core/java/android/provider/SearchIndexablesProvider.java b/core/java/android/provider/SearchIndexablesProvider.java index 3120e543561d..138e77b69627 100644 --- a/core/java/android/provider/SearchIndexablesProvider.java +++ b/core/java/android/provider/SearchIndexablesProvider.java @@ -72,6 +72,7 @@ public abstract class SearchIndexablesProvider extends ContentProvider { private static final int MATCH_RES_CODE = 1; private static final int MATCH_RAW_CODE = 2; private static final int MATCH_NON_INDEXABLE_KEYS_CODE = 3; + private static final int MATCH_SITE_MAP_PAIRS_CODE = 4; /** * Implementation is provided by the parent class. @@ -87,6 +88,8 @@ public abstract class SearchIndexablesProvider extends ContentProvider { MATCH_RAW_CODE); mMatcher.addURI(mAuthority, SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH, MATCH_NON_INDEXABLE_KEYS_CODE); + mMatcher.addURI(mAuthority, SearchIndexablesContract.SITE_MAP_PAIRS_PATH, + MATCH_SITE_MAP_PAIRS_CODE); // Sanity check our setup if (!info.exported) { @@ -112,6 +115,8 @@ public abstract class SearchIndexablesProvider extends ContentProvider { return queryRawData(null); case MATCH_NON_INDEXABLE_KEYS_CODE: return queryNonIndexableKeys(null); + case MATCH_SITE_MAP_PAIRS_CODE: + return querySiteMapPairs(); default: throw new UnsupportedOperationException("Unknown Uri " + uri); } @@ -150,6 +155,17 @@ public abstract class SearchIndexablesProvider extends ContentProvider { */ public abstract Cursor queryNonIndexableKeys(String[] projection); + /** + * Returns a {@link Cursor}, where rows are [parent class, child class] entries to form a site + * map. The list of pairs should be as complete as possible. + * + * @hide + */ + public Cursor querySiteMapPairs() { + // By default no-op. + return null; + } + @Override public String getType(Uri uri) { switch (mMatcher.match(uri)) { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4679aee12809..6b1632a0a693 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -31,6 +31,7 @@ import android.app.ActivityThread; import android.app.AppOpsManager; import android.app.Application; import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.app.SearchManager; import android.app.WallpaperManager; @@ -65,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; @@ -209,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. @@ -441,6 +449,18 @@ public final class Settings { "android.settings.ASSIST_GESTURE_SETTINGS"; /** + * Activity Action: Show settings to enroll fingerprints, and setup PIN/Pattern/Pass if + * necessary. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_FINGERPRINT_ENROLL = + "android.settings.FINGERPRINT_ENROLL"; + + /** * Activity Action: Show settings to allow configuration of cast endpoints. * <p> * In some cases, a matching Activity may not exist, so ensure you @@ -902,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. @@ -1311,6 +1334,18 @@ public final class Settings { = "android.settings.CHANNEL_NOTIFICATION_SETTINGS"; /** + * Activity Action: Show notification settings for a single {@link NotificationChannelGroup}. + * <p> + * Input: {@link #EXTRA_APP_PACKAGE}, the package containing the channel group to display. + * Input: {@link #EXTRA_CHANNEL_GROUP_ID}, the id of the channel group to display. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS = + "android.settings.CHANNEL_GROUP_NOTIFICATION_SETTINGS"; + + /** * Activity Extra: The package owner of the notification channel settings to display. * <p> * This must be passed as an extra field to the {@link #ACTION_CHANNEL_NOTIFICATION_SETTINGS}. @@ -1326,6 +1361,15 @@ public final class Settings { public static final String EXTRA_CHANNEL_ID = "android.provider.extra.CHANNEL_ID"; /** + * Activity Extra: The {@link NotificationChannelGroup#getId()} of the notification channel + * group settings to display. + * <p> + * This must be passed as an extra field to the + * {@link #ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS}. + */ + public static final String EXTRA_CHANNEL_GROUP_ID = "android.provider.extra.CHANNEL_GROUP_ID"; + + /** * Activity Action: Show notification redaction settings. * * @hide @@ -1852,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; @@ -2066,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 */ @@ -3072,6 +3123,10 @@ public final class Settings { * to dream after a period of inactivity. This value is also known as the * user activity timeout period since the screen isn't necessarily turned off * when it expires. + * + * <p> + * This value is bounded by maximum timeout set by + * {@link android.app.admin.DevicePolicyManager#setMaximumTimeToLock(ComponentName, long)}. */ public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout"; @@ -3504,7 +3559,7 @@ public final class Settings { /** @hide */ public static final Validator TIME_12_24_VALIDATOR = - new DiscreteValueValidator(new String[] {"12", "24"}); + new DiscreteValueValidator(new String[] {"12", "24", null}); /** * Date format string @@ -4515,6 +4570,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 */ @@ -4586,7 +4644,6 @@ public final class Settings { MOVED_TO_GLOBAL.add(Settings.Global.PDP_WATCHDOG_MAX_PDP_RESET_FAIL_COUNT); MOVED_TO_GLOBAL.add(Settings.Global.PDP_WATCHDOG_POLL_INTERVAL_MS); MOVED_TO_GLOBAL.add(Settings.Global.PDP_WATCHDOG_TRIGGER_PACKET_COUNT); - MOVED_TO_GLOBAL.add(Settings.Global.SAMPLING_PROFILER_MS); MOVED_TO_GLOBAL.add(Settings.Global.SETUP_PREPAID_DATA_SERVICE_URL); MOVED_TO_GLOBAL.add(Settings.Global.SETUP_PREPAID_DETECTION_REDIR_HOST); MOVED_TO_GLOBAL.add(Settings.Global.SETUP_PREPAID_DETECTION_TARGET_URL); @@ -5271,6 +5328,52 @@ public final class Settings { public static final String AUTOFILL_SERVICE = "autofill_service"; /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + @TestApi + public static final String AUTOFILL_FEATURE_FIELD_CLASSIFICATION = + "autofill_field_classification"; + + /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + public static final String AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE = + "autofill_user_data_max_user_data_size"; + + /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + public static final String AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE = + "autofill_user_data_max_field_classification_size"; + + /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + public static final String AUTOFILL_USER_DATA_MAX_VALUE_LENGTH = + "autofill_user_data_max_value_length"; + + /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): document (or remove) once feature is finished + * @hide + */ + public static final String AUTOFILL_USER_DATA_MIN_VALUE_LENGTH = + "autofill_user_data_min_value_length"; + + /** * @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead */ @Deprecated @@ -5693,6 +5796,7 @@ public final class Settings { * * @hide */ + @TestApi public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED = "accessibility_display_magnification_enabled"; @@ -6777,14 +6881,6 @@ public final class Settings { "lock_screen_show_notifications"; /** - * This preference stores the last stack active task time for each user, which affects what - * tasks will be visible in Overview. - * @hide - */ - public static final String OVERVIEW_LAST_STACK_ACTIVE_TIME = - "overview_last_stack_active_time"; - - /** * List of TV inputs that are currently hidden. This is a string * containing the IDs of all hidden TV inputs. Each ID is encoded by * {@link android.net.Uri#encode(String)} and separated by ':'. @@ -7128,6 +7224,36 @@ public final class Settings { public static final String QS_AUTO_ADDED_TILES = "qs_auto_tiles"; /** + * Whether the Lockdown button should be shown in the power menu. + * @hide + */ + public static final String LOCKDOWN_IN_POWER_MENU = "lockdown_in_power_menu"; + + /** + * Backup manager behavioral parameters. + * This is encoded as a key=value list, separated by commas. Ex: + * + * "key_value_backup_interval_milliseconds=14400000,key_value_backup_require_charging=true" + * + * The following keys are supported: + * + * <pre> + * key_value_backup_interval_milliseconds (long) + * key_value_backup_fuzz_milliseconds (long) + * key_value_backup_require_charging (boolean) + * key_value_backup_required_network_type (int) + * full_backup_interval_milliseconds (long) + * full_backup_require_charging (boolean) + * full_backup_required_network_type (int) + * </pre> + * + * <p> + * Type: string + * @hide + */ + public static final String BACKUP_MANAGER_CONSTANTS = "backup_manager_constants"; + + /** * This are the settings to be backed up. * * NOTE: Settings are backed up and restored in the order they appear @@ -7229,6 +7355,7 @@ public final class Settings { SCREENSAVER_COMPONENTS, SCREENSAVER_ACTIVATE_ON_DOCK, SCREENSAVER_ACTIVATE_ON_SLEEP, + LOCKDOWN_IN_POWER_MENU, }; /** @hide */ @@ -7256,8 +7383,6 @@ public final class Settings { CLONE_TO_MANAGED_PROFILE.add(LOCATION_PREVIOUS_MODE); CLONE_TO_MANAGED_PROFILE.add(LOCATION_PROVIDERS_ALLOWED); CLONE_TO_MANAGED_PROFILE.add(SELECTED_INPUT_METHOD_SUBTYPE); - CLONE_TO_MANAGED_PROFILE.add(SELECTED_SPELL_CHECKER); - CLONE_TO_MANAGED_PROFILE.add(SELECTED_SPELL_CHECKER_SUBTYPE); } /** @hide */ @@ -7487,6 +7612,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. */ @@ -7952,28 +8080,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"; /** @@ -8251,15 +8391,6 @@ public final class Settings { "pdp_watchdog_max_pdp_reset_fail_count"; /** - * A positive value indicates how often the SamplingProfiler - * should take snapshots. Zero value means SamplingProfiler - * is disabled. - * - * @hide - */ - public static final String SAMPLING_PROFILER_MS = "sampling_profiler_ms"; - - /** * URL to open browser on to allow user to manage a prepay account * @hide */ @@ -8442,6 +8573,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 @@ -8554,6 +8692,12 @@ public final class Settings { "wifi_scan_always_enabled"; /** + * Whether soft AP will shut down after a timeout period when no devices are connected. + * @hide + */ + public static final String SOFT_AP_TIMEOUT_ENABLED = "soft_ap_timeout_enabled"; + + /** * Value to specify if Wi-Fi Wakeup feature is enabled. * * Type: int (0 for false, 1 for true) @@ -9360,16 +9504,6 @@ public final class Settings { public static final String DEVICE_IDLE_CONSTANTS = "device_idle_constants"; /** - * Device Idle (Doze) specific settings for watches. See {@code #DEVICE_IDLE_CONSTANTS} - * - * <p> - * Type: string - * @hide - * @see com.android.server.DeviceIdleController.Constants - */ - public static final String DEVICE_IDLE_CONSTANTS_WATCH = "device_idle_constants_watch"; - - /** * Battery Saver specific settings * This is encoded as a key=value list, separated by commas. Ex: * @@ -9394,6 +9528,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 @@ -9427,8 +9571,8 @@ public final class Settings { * The following keys are supported: * * <pre> - * screen_brightness_array (string) - * dimming_scrim_array (string) + * screen_brightness_array (int[]) + * dimming_scrim_array (int[]) * prox_screen_off_delay (long) * prox_cooldown_trigger (long) * prox_cooldown_period (long) @@ -9768,6 +9912,27 @@ public final class Settings { public static final String WAIT_FOR_DEBUGGER = "wait_for_debugger"; /** + * Allow GPU debug layers? + * 0 = no + * 1 = yes + * @hide + */ + public static final String ENABLE_GPU_DEBUG_LAYERS = "enable_gpu_debug_layers"; + + /** + * App allowed to load GPU debug layers + * @hide + */ + public static final String GPU_DEBUG_APP = "gpu_debug_app"; + + /** + * Ordered GPU debug layer list + * i.e. <layer1>:<layer2>:...:<layerN> + * @hide + */ + public static final String GPU_DEBUG_LAYERS = "gpu_debug_layers"; + + /** * Control whether the process CPU usage meter should be shown. * * @deprecated This functionality is no longer available as of @@ -9999,6 +10164,16 @@ public final class Settings { public static final String POLICY_CONTROL = "policy_control"; /** + * {@link android.view.DisplayCutout DisplayCutout} emulation mode. + * + * @hide + */ + public static final String EMULATE_DISPLAY_CUTOUT = "emulate_display_cutout"; + + /** @hide */ public static final int EMULATE_DISPLAY_CUTOUT_OFF = 0; + /** @hide */ public static final int EMULATE_DISPLAY_CUTOUT_ON = 1; + + /** * Defines global zen mode. ZEN_MODE_OFF, ZEN_MODE_IMPORTANT_INTERRUPTIONS, * or ZEN_MODE_NO_INTERRUPTIONS. * @@ -10083,8 +10258,12 @@ public final class Settings { * <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 @@ -10092,8 +10271,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 @@ -10101,8 +10282,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. @@ -10110,8 +10293,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. @@ -10119,8 +10304,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 @@ -10128,8 +10316,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 @@ -10224,7 +10416,7 @@ public final class Settings { "allow_user_switching_when_system_user_locked"; /** - * Boot count since the device starts running APK level 24. + * Boot count since the device starts running API level 24. * <p> * Type: int */ @@ -10295,14 +10487,6 @@ public final class Settings { "location_settings_link_to_permissions_enabled"; /** - * Flag to enable use of RefactoredBackupManagerService. - * - * @hide - */ - public static final String BACKUP_REFACTORED_SERVICE_DISABLED = - "backup_refactored_service_disabled"; - - /** * Flag to set the waiting time for euicc factory reset inside System > Settings * Type: long * @@ -10321,6 +10505,15 @@ public final class Settings { "storage_settings_clobber_threshold"; /** + * If set to 1, {@link Secure#LOCATION_MODE} will be set to {@link Secure#LOCATION_MODE_OFF} + * temporarily for all users. + * + * @hide + */ + public static final String LOCATION_GLOBAL_KILL_SWITCH = + "location_global_kill_switch"; + + /** * Settings to backup. This is here so that it's in the same place as the settings * keys and easy to update. * @@ -10357,7 +10550,17 @@ public final class Settings { LOW_POWER_MODE_TRIGGER_LEVEL, BLUETOOTH_ON, PRIVATE_DNS_MODE, - PRIVATE_DNS_SPECIFIER + PRIVATE_DNS_SPECIFIER, + SOFT_AP_TIMEOUT_ENABLED + }; + + /** + * Global settings that shouldn't be persisted. + * + * @hide + */ + public static final String[] TRANSIENT_SETTINGS = { + LOCATION_GLOBAL_KILL_SWITCH, }; /** @hide */ @@ -10810,7 +11013,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 */ @@ -10931,7 +11134,7 @@ public final class Settings { * * <pre> * default (int) - * options_array (string) + * options_array (int[]) * </pre> * * All delays in integer minutes. Array order is respected. diff --git a/core/java/android/provider/VoicemailContract.java b/core/java/android/provider/VoicemailContract.java index 864a0fd7d7c5..6be0e76c552b 100644 --- a/core/java/android/provider/VoicemailContract.java +++ b/core/java/android/provider/VoicemailContract.java @@ -16,7 +16,6 @@ package android.provider; -import android.Manifest; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; @@ -50,7 +49,7 @@ import java.util.List; * </ul> * * <P> The minimum permission needed to access this content provider is - * {@link Manifest.permission#ADD_VOICEMAIL} + * {@link android.Manifest.permission#ADD_VOICEMAIL} * * <P>Voicemails are inserted by what is called as a "voicemail source" * application, which is responsible for syncing voicemail data between a remote @@ -293,11 +292,26 @@ public class VoicemailContract { * Flag used to indicate that local, unsynced changes are present. * Currently, this is used to indicate that the voicemail was read or deleted. * The value will be 1 if dirty is true, 0 if false. + * + * <p>When a caller updates a voicemail row (either with {@link ContentResolver#update} or + * {@link ContentResolver#applyBatch}), and if the {@link ContentValues} doesn't contain + * this column, the voicemail provider implicitly sets it to 0 if the calling package is + * the {@link #SOURCE_PACKAGE} or to 1 otherwise. To prevent this behavior, explicitly set + * {@link #DIRTY_RETAIN} to this column in the {@link ContentValues}. + * * <P>Type: INTEGER (boolean)</P> + * + * @see #DIRTY_RETAIN */ public static final String DIRTY = "dirty"; /** + * Value of {@link #DIRTY} when updating to indicate that the value should not be updated + * during this operation. + */ + public static final int DIRTY_RETAIN = -1; + + /** * Flag used to indicate that the voicemail was deleted but not synced to the server. * A deleted row should be ignored. * The value will be 1 if deleted is true, 0 if false. diff --git a/core/java/android/security/KeystoreArguments.aidl b/core/java/android/security/KeystoreArguments.aidl index d636414a05e3..dc8ed50182ed 100644 --- a/core/java/android/security/KeystoreArguments.aidl +++ b/core/java/android/security/KeystoreArguments.aidl @@ -17,4 +17,4 @@ package android.security; /* @hide */ -parcelable KeystoreArguments; +parcelable KeystoreArguments cpp_header "keystore/KeystoreArguments.h"; diff --git a/core/java/android/security/NetworkSecurityPolicy.java b/core/java/android/security/NetworkSecurityPolicy.java index 812c956f4c61..0c4eedab2fc0 100644 --- a/core/java/android/security/NetworkSecurityPolicy.java +++ b/core/java/android/security/NetworkSecurityPolicy.java @@ -16,7 +16,6 @@ package android.security; -import android.annotation.TestApi; import android.content.Context; import android.content.pm.PackageManager; import android.security.net.config.ApplicationConfig; @@ -63,7 +62,8 @@ public class NetworkSecurityPolicy { * traffic from applications is handled by higher-level network stacks/components which can * honor this aspect of the policy. * - * <p>NOTE: {@link android.webkit.WebView} does not honor this flag. + * <p>NOTE: {@link android.webkit.WebView} honors this flag for applications targeting API level + * 26 and up. */ public boolean isCleartextTrafficPermitted() { return libcore.net.NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(); diff --git a/core/java/android/security/keymaster/ExportResult.aidl b/core/java/android/security/keymaster/ExportResult.aidl index 4d9b2de6632f..17486531a3f0 100644 --- a/core/java/android/security/keymaster/ExportResult.aidl +++ b/core/java/android/security/keymaster/ExportResult.aidl @@ -17,4 +17,4 @@ package android.security.keymaster; /* @hide */ -parcelable ExportResult; +parcelable ExportResult cpp_header "keystore/ExportResult.h"; diff --git a/core/java/android/security/keymaster/KeyAttestationPackageInfo.java b/core/java/android/security/keymaster/KeyAttestationPackageInfo.java index 5a3f39079cf6..a93d1e113eec 100644 --- a/core/java/android/security/keymaster/KeyAttestationPackageInfo.java +++ b/core/java/android/security/keymaster/KeyAttestationPackageInfo.java @@ -28,7 +28,7 @@ import android.os.Parcelable; */ public class KeyAttestationPackageInfo implements Parcelable { private final String mPackageName; - private final int mPackageVersionCode; + private final long mPackageVersionCode; private final Signature[] mPackageSignatures; /** @@ -37,7 +37,7 @@ public class KeyAttestationPackageInfo implements Parcelable { * @param mPackageSignatures */ public KeyAttestationPackageInfo( - String mPackageName, int mPackageVersionCode, Signature[] mPackageSignatures) { + String mPackageName, long mPackageVersionCode, Signature[] mPackageSignatures) { super(); this.mPackageName = mPackageName; this.mPackageVersionCode = mPackageVersionCode; @@ -52,7 +52,7 @@ public class KeyAttestationPackageInfo implements Parcelable { /** * @return the mPackageVersionCode */ - public int getPackageVersionCode() { + public long getPackageVersionCode() { return mPackageVersionCode; } /** @@ -70,7 +70,7 @@ public class KeyAttestationPackageInfo implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mPackageName); - dest.writeInt(mPackageVersionCode); + dest.writeLong(mPackageVersionCode); dest.writeTypedArray(mPackageSignatures, flags); } @@ -89,7 +89,7 @@ public class KeyAttestationPackageInfo implements Parcelable { private KeyAttestationPackageInfo(Parcel source) { mPackageName = source.readString(); - mPackageVersionCode = source.readInt(); + mPackageVersionCode = source.readLong(); mPackageSignatures = source.createTypedArray(Signature.CREATOR); } } diff --git a/core/java/android/security/keymaster/KeyCharacteristics.aidl b/core/java/android/security/keymaster/KeyCharacteristics.aidl index be739d3223ab..32e75ad267b2 100644 --- a/core/java/android/security/keymaster/KeyCharacteristics.aidl +++ b/core/java/android/security/keymaster/KeyCharacteristics.aidl @@ -17,4 +17,4 @@ package android.security.keymaster; /* @hide */ -parcelable KeyCharacteristics; +parcelable KeyCharacteristics cpp_header "keystore/KeyCharacteristics.h"; diff --git a/core/java/android/security/keymaster/KeymasterArguments.aidl b/core/java/android/security/keymaster/KeymasterArguments.aidl index 1a73206512e9..44d9f0954781 100644 --- a/core/java/android/security/keymaster/KeymasterArguments.aidl +++ b/core/java/android/security/keymaster/KeymasterArguments.aidl @@ -17,4 +17,4 @@ package android.security.keymaster; /* @hide */ -parcelable KeymasterArguments; +parcelable KeymasterArguments cpp_header "keystore/KeymasterArguments.h"; diff --git a/core/java/android/security/keymaster/KeymasterBlob.aidl b/core/java/android/security/keymaster/KeymasterBlob.aidl index b7cd1c900efb..5c5db9ec314b 100644 --- a/core/java/android/security/keymaster/KeymasterBlob.aidl +++ b/core/java/android/security/keymaster/KeymasterBlob.aidl @@ -17,4 +17,4 @@ package android.security.keymaster; /* @hide */ -parcelable KeymasterBlob; +parcelable KeymasterBlob cpp_header "keystore/KeymasterBlob.h"; diff --git a/core/java/android/security/keymaster/KeymasterCertificateChain.aidl b/core/java/android/security/keymaster/KeymasterCertificateChain.aidl index dc1876aaaebd..ddb5cae1a254 100644 --- a/core/java/android/security/keymaster/KeymasterCertificateChain.aidl +++ b/core/java/android/security/keymaster/KeymasterCertificateChain.aidl @@ -17,4 +17,4 @@ package android.security.keymaster; /* @hide */ -parcelable KeymasterCertificateChain; +parcelable KeymasterCertificateChain cpp_header "keystore/KeymasterCertificateChain.h"; diff --git a/core/java/android/security/keymaster/OperationResult.aidl b/core/java/android/security/keymaster/OperationResult.aidl index ed26c8dd404b..db689d46521a 100644 --- a/core/java/android/security/keymaster/OperationResult.aidl +++ b/core/java/android/security/keymaster/OperationResult.aidl @@ -17,4 +17,4 @@ package android.security.keymaster; /* @hide */ -parcelable OperationResult; +parcelable OperationResult cpp_header "keystore/OperationResult.h"; diff --git a/core/java/android/security/net/config/ManifestConfigSource.java b/core/java/android/security/net/config/ManifestConfigSource.java index 8fcd5ab55e6a..79115a5ad3c2 100644 --- a/core/java/android/security/net/config/ManifestConfigSource.java +++ b/core/java/android/security/net/config/ManifestConfigSource.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.util.Log; import android.util.Pair; + import java.util.Set; /** @hide */ @@ -29,21 +30,14 @@ public class ManifestConfigSource implements ConfigSource { private final Object mLock = new Object(); private final Context mContext; - private final int mApplicationInfoFlags; - private final int mTargetSdkVersion; - private final int mConfigResourceId; - private final int mTargetSandboxVesrsion; + private final ApplicationInfo mApplicationInfo; private ConfigSource mConfigSource; public ManifestConfigSource(Context context) { mContext = context; - // Cache values because ApplicationInfo is mutable and apps do modify it :( - ApplicationInfo info = context.getApplicationInfo(); - mApplicationInfoFlags = info.flags; - mTargetSdkVersion = info.targetSdkVersion; - mConfigResourceId = info.networkSecurityConfigRes; - mTargetSandboxVesrsion = info.targetSandboxVersion; + // Cache the info because ApplicationInfo is mutable and apps do modify it :( + mApplicationInfo = new ApplicationInfo(context.getApplicationInfo()); } @Override @@ -61,17 +55,18 @@ public class ManifestConfigSource implements ConfigSource { if (mConfigSource != null) { return mConfigSource; } - + int configResource = mApplicationInfo.networkSecurityConfigRes; ConfigSource source; - if (mConfigResourceId != 0) { - boolean debugBuild = (mApplicationInfoFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + if (configResource != 0) { + boolean debugBuild = + (mApplicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; if (DBG) { Log.d(LOG_TAG, "Using Network Security Config from resource " - + mContext.getResources().getResourceEntryName(mConfigResourceId) + + mContext.getResources() + .getResourceEntryName(configResource) + " debugBuild: " + debugBuild); } - source = new XmlConfigSource(mContext, mConfigResourceId, debugBuild, - mTargetSdkVersion, mTargetSandboxVesrsion); + source = new XmlConfigSource(mContext, configResource, mApplicationInfo); } else { if (DBG) { Log.d(LOG_TAG, "No Network Security Config specified, using platform default"); @@ -79,10 +74,9 @@ public class ManifestConfigSource implements ConfigSource { // the legacy FLAG_USES_CLEARTEXT_TRAFFIC is not supported for Ephemeral apps, they // should use the network security config. boolean usesCleartextTraffic = - (mApplicationInfoFlags & ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC) != 0 - && mTargetSandboxVesrsion < 2; - source = new DefaultConfigSource(usesCleartextTraffic, mTargetSdkVersion, - mTargetSandboxVesrsion); + (mApplicationInfo.flags & ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC) != 0 + && mApplicationInfo.targetSandboxVersion < 2; + source = new DefaultConfigSource(usesCleartextTraffic, mApplicationInfo); } mConfigSource = source; return mConfigSource; @@ -93,10 +87,8 @@ public class ManifestConfigSource implements ConfigSource { private final NetworkSecurityConfig mDefaultConfig; - public DefaultConfigSource(boolean usesCleartextTraffic, int targetSdkVersion, - int targetSandboxVesrsion) { - mDefaultConfig = NetworkSecurityConfig.getDefaultBuilder(targetSdkVersion, - targetSandboxVesrsion) + DefaultConfigSource(boolean usesCleartextTraffic, ApplicationInfo info) { + mDefaultConfig = NetworkSecurityConfig.getDefaultBuilder(info) .setCleartextTrafficPermitted(usesCleartextTraffic) .build(); } diff --git a/core/java/android/security/net/config/NetworkSecurityConfig.java b/core/java/android/security/net/config/NetworkSecurityConfig.java index 789fc273b965..52f48ef8499b 100644 --- a/core/java/android/security/net/config/NetworkSecurityConfig.java +++ b/core/java/android/security/net/config/NetworkSecurityConfig.java @@ -16,9 +16,11 @@ package android.security.net.config; +import android.content.pm.ApplicationInfo; import android.os.Build; import android.util.ArrayMap; import android.util.ArraySet; + import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; @@ -28,8 +30,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import javax.net.ssl.X509TrustManager; - /** * @hide */ @@ -164,28 +164,32 @@ public final class NetworkSecurityConfig { * <p> * The default configuration has the following properties: * <ol> - * <li>Cleartext traffic is permitted for non-ephemeral apps.</li> + * <li>If the application targets API level 27 (Android O MR1) or lower then cleartext traffic + * is allowed by default.</li> * <li>Cleartext traffic is not permitted for ephemeral apps.</li> * <li>HSTS is not enforced.</li> * <li>No certificate pinning is used.</li> * <li>The system certificate store is trusted for connections.</li> * <li>If the application targets API level 23 (Android M) or lower then the user certificate - * store is trusted by default as well.</li> + * store is trusted by default as well for non-privileged applications.</li> + * <li>Privileged applications do not trust the user certificate store on Android P and higher. + * </li> * </ol> * * @hide */ - public static final Builder getDefaultBuilder(int targetSdkVersion, int targetSandboxVesrsion) { + public static Builder getDefaultBuilder(ApplicationInfo info) { Builder builder = new Builder() .setHstsEnforced(DEFAULT_HSTS_ENFORCED) // System certificate store, does not bypass static pins. .addCertificatesEntryRef( new CertificatesEntryRef(SystemCertificateSource.getInstance(), false)); - final boolean cleartextTrafficPermitted = targetSandboxVesrsion < 2; + final boolean cleartextTrafficPermitted = info.targetSdkVersion < Build.VERSION_CODES.P + && info.targetSandboxVersion < 2; builder.setCleartextTrafficPermitted(cleartextTrafficPermitted); // Applications targeting N and above must opt in into trusting the user added certificate // store. - if (targetSdkVersion <= Build.VERSION_CODES.M) { + if (info.targetSdkVersion <= Build.VERSION_CODES.M && !info.isPrivilegedApp()) { // User certificate store, does not bypass static pins. builder.addCertificatesEntryRef( new CertificatesEntryRef(UserCertificateSource.getInstance(), false)); diff --git a/core/java/android/security/net/config/XmlConfigSource.java b/core/java/android/security/net/config/XmlConfigSource.java index a111fbce183c..02be403ae150 100644 --- a/core/java/android/security/net/config/XmlConfigSource.java +++ b/core/java/android/security/net/config/XmlConfigSource.java @@ -1,13 +1,13 @@ package android.security.net.config; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.content.res.XmlResourceParser; -import android.os.Build; import android.util.ArraySet; import android.util.Base64; import android.util.Pair; -import com.android.internal.annotations.VisibleForTesting; + import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -36,37 +36,19 @@ public class XmlConfigSource implements ConfigSource { private final Object mLock = new Object(); private final int mResourceId; private final boolean mDebugBuild; - private final int mTargetSdkVersion; - private final int mTargetSandboxVesrsion; + private final ApplicationInfo mApplicationInfo; private boolean mInitialized; private NetworkSecurityConfig mDefaultConfig; private Set<Pair<Domain, NetworkSecurityConfig>> mDomainMap; private Context mContext; - @VisibleForTesting - public XmlConfigSource(Context context, int resourceId) { - this(context, resourceId, false); - } - - @VisibleForTesting - public XmlConfigSource(Context context, int resourceId, boolean debugBuild) { - this(context, resourceId, debugBuild, Build.VERSION_CODES.CUR_DEVELOPMENT); - } - - @VisibleForTesting - public XmlConfigSource(Context context, int resourceId, boolean debugBuild, - int targetSdkVersion) { - this(context, resourceId, debugBuild, targetSdkVersion, 1 /*targetSandboxVersion*/); - } - - public XmlConfigSource(Context context, int resourceId, boolean debugBuild, - int targetSdkVersion, int targetSandboxVesrsion) { - mResourceId = resourceId; + public XmlConfigSource(Context context, int resourceId, ApplicationInfo info) { mContext = context; - mDebugBuild = debugBuild; - mTargetSdkVersion = targetSdkVersion; - mTargetSandboxVesrsion = targetSandboxVesrsion; + mResourceId = resourceId; + mApplicationInfo = new ApplicationInfo(info); + + mDebugBuild = (mApplicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; } public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() { @@ -365,7 +347,7 @@ public class XmlConfigSource implements ConfigSource { // Use the platform default as the parent of the base config for any values not provided // there. If there is no base config use the platform default. NetworkSecurityConfig.Builder platformDefaultBuilder = - NetworkSecurityConfig.getDefaultBuilder(mTargetSdkVersion, mTargetSandboxVesrsion); + NetworkSecurityConfig.getDefaultBuilder(mApplicationInfo); addDebugAnchorsIfNeeded(debugConfigBuilder, platformDefaultBuilder); if (baseConfigBuilder != null) { baseConfigBuilder.setParent(platformDefaultBuilder); diff --git a/core/java/android/security/recoverablekeystore/RecoverableKeyGenerator.java b/core/java/android/security/recoverablekeystore/RecoverableKeyGenerator.java new file mode 100644 index 000000000000..4125f0baf49d --- /dev/null +++ b/core/java/android/security/recoverablekeystore/RecoverableKeyGenerator.java @@ -0,0 +1,133 @@ +/* + * 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.security.recoverablekeystore; + +import android.security.keystore.AndroidKeyStoreSecretKey; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyProtection; +import android.util.Log; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; + +/** + * Generates keys and stores them both in AndroidKeyStore and on disk, in wrapped form. + * + * <p>Generates 256-bit AES keys, which can be used for encrypt / decrypt with AES/GCM/NoPadding. + * They are synced to disk wrapped by a platform key. This allows them to be exported to a remote + * service. + * + * @hide + */ +public class RecoverableKeyGenerator { + private static final String TAG = "RecoverableKeyGenerator"; + private static final String KEY_GENERATOR_ALGORITHM = "AES"; + private static final int KEY_SIZE_BITS = 256; + + /** + * A new {@link RecoverableKeyGenerator} instance. + * + * @param platformKey Secret key used to wrap generated keys before persisting to disk. + * @param recoverableKeyStorage Class that manages persisting wrapped keys to disk. + * @throws NoSuchAlgorithmException if "AES" key generation or "AES/GCM/NoPadding" cipher is + * unavailable. Should never happen. + * + * @hide + */ + public static RecoverableKeyGenerator newInstance( + AndroidKeyStoreSecretKey platformKey, RecoverableKeyStorage recoverableKeyStorage) + throws NoSuchAlgorithmException { + // NB: This cannot use AndroidKeyStore as the provider, as we need access to the raw key + // material, so that it can be synced to disk in encrypted form. + KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_GENERATOR_ALGORITHM); + return new RecoverableKeyGenerator(keyGenerator, platformKey, recoverableKeyStorage); + } + + private final KeyGenerator mKeyGenerator; + private final RecoverableKeyStorage mRecoverableKeyStorage; + private final AndroidKeyStoreSecretKey mPlatformKey; + + private RecoverableKeyGenerator( + KeyGenerator keyGenerator, + AndroidKeyStoreSecretKey platformKey, + RecoverableKeyStorage recoverableKeyStorage) { + mKeyGenerator = keyGenerator; + mRecoverableKeyStorage = recoverableKeyStorage; + mPlatformKey = platformKey; + } + + /** + * Generates a 256-bit AES key with the given alias. + * + * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is + * persisted to disk so that it can be synced remotely, and then recovered on another device. + * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding. + * + * <p>The key handle returned to the caller is a reference to the AndroidKeyStore key, + * meaning that the caller is never able to access the raw, unencrypted key. + * + * @param alias The alias by which the key will be known in AndroidKeyStore. + * @throws InvalidKeyException if the platform key cannot be used to wrap keys. + * @throws IOException if there was an issue writing the wrapped key to the wrapped key store. + * @throws UnrecoverableEntryException if could not retrieve key after putting it in + * AndroidKeyStore. This should not happen. + * @return A handle to the AndroidKeyStore key. + * + * @hide + */ + public SecretKey generateAndStoreKey(String alias) throws KeyStoreException, + InvalidKeyException, IOException, UnrecoverableEntryException { + mKeyGenerator.init(KEY_SIZE_BITS); + SecretKey key = mKeyGenerator.generateKey(); + + mRecoverableKeyStorage.importIntoAndroidKeyStore( + alias, + key, + new KeyProtection.Builder( + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()); + WrappedKey wrappedKey = WrappedKey.fromSecretKey(mPlatformKey, key); + + try { + // Keep raw key material in memory for minimum possible time. + key.destroy(); + } catch (DestroyFailedException e) { + Log.w(TAG, "Could not destroy SecretKey."); + } + + mRecoverableKeyStorage.persistToDisk(alias, wrappedKey); + + try { + // Reload from the keystore, so that the caller is only provided with the handle of the + // key, not the raw key material. + return mRecoverableKeyStorage.loadFromAndroidKeyStore(alias); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException( + "Impossible: NoSuchAlgorithmException when attempting to retrieve a key " + + "that has only just been stored in AndroidKeyStore.", e); + } + } +} diff --git a/core/java/android/security/recoverablekeystore/RecoverableKeyStorage.java b/core/java/android/security/recoverablekeystore/RecoverableKeyStorage.java new file mode 100644 index 000000000000..c239e006c23b --- /dev/null +++ b/core/java/android/security/recoverablekeystore/RecoverableKeyStorage.java @@ -0,0 +1,71 @@ +/* + * 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.security.recoverablekeystore; + +import android.security.keystore.KeyProtection; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; + +import javax.crypto.SecretKey; + +/** + * Stores wrapped keys to disk, so they can be synced on the next screen unlock event. + * + * @hide + */ +public interface RecoverableKeyStorage { + + /** + * Writes {@code wrappedKey} to disk, keyed by the application's uid and the {@code alias}. + * + * @throws IOException if an error occurred writing to disk. + * + * @hide + */ + void persistToDisk(String alias, WrappedKey wrappedKey) throws IOException; + + /** + * Imports {@code key} into AndroidKeyStore, keyed by the application's uid and + * the {@code alias}. + * + * @param alias The alias of the key. + * @param key The key. + * @param keyProtection Protection params denoting what the key can be used for. (e.g., what + * Cipher modes, whether for encrpyt/decrypt or signing, etc.) + * @throws KeyStoreException if an error occurred loading the key into the AndroidKeyStore. + * + * @hide + */ + void importIntoAndroidKeyStore(String alias, SecretKey key, KeyProtection keyProtection) throws + KeyStoreException; + + /** + * Loads a key handle from AndroidKeyStore. + * + * @param alias Alias of the key to load. + * @return The key handle. + * @throws KeyStoreException if an error occurred loading the key from AndroidKeyStore. + * + * @hide + */ + SecretKey loadFromAndroidKeyStore(String alias) throws KeyStoreException, + NoSuchAlgorithmException, + UnrecoverableEntryException; +} diff --git a/core/java/android/security/recoverablekeystore/RecoverableKeyStorageImpl.java b/core/java/android/security/recoverablekeystore/RecoverableKeyStorageImpl.java new file mode 100644 index 000000000000..b9926dd9cf8d --- /dev/null +++ b/core/java/android/security/recoverablekeystore/RecoverableKeyStorageImpl.java @@ -0,0 +1,110 @@ +/* + * 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.security.recoverablekeystore; + +import android.security.keystore.KeyProtection; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; + +import javax.crypto.SecretKey; + +/** + * Implementation of {@link RecoverableKeyStorage}. + * + * <p>Persists wrapped keys to disk, and loads raw keys into AndroidKeyStore. + * + * @hide + */ +public class RecoverableKeyStorageImpl implements RecoverableKeyStorage { + private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore"; + + private final KeyStore mKeyStore; + + /** + * A new instance. + * + * @throws KeyStoreException if unable to load AndroidKeyStore. + * + * @hide + */ + public static RecoverableKeyStorageImpl newInstance() throws KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER); + try { + keyStore.load(/*param=*/ null); + } catch (CertificateException | IOException | NoSuchAlgorithmException e) { + // Should never happen. + throw new KeyStoreException("Unable to load keystore.", e); + } + return new RecoverableKeyStorageImpl(keyStore); + } + + private RecoverableKeyStorageImpl(KeyStore keyStore) { + mKeyStore = keyStore; + } + + /** + * Writes {@code wrappedKey} to disk, keyed by the application's uid and the {@code alias}. + * + * @throws IOException if an error occurred writing to disk. + * + * @hide + */ + @Override + public void persistToDisk(String alias, WrappedKey wrappedKey) throws IOException { + // TODO(robertberry) Add implementation. + throw new UnsupportedOperationException(); + } + + /** + * Imports {@code key} into AndroidKeyStore, keyed by the application's uid and the + * {@code alias}. + * + * @param alias The alias of the key. + * @param key The key. + * @param keyProtection Protection params denoting what the key can be used for. (e.g., what + * Cipher modes, whether for encrpyt/decrypt or signing, etc.) + * @throws KeyStoreException if an error occurred loading the key into the AndroidKeyStore. + * + * @hide + */ + @Override + public void importIntoAndroidKeyStore(String alias, SecretKey key, KeyProtection keyProtection) + throws KeyStoreException { + mKeyStore.setEntry(alias, new KeyStore.SecretKeyEntry(key), keyProtection); + } + + /** + * Loads a key handle from AndroidKeyStore. + * + * @param alias Alias of the key to load. + * @return The key handle. + * @throws KeyStoreException if an error occurred loading the key from AndroidKeyStore. + * + * @hide + */ + @Override + public SecretKey loadFromAndroidKeyStore(String alias) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException { + return ((KeyStore.SecretKeyEntry) mKeyStore.getEntry(alias, /*protParam=*/ null)) + .getSecretKey(); + } +} diff --git a/core/java/android/security/recoverablekeystore/WrappedKey.java b/core/java/android/security/recoverablekeystore/WrappedKey.java new file mode 100644 index 000000000000..51665ae2931d --- /dev/null +++ b/core/java/android/security/recoverablekeystore/WrappedKey.java @@ -0,0 +1,115 @@ +/* + * 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.security.recoverablekeystore; + +import java.security.InvalidKeyException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +/** + * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding. + * + * @hide + */ +public class WrappedKey { + private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding"; + + private final byte[] mNonce; + private final byte[] mKeyMaterial; + + /** + * Returns a wrapped form of {@code key}, using {@code wrappingKey} to encrypt the key material. + * + * @throws InvalidKeyException if {@code wrappingKey} cannot be used to encrypt {@code key}, or + * if {@code key} does not expose its key material. See + * {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does + * not expose its key material. + */ + public static WrappedKey fromSecretKey( + SecretKey wrappingKey, SecretKey key) throws InvalidKeyException, KeyStoreException { + if (key.getEncoded() == null) { + throw new InvalidKeyException( + "key does not expose encoded material. It cannot be wrapped."); + } + + Cipher cipher; + try { + cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new RuntimeException( + "Android does not support AES/GCM/NoPadding. This should never happen."); + } + + cipher.init(Cipher.WRAP_MODE, wrappingKey); + byte[] encryptedKeyMaterial; + try { + encryptedKeyMaterial = cipher.wrap(key); + } catch (IllegalBlockSizeException e) { + Throwable cause = e.getCause(); + if (cause instanceof KeyStoreException) { + // If AndroidKeyStore encounters any error here, it throws IllegalBlockSizeException + // with KeyStoreException as the cause. This is due to there being no better option + // here, as the Cipher#wrap only checked throws InvalidKeyException or + // IllegalBlockSizeException. If this is the case, we want to propagate it to the + // caller, so rethrow the cause. + throw (KeyStoreException) cause; + } else { + throw new RuntimeException( + "IllegalBlockSizeException should not be thrown by AES/GCM/NoPadding mode.", + e); + } + } + + return new WrappedKey(/*mNonce=*/ cipher.getIV(), /*mKeyMaterial=*/ encryptedKeyMaterial); + } + + /** + * A new instance. + * + * @param nonce The nonce with which the key material was encrypted. + * @param keyMaterial The encrypted bytes of the key material. + * + * @hide + */ + public WrappedKey(byte[] nonce, byte[] keyMaterial) { + mNonce = nonce; + mKeyMaterial = keyMaterial; + } + + /** + * Returns the nonce with which the key material was encrypted. + * + * @hide + */ + public byte[] getNonce() { + return mNonce; + } + + /** + * Returns the encrypted key material. + * + * @hide + */ + public byte[] getKeyMaterial() { + return mKeyMaterial; + } +} diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java index 3e37c1452349..97fdcefc9d56 100644 --- a/core/java/android/service/autofill/AutofillService.java +++ b/core/java/android/service/autofill/AutofillService.java @@ -65,7 +65,7 @@ import com.android.internal.os.SomeArgs; * <li>The service replies through {@link FillCallback#onSuccess(FillResponse)}. * <li>The Android System calls {@link #onDisconnected()} and unbinds from the * {@code AutofillService}. - * <li>The Android System displays an UI affordance with the options sent by the service. + * <li>The Android System displays an autofill UI with the options sent by the service. * <li>The user picks an option. * <li>The proper views are autofilled. * </ol> @@ -187,7 +187,7 @@ import com.android.internal.os.SomeArgs; * protect a dataset that contains sensitive information by requiring dataset authentication * (see {@link Dataset.Builder#setAuthentication(android.content.IntentSender)}), and to include * info about the "primary" field of the partition in the custom presentation for "secondary" - * fields — that would prevent a malicious app from getting the "primary" fields without the + * fields—that would prevent a malicious app from getting the "primary" fields without the * user realizing they're being released (for example, a malicious app could have fields for a * credit card number, verification code, and expiration date crafted in a way that just the latter * is visible; by explicitly indicating the expiration date is related to a given credit card @@ -438,6 +438,7 @@ import com.android.internal.os.SomeArgs; * AutofillValue password = passwordNode.getAutofillValue().getTextValue().toString(); * * save(username, password); + * </pre> * * <a name="Privacy"></a> * <h3>Privacy</h3> @@ -452,7 +453,12 @@ import com.android.internal.os.SomeArgs; * email address), the service should only use it locally (i.e., in the app's process) for * heuristics purposes, but it should not be sent to external servers. * - * </pre> + * <a name="FieldsClassification"></a> + * <h3>Metrics and fields classification</h3 + * + * <p>TODO(b/67867469): document it or remove this section; in particular, document the relationship + * between set/getUserData(), FillResponse.setFieldClassificationIds(), and + * FillEventHistory.getFieldsClassification. */ public abstract class AutofillService extends Service { private static final String TAG = "AutofillService"; diff --git a/core/java/android/service/autofill/AutofillServiceInfo.java b/core/java/android/service/autofill/AutofillServiceInfo.java index 1a9afccdabe2..5c7388f79a49 100644 --- a/core/java/android/service/autofill/AutofillServiceInfo.java +++ b/core/java/android/service/autofill/AutofillServiceInfo.java @@ -90,9 +90,7 @@ public final class AutofillServiceInfo { @Nullable private static TypedArray getMetaDataArray(PackageManager pm, ServiceInfo si) { // Check for permissions. - // TODO(b/37563972): remove support to BIND_AUTOFILL once clients use BIND_AUTOFILL_SERVICE - if (!Manifest.permission.BIND_AUTOFILL_SERVICE.equals(si.permission) - && !Manifest.permission.BIND_AUTOFILL.equals(si.permission)) { + if (!Manifest.permission.BIND_AUTOFILL_SERVICE.equals(si.permission)) { Log.w(TAG, "AutofillService from '" + si.packageName + "' does not require permission " + Manifest.permission.BIND_AUTOFILL_SERVICE); throw new SecurityException("Service does not require permission " diff --git a/core/java/android/service/autofill/BatchUpdates.java b/core/java/android/service/autofill/BatchUpdates.java new file mode 100644 index 000000000000..90acc881e171 --- /dev/null +++ b/core/java/android/service/autofill/BatchUpdates.java @@ -0,0 +1,216 @@ +/* + * 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.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; +import android.widget.RemoteViews; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; + +/** + * Defines actions to be applied to a {@link RemoteViews template presentation}. + * + * + * <p>It supports 2 types of actions: + * + * <ol> + * <li>{@link RemoteViews Actions} to be applied to the template. + * <li>{@link Transformation Transformations} to be applied on child views. + * </ol> + * + * <p>Typically used on {@link CustomDescription custom descriptions} to conditionally display + * differents views based on user input - see + * {@link CustomDescription.Builder#batchUpdate(Validator, BatchUpdates)} for more information. + */ +public final class BatchUpdates implements Parcelable { + + private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations; + private final RemoteViews mUpdates; + + private BatchUpdates(Builder builder) { + mTransformations = builder.mTransformations; + mUpdates = builder.mUpdates; + } + + /** @hide */ + @Nullable + public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() { + return mTransformations; + } + + /** @hide */ + @Nullable + public RemoteViews getUpdates() { + return mUpdates; + } + + /** + * Builder for {@link BatchUpdates} objects. + */ + public static class Builder { + private RemoteViews mUpdates; + + private boolean mDestroyed; + private ArrayList<Pair<Integer, InternalTransformation>> mTransformations; + + /** + * Applies the {@code updates} in the underlying presentation template. + * + * <p><b>Note:</b> The updates are applied before the + * {@link #transformChild(int, Transformation) transformations} are applied to the children + * views. + * + * @param updates a {@link RemoteViews} with the updated actions to be applied in the + * underlying presentation template. + * + * @return this builder + * @throws IllegalArgumentException if {@code condition} is not a class provided + * by the Android System. + */ + public Builder updateTemplate(@NonNull RemoteViews updates) { + throwIfDestroyed(); + mUpdates = Preconditions.checkNotNull(updates); + return this; + } + + /** + * Adds a transformation to replace the value of a child view with the fields in the + * screen. + * + * <p>When multiple transformations are added for the same child view, they are applied + * in the same order as added. + * + * <p><b>Note:</b> The transformations are applied after the + * {@link #updateTemplate(RemoteViews) updates} are applied to the presentation template. + * + * @param id view id of the children view. + * @param transformation an implementation provided by the Android System. + * @return this builder. + * @throws IllegalArgumentException if {@code transformation} is not a class provided + * by the Android System. + */ + public Builder transformChild(int id, @NonNull Transformation transformation) { + throwIfDestroyed(); + Preconditions.checkArgument((transformation instanceof InternalTransformation), + "not provided by Android System: " + transformation); + if (mTransformations == null) { + mTransformations = new ArrayList<>(); + } + mTransformations.add(new Pair<>(id, (InternalTransformation) transformation)); + return this; + } + + /** + * Creates a new {@link BatchUpdates} instance. + * + * @throws IllegalStateException if {@link #build()} was already called before or no call + * to {@link #updateTemplate(RemoteViews)} or {@link #transformChild(int, Transformation)} + * has been made. + */ + public BatchUpdates build() { + throwIfDestroyed(); + Preconditions.checkState(mUpdates != null || mTransformations != null, + "must call either updateTemplate() or transformChild() at least once"); + mDestroyed = true; + return new BatchUpdates(this); + } + + private void throwIfDestroyed() { + if (mDestroyed) { + throw new IllegalStateException("Already called #build()"); + } + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return new StringBuilder("BatchUpdates: [") + .append(", transformations=") + .append(mTransformations == null ? "N/A" : mTransformations.size()) + .append(", updates=").append(mUpdates) + .append("]").toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mTransformations == null) { + dest.writeIntArray(null); + } else { + final int size = mTransformations.size(); + final int[] ids = new int[size]; + final InternalTransformation[] values = new InternalTransformation[size]; + for (int i = 0; i < size; i++) { + final Pair<Integer, InternalTransformation> pair = mTransformations.get(i); + ids[i] = pair.first; + values[i] = pair.second; + } + dest.writeIntArray(ids); + dest.writeParcelableArray(values, flags); + } + dest.writeParcelable(mUpdates, flags); + } + public static final Parcelable.Creator<BatchUpdates> CREATOR = + new Parcelable.Creator<BatchUpdates>() { + @Override + public BatchUpdates createFromParcel(Parcel parcel) { + // 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. + final Builder builder = new Builder(); + final int[] ids = parcel.createIntArray(); + if (ids != null) { + final InternalTransformation[] values = + parcel.readParcelableArray(null, InternalTransformation.class); + final int size = ids.length; + for (int i = 0; i < size; i++) { + builder.transformChild(ids[i], values[i]); + } + } + final RemoteViews updates = parcel.readParcelable(null); + if (updates != null) { + builder.updateTemplate(updates); + } + return builder.build(); + } + + @Override + public BatchUpdates[] newArray(int size) { + return new BatchUpdates[size]; + } + }; +} diff --git a/core/java/android/service/autofill/CustomDescription.java b/core/java/android/service/autofill/CustomDescription.java index 9a4cbc415d64..b8e8b19f9786 100644 --- a/core/java/android/service/autofill/CustomDescription.java +++ b/core/java/android/service/autofill/CustomDescription.java @@ -19,11 +19,11 @@ package android.service.autofill; import static android.view.autofill.Helper.sDebug; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Activity; import android.app.PendingIntent; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; import android.util.Pair; import android.widget.RemoteViews; @@ -32,7 +32,7 @@ import com.android.internal.util.Preconditions; import java.util.ArrayList; /** - * Defines a custom description for the Save UI affordance. + * Defines a custom description for the autofill save UI. * * <p>This is useful when the autofill service needs to show a detailed view of what would be saved; * for example, when the screen contains a credit card, it could display a logo of the credit card @@ -67,18 +67,18 @@ import java.util.ArrayList; * // Image child - different logo for each bank, based on credit card prefix * builder.addChild(R.id.templateccLogo, * new ImageTransformation.Builder(ccNumberId) - * .addOption(Pattern.compile(""^4815.*$"), R.drawable.ic_credit_card_logo1) - * .addOption(Pattern.compile(""^1623.*$"), R.drawable.ic_credit_card_logo2) - * .addOption(Pattern.compile(""^42.*$"), R.drawable.ic_credit_card_logo3) + * .addOption(Pattern.compile("^4815.*$"), R.drawable.ic_credit_card_logo1) + * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2) + * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3) * .build(); * // Masked credit card number (as .....LAST_4_DIGITS) * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation - * .Builder(ccNumberId, Pattern.compile(""^.*(\\d\\d\\d\\d)$"), "...$1") + * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1") * .build(); * // Expiration date as MM / YYYY: * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation - * .Builder(ccExpMonthId, Pattern.compile(""^(\\d\\d)$"), "Exp: $1") - * .addField(ccExpYearId, Pattern.compile(""^(\\d\\d)$"), "/$1") + * .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1") + * .addField(ccExpYearId, Pattern.compile("^(\\d\\d)$"), "/$1") * .build(); * </pre> * @@ -87,47 +87,43 @@ import java.util.ArrayList; */ public final class CustomDescription implements Parcelable { - private static final String TAG = "CustomDescription"; - private final RemoteViews mPresentation; private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations; + private final ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates; private CustomDescription(Builder builder) { mPresentation = builder.mPresentation; mTransformations = builder.mTransformations; + mUpdates = builder.mUpdates; } /** @hide */ - public RemoteViews getPresentation(ValueFinder finder) { - if (mTransformations != null) { - final int size = mTransformations.size(); - if (sDebug) Log.d(TAG, "getPresentation(): applying " + size + " transformations"); - for (int i = 0; i < size; i++) { - final Pair<Integer, InternalTransformation> pair = mTransformations.get(i); - final int id = pair.first; - final InternalTransformation transformation = pair.second; - if (sDebug) Log.d(TAG, "#" + i + ": " + transformation); - - try { - transformation.apply(finder, mPresentation, id); - } catch (Exception e) { - // Do not log full exception to avoid PII leaking - Log.e(TAG, "Could not apply transformation " + transformation + ": " - + e.getClass()); - return null; - } - } - } + @Nullable + public RemoteViews getPresentation() { return mPresentation; } + /** @hide */ + @Nullable + public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() { + return mTransformations; + } + + /** @hide */ + @Nullable + public ArrayList<Pair<InternalValidator, BatchUpdates>> getUpdates() { + return mUpdates; + } + /** * Builder for {@link CustomDescription} objects. */ public static class Builder { private final RemoteViews mPresentation; + private boolean mDestroyed; private ArrayList<Pair<Integer, InternalTransformation>> mTransformations; + private ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates; /** * Default constructor. @@ -135,7 +131,7 @@ public final class CustomDescription implements Parcelable { * <p><b>Note:</b> If any child view of presentation triggers a * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise - * it might not be triggered or the Save affordance might not be shown when its activity + * it might not be triggered or the autofill save UI might not be shown when its activity * is finished: * <ul> * <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag. @@ -145,9 +141,11 @@ public final class CustomDescription implements Parcelable { * </ul> * * @param parentPresentation template presentation with (optional) children views. + * @throws NullPointerException if {@code parentPresentation} is null (on Android + * {@link android.os.Build.VERSION_CODES#P} or higher). */ - public Builder(RemoteViews parentPresentation) { - mPresentation = parentPresentation; + public Builder(@NonNull RemoteViews parentPresentation) { + mPresentation = Preconditions.checkNotNull(parentPresentation); } /** @@ -164,6 +162,7 @@ public final class CustomDescription implements Parcelable { * by the Android System. */ public Builder addChild(int id, @NonNull Transformation transformation) { + throwIfDestroyed(); Preconditions.checkArgument((transformation instanceof InternalTransformation), "not provided by Android System: " + transformation); if (mTransformations == null) { @@ -174,11 +173,109 @@ public final class CustomDescription implements Parcelable { } /** + * Updates the {@link RemoteViews presentation template} when a condition is satisfied. + * + * <p>The updates are applied in the sequence they are added, after the + * {@link #addChild(int, Transformation) transformations} are applied to the children + * views. + * + * <p>For example, to make children views visible when fields are not empty: + * + * <pre class="prettyprint"> + * RemoteViews template = new RemoteViews(pgkName, R.layout.my_full_template); + * + * Pattern notEmptyPattern = Pattern.compile(".+"); + * Validator hasAddress = new RegexValidator(addressAutofillId, notEmptyPattern); + * Validator hasCcNumber = new RegexValidator(ccNumberAutofillId, notEmptyPattern); + * + * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_full_template) + * addressUpdates.setViewVisibility(R.id.address, View.VISIBLE); + * + * // Make address visible + * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder() + * .updateTemplate(addressUpdates) + * .build(); + * + * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_full_template) + * ccUpdates.setViewVisibility(R.id.cc_number, View.VISIBLE); + * + * // Mask credit card number (as .....LAST_4_DIGITS) and make it visible + * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder() + * .updateTemplate(ccUpdates) + * .transformChild(R.id.templateCcNumber, new CharSequenceTransformation + * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1") + * .build()) + * .build(); + * + * CustomDescription customDescription = new CustomDescription.Builder(template) + * .batchUpdate(hasAddress, addressBatchUpdates) + * .batchUpdate(hasCcNumber, ccBatchUpdates) + * .build(); + * </pre> + * + * <p>Another approach is to add a child first, then apply the transformations. Example: + * + * <pre class="prettyprint"> + * RemoteViews template = new RemoteViews(pgkName, R.layout.my_base_template); + * + * RemoteViews addressPresentation = new RemoteViews(pgkName, R.layout.address) + * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_template) + * addressUpdates.addView(R.id.parentId, addressPresentation); + * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder() + * .updateTemplate(addressUpdates) + * .build(); + * + * RemoteViews ccPresentation = new RemoteViews(pgkName, R.layout.cc) + * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_template) + * ccUpdates.addView(R.id.parentId, ccPresentation); + * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder() + * .updateTemplate(ccUpdates) + * .transformChild(R.id.templateCcNumber, new CharSequenceTransformation + * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1") + * .build()) + * .build(); + * + * CustomDescription customDescription = new CustomDescription.Builder(template) + * .batchUpdate(hasAddress, addressBatchUpdates) + * .batchUpdate(hasCcNumber, ccBatchUpdates) + * .build(); + * </pre> + * + * @param condition condition used to trigger the updates. + * @param updates actions to be applied to the + * {@link #CustomDescription.Builder(RemoteViews) template presentation} when the condition + * is satisfied. + * + * @return this builder + * @throws IllegalArgumentException if {@code condition} is not a class provided + * by the Android System. + */ + public Builder batchUpdate(@NonNull Validator condition, @NonNull BatchUpdates updates) { + throwIfDestroyed(); + Preconditions.checkArgument((condition instanceof InternalValidator), + "not provided by Android System: " + condition); + Preconditions.checkNotNull(updates); + if (mUpdates == null) { + mUpdates = new ArrayList<>(); + } + mUpdates.add(new Pair<>((InternalValidator) condition, updates)); + return this; + } + + /** * Creates a new {@link CustomDescription} instance. */ public CustomDescription build() { + throwIfDestroyed(); + mDestroyed = true; return new CustomDescription(this); } + + private void throwIfDestroyed() { + if (mDestroyed) { + throw new IllegalStateException("Already called #build()"); + } + } } ///////////////////////////////////// @@ -190,7 +287,10 @@ public final class CustomDescription implements Parcelable { return new StringBuilder("CustomDescription: [presentation=") .append(mPresentation) - .append(", transformations=").append(mTransformations) + .append(", transformations=") + .append(mTransformations == null ? "N/A" : mTransformations.size()) + .append(", updates=") + .append(mUpdates == null ? "N/A" : mUpdates.size()) .append("]").toString(); } @@ -205,6 +305,8 @@ public final class CustomDescription implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(mPresentation, flags); + if (mPresentation == null) return; + if (mTransformations == null) { dest.writeIntArray(null); } else { @@ -219,6 +321,21 @@ public final class CustomDescription implements Parcelable { dest.writeIntArray(ids); dest.writeParcelableArray(values, flags); } + if (mUpdates == null) { + dest.writeParcelableArray(null, flags); + } else { + final int size = mUpdates.size(); + final InternalValidator[] conditions = new InternalValidator[size]; + final BatchUpdates[] updates = new BatchUpdates[size]; + + for (int i = 0; i < size; i++) { + final Pair<InternalValidator, BatchUpdates> pair = mUpdates.get(i); + conditions[i] = pair.first; + updates[i] = pair.second; + } + dest.writeParcelableArray(conditions, flags); + dest.writeParcelableArray(updates, flags); + } } public static final Parcelable.Creator<CustomDescription> CREATOR = new Parcelable.Creator<CustomDescription>() { @@ -227,7 +344,10 @@ public final class CustomDescription implements Parcelable { // 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. - final Builder builder = new Builder(parcel.readParcelable(null)); + final RemoteViews parentPresentation = parcel.readParcelable(null); + if (parentPresentation == null) return null; + + final Builder builder = new Builder(parentPresentation); final int[] ids = parcel.createIntArray(); if (ids != null) { final InternalTransformation[] values = @@ -237,6 +357,15 @@ public final class CustomDescription implements Parcelable { builder.addChild(ids[i], values[i]); } } + final InternalValidator[] conditions = + parcel.readParcelableArray(null, InternalValidator.class); + if (conditions != null) { + final BatchUpdates[] updates = parcel.readParcelableArray(null, BatchUpdates.class); + final int size = conditions.length; + for (int i = 0; i < size; i++) { + builder.batchUpdate(conditions[i], updates[i]); + } + } return builder.build(); } diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index 65b0efcbe032..266bcda7797a 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -29,32 +29,77 @@ import android.widget.RemoteViews; import com.android.internal.util.Preconditions; +import java.io.Serializable; import java.util.ArrayList; +import java.util.regex.Pattern; /** - * A dataset object represents a group of key/value pairs used to autofill parts of a screen. + * A dataset object represents a group of fields (key / value pairs) used to autofill parts of a + * screen. * - * <p>In its simplest form, a dataset contains one or more key / value pairs (comprised of - * {@link AutofillId} and {@link AutofillValue} respectively); and one or more - * {@link RemoteViews presentation} for these pairs (a pair could have its own - * {@link RemoteViews presentation}, or use the default {@link RemoteViews presentation} associated - * with the whole dataset). When an autofill service returns datasets in a {@link FillResponse} + * <a name="BasicUsage"></a> + * <h3>Basic usage</h3> + * + * <p>In its simplest form, a dataset contains one or more fields (comprised of + * an {@link AutofillId id}, a {@link AutofillValue value}, and an optional filter + * {@link Pattern regex}); and one or more {@link RemoteViews presentations} for these fields + * (each field could have its own {@link RemoteViews presentation}, or use the default + * {@link RemoteViews presentation} associated with the whole dataset). + * + * <p>When an autofill service returns datasets in a {@link FillResponse} * and the screen input is focused in a view that is present in at least one of these datasets, - * the Android System displays a UI affordance containing the {@link RemoteViews presentation} of + * the Android System displays a UI containing the {@link RemoteViews presentation} of * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a - * dataset from the affordance, all views in that dataset are autofilled. + * dataset from the UI, all views in that dataset are autofilled. + * + * <a name="Authentication"></a> + * <h3>Dataset authentication</h3> + * + * <p>In a more sophisticated form, the dataset values can be protected until the user authenticates + * the dataset—in that case, when a dataset is selected by the user, the Android System + * launches an intent set by the service to "unlock" the dataset. + * + * <p>For example, when a data set contains credit card information (such as number, + * expiration date, and verification code), you could provide a dataset presentation saying + * "Tap to authenticate". Then when the user taps that option, you would launch an activity asking + * the user to enter the credit card code, and if the user enters a valid code, you could then + * "unlock" the dataset. + * + * <p>You can also use authenticated datasets to offer an interactive UI for the user. For example, + * if the activity being autofilled is an account creation screen, you could use an authenticated + * dataset to automatically generate a random password for the user. + * + * <p>See {@link Dataset.Builder#setAuthentication(IntentSender)} for more details about the dataset + * authentication mechanism. * - * <p>In a more sophisticated form, the dataset value can be protected until the user authenticates - * the dataset - see {@link Dataset.Builder#setAuthentication(IntentSender)}. + * <a name="Filtering"></a> + * <h3>Filtering</h3> + * <p>The autofill UI automatically changes which values are shown based on value of the view + * anchoring it, following the rules below: + * <ol> + * <li>If the view's {@link android.view.View#getAutofillValue() autofill value} is not + * {@link AutofillValue#isText() text} or is empty, all datasets are shown. + * <li>Datasets that have a filter regex (set through + * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern)} or + * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}) and whose + * regex matches the view's text value converted to lower case are shown. + * <li>Datasets that do not require authentication, have a field value that is + * {@link AutofillValue#isText() text} and whose {@link AutofillValue#getTextValue() value} starts + * with the lower case value of the view's text are shown. + * <li>All other datasets are hidden. + * </ol> * - * @see android.service.autofill.AutofillService for more information and examples about the - * role of datasets in the autofill workflow. + * <a name="MoreInfo"></a> + * <h3>More information</h3> + * <p>See {@link android.service.autofill.AutofillService} for more information and examples about + * the role of datasets in the autofill workflow. */ public final class Dataset implements Parcelable { private final ArrayList<AutofillId> mFieldIds; private final ArrayList<AutofillValue> mFieldValues; private final ArrayList<RemoteViews> mFieldPresentations; + private final ArrayList<Pattern> mFieldFilters; private final RemoteViews mPresentation; private final IntentSender mAuthentication; @Nullable String mId; @@ -63,6 +108,7 @@ public final class Dataset implements Parcelable { mFieldIds = builder.mFieldIds; mFieldValues = builder.mFieldValues; mFieldPresentations = builder.mFieldPresentations; + mFieldFilters = builder.mFieldFilters; mPresentation = builder.mPresentation; mAuthentication = builder.mAuthentication; mId = builder.mId; @@ -85,6 +131,12 @@ public final class Dataset implements Parcelable { } /** @hide */ + @Nullable + public Pattern getFilter(int index) { + return mFieldFilters.get(index); + } + + /** @hide */ public @Nullable IntentSender getAuthentication() { return mAuthentication; } @@ -98,11 +150,21 @@ public final class Dataset implements Parcelable { public String toString() { if (!sDebug) return super.toString(); - return new StringBuilder("Dataset " + mId + " [") - .append("fieldIds=").append(mFieldIds) + final StringBuilder builder = new StringBuilder("Dataset[id="); + if (mId == null) { + builder.append("null"); + } else { + // Cannot disclose id because it could contain PII. + builder.append(mId.length()).append("_chars"); + } + + return builder + .append(", fieldIds=").append(mFieldIds) .append(", fieldValues=").append(mFieldValues) .append(", fieldPresentations=") .append(mFieldPresentations == null ? 0 : mFieldPresentations.size()) + .append(", fieldFilters=") + .append(mFieldFilters == null ? 0 : mFieldFilters.size()) .append(", hasPresentation=").append(mPresentation != null) .append(", hasAuthentication=").append(mAuthentication != null) .append(']').toString(); @@ -127,6 +189,7 @@ public final class Dataset implements Parcelable { private ArrayList<AutofillId> mFieldIds; private ArrayList<AutofillValue> mFieldValues; private ArrayList<RemoteViews> mFieldPresentations; + private ArrayList<Pattern> mFieldFilters; private RemoteViews mPresentation; private IntentSender mAuthentication; private boolean mDestroyed; @@ -146,13 +209,19 @@ public final class Dataset implements Parcelable { * Creates a new builder for a dataset where each field will be visualized independently. * * <p>When using this constructor, fields must be set through - * {@link #setValue(AutofillId, AutofillValue, RemoteViews)}. + * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or + * {@link #setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}. */ public Builder() { } /** - * Requires a dataset authentication before autofilling the activity with this dataset. + * Triggers a custom UI before before autofilling the screen with the contents of this + * dataset. + * + * <p><b>Note:</b> Although the name of this method suggests that it should be used just for + * authentication flow, it can be used for other advanced flows; see {@link AutofillService} + * for examples. * * <p>This method is called when you need to provide an authentication * UI for the data set. For example, when a data set contains credit card information @@ -182,12 +251,12 @@ public final class Dataset implements Parcelable { * credit card information without the CVV for the data set in the {@link FillResponse * response} then the returned data set should contain the CVV entry. * - * <p><b>NOTE:</b> Do not make the provided pending intent + * <p><b>Note:</b> Do not make the provided pending intent * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the * platform needs to fill in the authentication arguments. * * @param authentication Intent to an activity with your authentication flow. - * @return This builder. + * @return this builder. * * @see android.app.PendingIntent */ @@ -198,20 +267,26 @@ public final class Dataset implements Parcelable { } /** - * Sets the id for the dataset so its usage history can be retrieved later. + * Sets the id for the dataset so its usage can be tracked. + * + * <p>Dataset usage can be tracked for 2 purposes: * - * <p>The id of the last selected dataset can be read from - * {@link AutofillService#getFillEventHistory()}. If the id is not set it will not be clear - * if a dataset was selected as {@link AutofillService#getFillEventHistory()} uses - * {@code null} to indicate that no dataset was selected. + * <ul> + * <li>For statistical purposes, the service can call + * {@link AutofillService#getFillEventHistory()} when handling {@link + * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)} + * calls. + * <li>For normal autofill workflow, the service can call + * {@link SaveRequest#getDatasetIds()} when handling + * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} calls. + * </ul> * * @param id id for this dataset or {@code null} to unset. - - * @return This builder. + * + * @return this builder. */ public @NonNull Builder setId(@Nullable String id) { throwIfDestroyed(); - mId = id; return this; } @@ -219,21 +294,29 @@ public final class Dataset implements Parcelable { /** * Sets the value of a field. * + * <b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, this method would + * throw an {@link IllegalStateException} if this builder was constructed without a + * {@link RemoteViews presentation}. Android {@link android.os.Build.VERSION_CODES#P} and + * higher removed this restriction because datasets used as an + * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT + * authentication result} do not need a presentation. But if you don't set the presentation + * in the constructor in a dataset that is meant to be shown to the user, the autofill UI + * for this field will not be displayed. + * + * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and + * higher, datasets that require authentication can be also be filtered by passing a + * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter. + * * @param id id returned by {@link * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. * @param value value to be autofilled. Pass {@code null} if you do not have the value * but the target view is a logical part of the dataset. For example, if - * the dataset needs an authentication and you have no access to the value. - * @return This builder. - * @throws IllegalStateException if the builder was constructed without a - * {@link RemoteViews presentation}. + * the dataset needs authentication and you have no access to the value. + * @return this builder. */ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) { throwIfDestroyed(); - if (mPresentation == null) { - throw new IllegalStateException("Dataset presentation not set on constructor"); - } - setValueAndPresentation(id, value, null); + setLifeTheUniverseAndEverything(id, value, null, null); return this; } @@ -241,41 +324,116 @@ public final class Dataset implements Parcelable { * Sets the value of a field, using a custom {@link RemoteViews presentation} to * visualize it. * + * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and + * higher, datasets that require authentication can be also be filtered by passing a + * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter. + * * @param id id returned by {@link * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. - * @param value value to be auto filled. Pass {@code null} if you do not have the value + * @param value the value to be autofilled. Pass {@code null} if you do not have the value * but the target view is a logical part of the dataset. For example, if - * the dataset needs an authentication and you have no access to the value. - * Filtering matches any user typed string to {@code null} values. - * @param presentation The presentation used to visualize this field. - * @return This builder. + * the dataset needs authentication and you have no access to the value. + * @param presentation the presentation used to visualize this field. + * @return this builder. + * */ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation) { throwIfDestroyed(); Preconditions.checkNotNull(presentation, "presentation cannot be null"); - setValueAndPresentation(id, value, presentation); + setLifeTheUniverseAndEverything(id, value, presentation, null); + return this; + } + + /** + * Sets the value of a field using an <a href="#Filtering">explicit filter</a>. + * + * <p>This method is typically used when the dataset requires authentication and the service + * does not know its value but wants to hide the dataset after the user enters a minimum + * number of characters. For example, if the dataset represents a credit card number and the + * service does not want to show the "Tap to authenticate" message until the user tapped + * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. + * + * <p><b>Note:</b> If the dataset requires authentication but the service knows its text + * value it's easier to filter by calling {@link #setValue(AutofillId, AutofillValue)} and + * use the value to filter. + * + * @param id id returned by {@link + * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. + * @param value the value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if + * the dataset needs authentication and you have no access to the value. + * @param filter regex used to determine if the dataset should be shown in the autofill UI. + * + * @return this builder. + * @throws IllegalStateException if the builder was constructed without a + * {@link RemoteViews presentation}. + */ + public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, + @NonNull Pattern filter) { + throwIfDestroyed(); + Preconditions.checkNotNull(filter, "filter cannot be null"); + Preconditions.checkState(mPresentation != null, + "Dataset presentation not set on constructor"); + setLifeTheUniverseAndEverything(id, value, null, filter); + return this; + } + + /** + * Sets the value of a field, using a custom {@link RemoteViews presentation} to + * visualize it and a <a href="#Filtering">explicit filter</a>. + * + * <p>This method is typically used when the dataset requires authentication and the service + * does not know its value but wants to hide the dataset after the user enters a minimum + * number of characters. For example, if the dataset represents a credit card number and the + * service does not want to show the "Tap to authenticate" message until the user tapped + * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}. + * + * <p><b>Note:</b> If the dataset requires authentication but the service knows its text + * value it's easier to filter by calling + * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter. + * + * @param id id returned by {@link + * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. + * @param value the value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if + * the dataset needs authentication and you have no access to the value. + * @param presentation the presentation used to visualize this field. + * @param filter regex used to determine if the dataset should be shown in the autofill UI. + * + * @return this builder. + */ + public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, + @NonNull Pattern filter, @NonNull RemoteViews presentation) { + throwIfDestroyed(); + Preconditions.checkNotNull(filter, "filter cannot be null"); + Preconditions.checkNotNull(presentation, "presentation cannot be null"); + setLifeTheUniverseAndEverything(id, value, presentation, filter); return this; } - private void setValueAndPresentation(AutofillId id, AutofillValue value, - RemoteViews presentation) { + private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, + @Nullable AutofillValue value, @Nullable RemoteViews presentation, + @Nullable Pattern filter) { Preconditions.checkNotNull(id, "id cannot be null"); if (mFieldIds != null) { final int existingIdx = mFieldIds.indexOf(id); if (existingIdx >= 0) { mFieldValues.set(existingIdx, value); mFieldPresentations.set(existingIdx, presentation); + mFieldFilters.set(existingIdx, filter); return; } } else { mFieldIds = new ArrayList<>(); mFieldValues = new ArrayList<>(); mFieldPresentations = new ArrayList<>(); + mFieldFilters = new ArrayList<>(); } mFieldIds.add(id); mFieldValues.add(value); mFieldPresentations.add(presentation); + mFieldFilters.add(filter); } /** @@ -283,8 +441,9 @@ public final class Dataset implements Parcelable { * * <p>You should not interact with this builder once this method is called. * - * <p>It is required that you specify at least one field before calling this method. It's - * also mandatory to provide a presentation view to visualize the data set in the UI. + * @throws IllegalStateException if no field was set (through + * {@link #setValue(AutofillId, AutofillValue)} or + * {@link #setValue(AutofillId, AutofillValue, RemoteViews)}). * * @return The built dataset. */ @@ -292,7 +451,7 @@ public final class Dataset implements Parcelable { throwIfDestroyed(); mDestroyed = true; if (mFieldIds == null) { - throw new IllegalArgumentException("at least one value must be set"); + throw new IllegalStateException("at least one value must be set"); } return new Dataset(this); } @@ -316,9 +475,10 @@ public final class Dataset implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeParcelable(mPresentation, flags); - parcel.writeTypedArrayList(mFieldIds, flags); - parcel.writeTypedArrayList(mFieldValues, flags); + parcel.writeTypedList(mFieldIds, flags); + parcel.writeTypedList(mFieldValues, flags); parcel.writeParcelableList(mFieldPresentations, flags); + parcel.writeSerializable(mFieldFilters); parcel.writeParcelable(mAuthentication, flags); parcel.writeString(mId); } @@ -333,10 +493,14 @@ public final class Dataset implements Parcelable { final Builder builder = (presentation == null) ? new Builder() : new Builder(presentation); - final ArrayList<AutofillId> ids = parcel.readTypedArrayList(null); - final ArrayList<AutofillValue> values = parcel.readTypedArrayList(null); + final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR); + final ArrayList<AutofillValue> values = + parcel.createTypedArrayList(AutofillValue.CREATOR); final ArrayList<RemoteViews> presentations = new ArrayList<>(); parcel.readParcelableList(presentations, null); + @SuppressWarnings("unchecked") + final ArrayList<Serializable> filters = + (ArrayList<Serializable>) parcel.readSerializable(); final int idCount = (ids != null) ? ids.size() : 0; final int valueCount = (values != null) ? values.size() : 0; for (int i = 0; i < idCount; i++) { @@ -344,7 +508,8 @@ public final class Dataset implements Parcelable { final AutofillValue value = (valueCount > i) ? values.get(i) : null; final RemoteViews fieldPresentation = presentations.isEmpty() ? null : presentations.get(i); - builder.setValueAndPresentation(id, value, fieldPresentation); + final Pattern filter = (Pattern) filters.get(i); + builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, filter); } builder.setAuthentication(parcel.readParcelable(null)); builder.setId(parcel.readString()); diff --git a/core/java/android/service/autofill/EditDistanceScorer.java b/core/java/android/service/autofill/EditDistanceScorer.java new file mode 100644 index 000000000000..e25cd0467cc8 --- /dev/null +++ b/core/java/android/service/autofill/EditDistanceScorer.java @@ -0,0 +1,103 @@ +/* + * 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.service.autofill; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.autofill.AutofillValue; + +/** + * Helper used to calculate the classification score between an actual {@link AutofillValue} filled + * by the user and the expected value predicted by an autofill service. + * + * TODO(b/67867469): + * - improve javadoc + * - document algorithm / copy from InternalScorer + * - unhide / remove testApi + * @hide + */ +@TestApi +public final class EditDistanceScorer extends InternalScorer implements Scorer, Parcelable { + + private static final EditDistanceScorer sInstance = new EditDistanceScorer(); + + /** + * Gets the singleton instance. + */ + public static EditDistanceScorer getInstance() { + return sInstance; + } + + private EditDistanceScorer() { + } + + @Override + public float getScore(@NonNull AutofillValue actualValue, @NonNull String userData) { + if (actualValue == null || !actualValue.isText() || userData == null) return 0; + // TODO(b/67867469): implement edit distance - currently it's returning either 0, 100%, or + // partial match when number of chars match + final String textValue = actualValue.getTextValue().toString(); + final int total = textValue.length(); + if (total != userData.length()) return 0F; + + int matches = 0; + for (int i = 0; i < total; i++) { + if (Character.toLowerCase(textValue.charAt(i)) == Character + .toLowerCase(userData.charAt(i))) { + matches++; + } + } + + return ((float) matches) / total; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + return "EditDistanceScorer"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + // Do nothing + } + + public static final Parcelable.Creator<EditDistanceScorer> CREATOR = + new Parcelable.Creator<EditDistanceScorer>() { + @Override + public EditDistanceScorer createFromParcel(Parcel parcel) { + return EditDistanceScorer.getInstance(); + } + + @Override + public EditDistanceScorer[] newArray(int size) { + return new EditDistanceScorer[size]; + } + }; +} diff --git a/core/java/android/service/autofill/FieldClassification.java b/core/java/android/service/autofill/FieldClassification.java new file mode 100644 index 000000000000..0a60208c45a7 --- /dev/null +++ b/core/java/android/service/autofill/FieldClassification.java @@ -0,0 +1,192 @@ +/* + * 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.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.autofill.Helper; + +import com.android.internal.util.Preconditions; + +import com.google.android.collect.Lists; + +import java.util.List; + +/** + * Gets the <a href="#FieldsClassification">fields classification</a> results for a given field. + * + * TODO(b/67867469): + * - improve javadoc + * - unhide / remove testApi + * + * @hide + */ +@TestApi +public final class FieldClassification implements Parcelable { + + private final Match mMatch; + + /** @hide */ + public FieldClassification(@NonNull Match match) { + mMatch = Preconditions.checkNotNull(match); + } + + /** + * Gets the {@link Match matches} with the highest {@link Match#getScore() scores}. + * + * <p><b>Note:</b> There's no guarantee of how many matches will be returned. In fact, + * the Android System might return just the top match to minimize the impact of field + * classification in the device's health. + */ + @NonNull + public List<Match> getMatches() { + return Lists.newArrayList(mMatch); + } + + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "FieldClassification: " + mMatch; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mMatch, flags); + } + + public static final Parcelable.Creator<FieldClassification> CREATOR = + new Parcelable.Creator<FieldClassification>() { + + @Override + public FieldClassification createFromParcel(Parcel parcel) { + return new FieldClassification(parcel.readParcelable(null)); + } + + @Override + public FieldClassification[] newArray(int size) { + return new FieldClassification[size]; + } + }; + + /** + * Gets the score of a {@link UserData} entry for the field. + * + * TODO(b/67867469): + * - improve javadoc + * - unhide / remove testApi + * + * @hide + */ + @TestApi + public static final class Match implements Parcelable { + + private final String mRemoteId; + private final float mScore; + + /** @hide */ + public Match(String remoteId, float score) { + mRemoteId = Preconditions.checkNotNull(remoteId); + mScore = score; + } + + /** + * Gets the remote id of the {@link UserData} entry. + */ + @NonNull + public String getRemoteId() { + return mRemoteId; + } + + /** + * Gets a score between the value of this field and the value of the {@link UserData} entry. + * + * <p>The score is based in a case-insensitive comparisson of all characters from both the + * field value and the user data entry, and it ranges from {@code 0} to {@code 1000000}: + * <ul> + * <li>{@code 1.0} represents a full match ({@code 100%}). + * <li>{@code 0.0} represents a full mismatch ({@code 0%}). + * <li>Any other value is a partial match. + * </ul> + * + * <p>How the score is calculated depends on the algorithm used by the Android System. + * For example, if the user data is {@code "abc"} and the field value us {@code " abc"}, + * the result could be: + * <ul> + * <li>{@code 1.0} if the algorithm trims the values. + * <li>{@code 0.0} if the algorithm compares the values sequentially. + * <li>{@code 0.75} if the algorithm consideres that 3/4 (75%) of the characters match. + * </ul> + * + * <p>Currently, the autofill service cannot configure the algorithm. + */ + public float getScore() { + return mScore; + } + + @Override + public String toString() { + if (!sDebug) return super.toString(); + + final StringBuilder string = new StringBuilder("Match: remoteId="); + Helper.appendRedacted(string, mRemoteId); + return string.append(", score=").append(mScore).toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mRemoteId); + parcel.writeFloat(mScore); + } + + @SuppressWarnings("hiding") + public static final Parcelable.Creator<Match> CREATOR = new Parcelable.Creator<Match>() { + + @Override + public Match createFromParcel(Parcel parcel) { + return new Match(parcel.readString(), parcel.readFloat()); + } + + @Override + public Match[] newArray(int size) { + return new Match[size]; + } + }; + } +} diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java index 768e743612ed..facad2d1ab30 100644 --- a/core/java/android/service/autofill/FillEventHistory.java +++ b/core/java/android/service/autofill/FillEventHistory.java @@ -16,20 +16,34 @@ package android.service.autofill; +import static android.view.autofill.Helper.sVerbose; + 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; import android.os.Parcelable; +import android.service.autofill.FieldClassification.Match; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; /** * Describes what happened after the last @@ -48,10 +62,7 @@ import java.util.List; * the history will clear out after some pre-defined time). */ public final class FillEventHistory implements Parcelable { - /** - * Not in parcel. The UID of the {@link AutofillService} that created the {@link FillResponse}. - */ - private final int mServiceUid; + private static final String TAG = "FillEventHistory"; /** * Not in parcel. The ID of the autofill session that created the {@link FillResponse}. @@ -61,17 +72,6 @@ public final class FillEventHistory implements Parcelable { @Nullable private final Bundle mClientState; @Nullable List<Event> mEvents; - /** - * Gets the UID of the {@link AutofillService} that created the {@link FillResponse}. - * - * @return The UID of the {@link AutofillService} - * - * @hide - */ - public int getServiceUid() { - return mServiceUid; - } - /** @hide */ public int getSessionId() { return mSessionId; @@ -83,7 +83,10 @@ public final class FillEventHistory implements Parcelable { * <p><b>Note: </b>the state is associated with the app that was autofilled in the previous * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)} * , which is not necessary the same app being autofilled now. + * + * @deprecated use {@link #getEvents()} then {@link Event#getClientState()} instead. */ + @Deprecated @Nullable public Bundle getClientState() { return mClientState; } @@ -111,31 +114,52 @@ public final class FillEventHistory implements Parcelable { /** * @hide */ - public FillEventHistory(int serviceUid, int sessionId, @Nullable Bundle clientState) { + public FillEventHistory(int sessionId, @Nullable Bundle clientState) { mClientState = clientState; - mServiceUid = serviceUid; mSessionId = sessionId; } @Override + public String toString() { + return mEvents == null ? "no events" : mEvents.toString(); + } + + @Override public int describeContents() { return 0; } @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeBundle(mClientState); - + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeBundle(mClientState); if (mEvents == null) { - dest.writeInt(0); + parcel.writeInt(0); } else { - dest.writeInt(mEvents.size()); + parcel.writeInt(mEvents.size()); int numEvents = mEvents.size(); for (int i = 0; i < numEvents; i++) { Event event = mEvents.get(i); - dest.writeInt(event.getType()); - dest.writeString(event.getDatasetId()); + parcel.writeInt(event.mEventType); + parcel.writeString(event.mDatasetId); + parcel.writeBundle(event.mClientState); + parcel.writeStringList(event.mSelectedDatasetIds); + parcel.writeArraySet(event.mIgnoredDatasetIds); + parcel.writeTypedList(event.mChangedFieldIds); + parcel.writeStringList(event.mChangedDatasetIds); + + parcel.writeTypedList(event.mManuallyFilledFieldIds); + if (event.mManuallyFilledFieldIds != null) { + final int size = event.mManuallyFilledFieldIds.size(); + for (int j = 0; j < size; j++) { + parcel.writeStringList(event.mManuallyFilledDatasetIds.get(j)); + } + } + final AutofillId[] detectedFields = event.mDetectedFieldIds; + parcel.writeParcelableArray(detectedFields, flags); + if (detectedFields != null) { + parcel.writeParcelableArray(event.mDetectedMatches, flags); + } } } } @@ -173,17 +197,61 @@ public final class FillEventHistory implements Parcelable { /** A save UI was shown. */ public static final int TYPE_SAVE_SHOWN = 3; + /** + * A committed autofill context for which the autofill service provided datasets. + * + * <p>This event is useful to track: + * <ul> + * <li>Which datasets (if any) were selected by the user + * ({@link #getSelectedDatasetIds()}). + * <li>Which datasets (if any) were NOT selected by the user + * ({@link #getIgnoredDatasetIds()}). + * <li>Which fields in the selected datasets were changed by the user after the dataset + * was selected ({@link #getChangedFields()}. + * </ul> + * + * <p><b>Note: </b>This event is only generated when: + * <ul> + * <li>The autofill context is committed. + * <li>The service provides at least one dataset in the + * {@link FillResponse fill responses} associated with the context. + * <li>The last {@link FillResponse fill responses} associated with the context has the + * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} flag. + * </ul> + * + * <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 */ @IntDef( value = {TYPE_DATASET_SELECTED, TYPE_DATASET_AUTHENTICATION_SELECTED, TYPE_AUTHENTICATION_SELECTED, - TYPE_SAVE_SHOWN}) + TYPE_SAVE_SHOWN, + TYPE_CONTEXT_COMMITTED}) @Retention(RetentionPolicy.SOURCE) @interface EventIds{} @EventIds private final int mEventType; @Nullable private final String mDatasetId; + @Nullable private final Bundle mClientState; + + // Note: mSelectedDatasetIds is stored as List<> instead of Set because Session already + // stores it as List + @Nullable private final List<String> mSelectedDatasetIds; + @Nullable private final ArraySet<String> mIgnoredDatasetIds; + + @Nullable private final ArrayList<AutofillId> mChangedFieldIds; + @Nullable private final ArrayList<String> mChangedDatasetIds; + + @Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds; + @Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds; + + @Nullable private final AutofillId[] mDetectedFieldIds; + @Nullable private final Match[] mDetectedMatches; /** * Returns the type of the event. @@ -204,18 +272,251 @@ public final class FillEventHistory implements Parcelable { } /** + * Returns the client state from the {@link FillResponse} used to generate this event. + * + * <p><b>Note: </b>the state is associated with the app that was autofilled in the previous + * {@link + * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}, + * which is not necessary the same app being autofilled now. + */ + @Nullable public Bundle getClientState() { + return mClientState; + } + + /** + * Returns which datasets were selected by the user. + * + * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}. + */ + @NonNull public Set<String> getSelectedDatasetIds() { + return mSelectedDatasetIds == null ? Collections.emptySet() + : new ArraySet<>(mSelectedDatasetIds); + } + + /** + * Returns which datasets were NOT selected by the user. + * + * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}. + */ + @NonNull public Set<String> getIgnoredDatasetIds() { + return mIgnoredDatasetIds == null ? Collections.emptySet() : mIgnoredDatasetIds; + } + + /** + * Returns which fields in the selected datasets were changed by the user after the dataset + * was selected. + * + * <p>For example, server provides: + * + * <pre class="prettyprint"> + * FillResponse response = new FillResponse.Builder() + * .addDataset(new Dataset.Builder(presentation1) + * .setId("4815") + * .setValue(usernameId, AutofillValue.forText("MrPlow")) + * .build()) + * .addDataset(new Dataset.Builder(presentation2) + * .setId("162342") + * .setValue(passwordId, AutofillValue.forText("D'OH")) + * .build()) + * .build(); + * </pre> + * + * <p>User select both datasets (for username and password) but after the fields are + * autofilled, user changes them to: + * + * <pre class="prettyprint"> + * username = "ElBarto"; + * password = "AyCaramba"; + * </pre> + * + * <p>Then the result is the following map: + * + * <pre class="prettyprint"> + * usernameId => "4815" + * passwordId => "162342" + * </pre> + * + * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}. + * + * @return map map whose key is the id of the change fields, and value is the id of + * dataset that has that field and was selected by the user. + */ + @NonNull public Map<AutofillId, String> getChangedFields() { + if (mChangedFieldIds == null || mChangedDatasetIds == null) { + return Collections.emptyMap(); + } + + final int size = mChangedFieldIds.size(); + final ArrayMap<AutofillId, String> changedFields = new ArrayMap<>(size); + for (int i = 0; i < size; i++) { + changedFields.put(mChangedFieldIds.get(i), mChangedDatasetIds.get(i)); + } + return changedFields; + } + + /** + * Gets the <a href="#FieldsClassification">fields classification</a> results. + * + * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the + * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...) + * fields classification}. + * + * TODO(b/67867469): + * - improve javadoc + * - unhide / remove testApi + * + * @hide + */ + @TestApi + @NonNull public Map<AutofillId, FieldClassification> getFieldsClassification() { + if (mDetectedFieldIds == null) { + return Collections.emptyMap(); + } + final int size = mDetectedFieldIds.length; + final ArrayMap<AutofillId, FieldClassification> map = new ArrayMap<>(size); + for (int i = 0; i < size; i++) { + final AutofillId id = mDetectedFieldIds[i]; + final Match match = mDetectedMatches[i]; + if (sVerbose) { + Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", match=" + match); + } + map.put(id, new FieldClassification(match)); + } + return map; + } + + /** + * Returns which fields were available on datasets provided by the service but manually + * entered by the user. + * + * <p>For example, server provides: + * + * <pre class="prettyprint"> + * FillResponse response = new FillResponse.Builder() + * .addDataset(new Dataset.Builder(presentation1) + * .setId("4815") + * .setValue(usernameId, AutofillValue.forText("MrPlow")) + * .setValue(passwordId, AutofillValue.forText("AyCaramba")) + * .build()) + * .addDataset(new Dataset.Builder(presentation2) + * .setId("162342") + * .setValue(usernameId, AutofillValue.forText("ElBarto")) + * .setValue(passwordId, AutofillValue.forText("D'OH")) + * .build()) + * .addDataset(new Dataset.Builder(presentation3) + * .setId("108") + * .setValue(usernameId, AutofillValue.forText("MrPlow")) + * .setValue(passwordId, AutofillValue.forText("D'OH")) + * .build()) + * .build(); + * </pre> + * + * <p>User doesn't select a dataset but manually enters: + * + * <pre class="prettyprint"> + * username = "MrPlow"; + * password = "D'OH"; + * </pre> + * + * <p>Then the result is the following map: + * + * <pre class="prettyprint"> + * usernameId => { "4815", "108"} + * passwordId => { "162342", "108" } + * </pre> + * + * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}. + * + * @return map map whose key is the id of the manually-entered field, and value is the + * ids of the datasets that have that value but were not selected by the user. + */ + @Nullable public Map<AutofillId, Set<String>> getManuallyEnteredField() { + if (mManuallyFilledFieldIds == null || mManuallyFilledDatasetIds == null) { + return Collections.emptyMap(); + } + + final int size = mManuallyFilledFieldIds.size(); + final Map<AutofillId, Set<String>> manuallyFilledFields = new ArrayMap<>(size); + for (int i = 0; i < size; i++) { + final AutofillId fieldId = mManuallyFilledFieldIds.get(i); + final ArrayList<String> datasetIds = mManuallyFilledDatasetIds.get(i); + manuallyFilledFields.put(fieldId, new ArraySet<>(datasetIds)); + } + return manuallyFilledFields; + } + + /** * Creates a new event. * * @param eventType The type of the event * @param datasetId The dataset the event was on, or {@code null} if the event was on the * whole response. + * @param clientState The client state associated with the event. + * @param selectedDatasetIds The ids of datasets selected by the user. + * @param ignoredDatasetIds The ids of datasets NOT select by the user. + * @param changedFieldIds The ids of fields changed by the user. + * @param changedDatasetIds The ids of the datasets that havd values matching the + * respective entry on {@code changedFieldIds}. + * @param manuallyFilledFieldIds The ids of fields that were manually entered by the user + * 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 + * {@code manuallyFilledDatasetIds} doesn't match. * * @hide */ - public Event(int eventType, String datasetId) { - mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_SAVE_SHOWN, + // TODO(b/67867469): document field classification 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 AutofillId[] detectedFieldIds, @Nullable Match[] detectedMaches) { + mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED, "eventType"); mDatasetId = datasetId; + mClientState = clientState; + mSelectedDatasetIds = selectedDatasetIds; + mIgnoredDatasetIds = ignoredDatasetIds; + if (changedFieldIds != null) { + Preconditions.checkArgument(!ArrayUtils.isEmpty(changedFieldIds) + && changedDatasetIds != null + && changedFieldIds.size() == changedDatasetIds.size(), + "changed ids must have same length and not be empty"); + } + mChangedFieldIds = changedFieldIds; + mChangedDatasetIds = changedDatasetIds; + if (manuallyFilledFieldIds != null) { + Preconditions.checkArgument(!ArrayUtils.isEmpty(manuallyFilledFieldIds) + && manuallyFilledDatasetIds != null + && manuallyFilledFieldIds.size() == manuallyFilledDatasetIds.size(), + "manually filled ids must have same length and not be empty"); + } + mManuallyFilledFieldIds = manuallyFilledFieldIds; + mManuallyFilledDatasetIds = manuallyFilledDatasetIds; + + mDetectedFieldIds = detectedFieldIds; + mDetectedMatches = detectedMaches; + } + + @Override + public String toString() { + return "FillEvent [datasetId=" + mDatasetId + + ", type=" + mEventType + + ", selectedDatasets=" + mSelectedDatasetIds + + ", ignoredDatasetIds=" + mIgnoredDatasetIds + + ", changedFieldIds=" + mChangedFieldIds + + ", changedDatasetsIds=" + mChangedDatasetIds + + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds + + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds + + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds) + + ", detectedMaches =" + Arrays.toString(mDetectedMatches) + + "]"; } } @@ -223,13 +524,45 @@ public final class FillEventHistory implements Parcelable { new Parcelable.Creator<FillEventHistory>() { @Override public FillEventHistory createFromParcel(Parcel parcel) { - FillEventHistory selection = new FillEventHistory(0, 0, parcel.readBundle()); + FillEventHistory selection = new FillEventHistory(0, parcel.readBundle()); - int numEvents = parcel.readInt(); + final int numEvents = parcel.readInt(); for (int i = 0; i < numEvents; i++) { - selection.addEvent(new Event(parcel.readInt(), parcel.readString())); - } + final int eventType = parcel.readInt(); + final String datasetId = parcel.readString(); + final Bundle clientState = parcel.readBundle(); + final ArrayList<String> selectedDatasetIds = parcel.createStringArrayList(); + @SuppressWarnings("unchecked") + final ArraySet<String> ignoredDatasets = + (ArraySet<String>) parcel.readArraySet(null); + final ArrayList<AutofillId> changedFieldIds = + parcel.createTypedArrayList(AutofillId.CREATOR); + final ArrayList<String> changedDatasetIds = parcel.createStringArrayList(); + final ArrayList<AutofillId> manuallyFilledFieldIds = + parcel.createTypedArrayList(AutofillId.CREATOR); + final ArrayList<ArrayList<String>> manuallyFilledDatasetIds; + if (manuallyFilledFieldIds != null) { + final int size = manuallyFilledFieldIds.size(); + manuallyFilledDatasetIds = new ArrayList<>(size); + for (int j = 0; j < size; j++) { + manuallyFilledDatasetIds.add(parcel.createStringArrayList()); + } + } else { + manuallyFilledDatasetIds = null; + } + final AutofillId[] detectedFieldIds = parcel.readParcelableArray(null, + AutofillId.class); + final Match[] detectedMatches = (detectedFieldIds != null) + ? parcel.readParcelableArray(null, Match.class) + : null; + + selection.addEvent(new Event(eventType, datasetId, clientState, + selectedDatasetIds, ignoredDatasets, + changedFieldIds, changedDatasetIds, + manuallyFilledFieldIds, manuallyFilledDatasetIds, + detectedFieldIds, detectedMatches)); + } return selection; } diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java index 8a8c670d9514..ddc92f6a656f 100644 --- a/core/java/android/service/autofill/FillRequest.java +++ b/core/java/android/service/autofill/FillRequest.java @@ -127,9 +127,15 @@ public final class FillRequest implements Parcelable { } /** - * Gets the extra client state returned from the last {@link - * AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)} - * fill request}. + * Gets the latest client state bundle set by the service in a + * {@link FillResponse.Builder#setClientState(Bundle) fill response}. + * + * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state + * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On + * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of + * an authenticated request through the + * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are + * also considered (and take precedence when set). * * @return The client state. */ diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index 6d8a95991f05..04db69845c86 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -19,8 +19,10 @@ package android.service.autofill; import static android.service.autofill.FillRequest.INVALID_REQUEST_ID; 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; @@ -30,6 +32,10 @@ import android.os.Parcelable; import android.view.autofill.AutofillId; import android.widget.RemoteViews; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -42,23 +48,55 @@ import java.util.List; */ public final class FillResponse implements Parcelable { + /** + * Flag used to generate {@link FillEventHistory.Event events} of type + * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}—if this flag is not passed to + * {@link Builder#setFlags(int)}, these events are not generated. + */ + public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1; + + /** + * Flag used to change the behavior of {@link FillResponse.Builder#disableAutofill(long)}— + * when this flag is passed to {@link Builder#setFlags(int)}, autofill is disabled only for the + * activiy that generated the {@link FillRequest}, not the whole app. + */ + public static final int FLAG_DISABLE_ACTIVITY_ONLY = 0x2; + + /** @hide */ + @IntDef(flag = true, value = { + FLAG_TRACK_CONTEXT_COMMITED, + FLAG_DISABLE_ACTIVITY_ONLY + }) + @Retention(RetentionPolicy.SOURCE) + @interface FillResponseFlags {} + private final @Nullable ParceledListSlice<Dataset> mDatasets; private final @Nullable SaveInfo mSaveInfo; private final @Nullable Bundle mClientState; private final @Nullable RemoteViews mPresentation; + private final @Nullable RemoteViews mHeader; + private final @Nullable RemoteViews mFooter; private final @Nullable IntentSender mAuthentication; private final @Nullable AutofillId[] mAuthenticationIds; private final @Nullable AutofillId[] mIgnoredIds; + private final long mDisableDuration; + private final @Nullable AutofillId[] mFieldClassificationIds; + private final int mFlags; private int mRequestId; private FillResponse(@NonNull Builder builder) { mDatasets = (builder.mDatasets != null) ? new ParceledListSlice<>(builder.mDatasets) : null; mSaveInfo = builder.mSaveInfo; - mClientState = builder.mCLientState; + mClientState = builder.mClientState; mPresentation = builder.mPresentation; + mHeader = builder.mHeader; + mFooter = builder.mFooter; mAuthentication = builder.mAuthentication; mAuthenticationIds = builder.mAuthenticationIds; mIgnoredIds = builder.mIgnoredIds; + mDisableDuration = builder.mDisableDuration; + mFieldClassificationIds = builder.mFieldClassificationIds; + mFlags = builder.mFlags; mRequestId = INVALID_REQUEST_ID; } @@ -83,6 +121,16 @@ public final class FillResponse implements Parcelable { } /** @hide */ + public @Nullable RemoteViews getHeader() { + return mHeader; + } + + /** @hide */ + public @Nullable RemoteViews getFooter() { + return mFooter; + } + + /** @hide */ public @Nullable IntentSender getAuthentication() { return mAuthentication; } @@ -97,6 +145,21 @@ public final class FillResponse implements Parcelable { return mIgnoredIds; } + /** @hide */ + public long getDisableDuration() { + return mDisableDuration; + } + + /** @hide */ + public @Nullable AutofillId[] getFieldClassificationIds() { + return mFieldClassificationIds; + } + + /** @hide */ + public int getFlags() { + return mFlags; + } + /** * Associates a {@link FillResponse} to a request. * @@ -122,16 +185,25 @@ public final class FillResponse implements Parcelable { public static final class Builder { private ArrayList<Dataset> mDatasets; private SaveInfo mSaveInfo; - private Bundle mCLientState; + private Bundle mClientState; private RemoteViews mPresentation; + private RemoteViews mHeader; + private RemoteViews mFooter; private IntentSender mAuthentication; private AutofillId[] mAuthenticationIds; private AutofillId[] mIgnoredIds; + private long mDisableDuration; + private AutofillId[] mFieldClassificationIds; + private int mFlags; private boolean mDestroyed; /** - * Requires a fill response authentication before autofilling the screen with - * any data set in this response. + * Triggers a custom UI before before autofilling the screen with any data set in this + * response. + * + * <p><b>Note:</b> Although the name of this method suggests that it should be used just for + * authentication flow, it can be used for other advanced flows; see {@link AutofillService} + * for examples. * * <p>This is typically useful when a user interaction is required to unlock their * data vault if you encrypt the data set labels and data set data. It is recommended @@ -163,23 +235,33 @@ public final class FillResponse implements Parcelable { * which is used to visualize visualize the response for triggering the authentication * flow. * - * <p></><strong>Note:</strong> Do not make the provided pending intent + * <p><b>Note:</b> Do not make the provided pending intent * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the * platform needs to fill in the authentication arguments. * * @param authentication Intent to an activity with your authentication flow. * @param presentation The presentation to visualize the response. - * @param ids id of Views that when focused will display the authentication UI affordance. + * @param ids id of Views that when focused will display the authentication UI. * * @return This builder. + * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if - * neither {@code authentication} nor {@code presentation} is non-{@code null}. + * both {@code authentication} and {@code presentation} are {@code null}, or if + * both {@code authentication} and {@code presentation} are non-{@code null} + * + * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a + * {@link #setFooter(RemoteViews) footer} are already set for this builder. * * @see android.app.PendingIntent#getIntentSender() */ public @NonNull Builder setAuthentication(@NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation) { throwIfDestroyed(); + throwIfDisableAutofillCalled(); + if (mHeader != null || mFooter != null) { + throw new IllegalStateException("Already called #setHeader() or #setFooter()"); + } + if (ids == null || ids.length == 0) { throw new IllegalArgumentException("ids cannot be null or empry"); } @@ -202,6 +284,7 @@ public final class FillResponse implements Parcelable { * text field representing the result of a Captcha challenge. */ public Builder setIgnoredIds(AutofillId...ids) { + throwIfDestroyed(); mIgnoredIds = ids; return this; } @@ -222,6 +305,7 @@ public final class FillResponse implements Parcelable { */ public @NonNull Builder addDataset(@Nullable Dataset dataset) { throwIfDestroyed(); + throwIfDisableAutofillCalled(); if (dataset == null) { return this; } @@ -241,47 +325,207 @@ public final class FillResponse implements Parcelable { */ public @NonNull Builder setSaveInfo(@NonNull SaveInfo saveInfo) { throwIfDestroyed(); + throwIfDisableAutofillCalled(); mSaveInfo = saveInfo; return this; } /** - * Sets a {@link Bundle state} that will be passed to subsequent APIs that - * manipulate this response. For example, they are passed to subsequent - * calls to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, - * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}. - * You can use this to store intermediate state that is persistent across multiple - * fill requests and the subsequent save request. + * Sets a bundle with state that is passed to subsequent APIs that manipulate this response. + * + * <p>You can use this bundle to store intermediate state that is passed to subsequent calls + * to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, + * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}, and + * you can also retrieve it by calling {@link FillEventHistory.Event#getClientState()}. * * <p>If this method is called on multiple {@link FillResponse} objects for the same * screen, just the latest bundle is passed back to the service. * - * <p>Once a {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback) - * save request} is made the client state is cleared. - * * @param clientState The custom client state. * @return This builder. */ public Builder setClientState(@Nullable Bundle clientState) { throwIfDestroyed(); - mCLientState = clientState; + throwIfDisableAutofillCalled(); + mClientState = clientState; + return this; + } + + /** + * Sets which fields are used for <a href="#FieldsClassification">fields classification</a> + * + * @throws IllegalArgumentException is length of {@code ids} args is more than + * {@link UserData#getMaxFieldClassificationIdsSize()}. + * @throws IllegalStateException if {@link #build()} or {@link #disableAutofill(long)} was + * already called. + * @throws NullPointerException if {@code ids} or any element on it is {@code null}. + * + * TODO(b/67867469): + * - improve javadoc: explain relationship with UserData and how to check results + * - unhide / remove testApi + * - implement multiple ids + * + * @hide + */ + @TestApi + public Builder setFieldClassificationIds(@NonNull AutofillId... ids) { + throwIfDestroyed(); + throwIfDisableAutofillCalled(); + Preconditions.checkArrayElementsNotNull(ids, "ids"); + Preconditions.checkArgumentInRange(ids.length, 1, + UserData.getMaxFieldClassificationIdsSize(), "ids length"); + mFieldClassificationIds = ids; + return this; + } + + /** + * Sets flags changing the response behavior. + * + * @param flags a combination of {@link #FLAG_TRACK_CONTEXT_COMMITED} and + * {@link #FLAG_DISABLE_ACTIVITY_ONLY}, or {@code 0}. + * + * @return This builder. + */ + public Builder setFlags(@FillResponseFlags int flags) { + throwIfDestroyed(); + mFlags = Preconditions.checkFlagsArgument(flags, + FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY); + return this; + } + + /** + * Disables autofill for the app or activity. + * + * <p>This method is useful to optimize performance in cases where the service knows it + * can not autofill an app—for example, when the service has a list of "blacklisted" + * apps such as office suites. + * + * <p>By default, it disables autofill for all activities in the app, unless the response is + * {@link #setFlags(int) flagged} with {@link #FLAG_DISABLE_ACTIVITY_ONLY}. + * + * <p>Autofill for the app or activity is automatically re-enabled after any of the + * following conditions: + * + * <ol> + * <li>{@code duration} milliseconds have passed. + * <li>The autofill service for the user has changed. + * <li>The device has rebooted. + * </ol> + * + * <p><b>Note:</b> Activities that are running when autofill is re-enabled remain + * disabled for autofill until they finish and restart. + * + * @param duration duration to disable autofill, in milliseconds. + * + * @return this builder + * + * @throws IllegalArgumentException if {@code duration} is not a positive number. + * @throws IllegalStateException if either {@link #addDataset(Dataset)}, + * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, + * {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or + * {link #setFieldClassificationIds(AutofillId...)} was already called. + */ + // TODO(b/67867469): add @ to {link setFieldClassificationIds} once it's public + public Builder disableAutofill(long duration) { + throwIfDestroyed(); + if (duration <= 0) { + throw new IllegalArgumentException("duration must be greater than 0"); + } + if (mAuthentication != null || mDatasets != null || mSaveInfo != null + || mFieldClassificationIds != null || mClientState != null) { + throw new IllegalStateException("disableAutofill() must be the only method called"); + } + + mDisableDuration = duration; + return this; + } + + /** + * Sets a header to be shown as the first element in the list of datasets. + * + * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset}, + * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this + * method should only be used on {@link FillResponse FillResponses} that do not require + * authentication (as the header could have been set directly in the main presentation in + * these cases). + * + * @param header a presentation to represent the header. This presentation is not clickable + * —calling + * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would + * have no effect. + * + * @return this builder + * + * @throws IllegalStateException if an + * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) authentication} was + * already set for this builder. + */ + // TODO(b/69796626): make it sticky / update javadoc + public Builder setHeader(@NonNull RemoteViews header) { + throwIfDestroyed(); + throwIfAuthenticationCalled(); + mHeader = Preconditions.checkNotNull(header); + return this; + } + + /** + * Sets a footer to be shown as the last element in the list of datasets. + * + * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset}, + * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this + * method should only be used on {@link FillResponse FillResponses} that do not require + * authentication (as the footer could have been set directly in the main presentation in + * these cases). + * + * @param footer a presentation to represent the footer. This presentation is not clickable + * —calling + * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would + * have no effect. + * + * @return this builder + * + * @throws IllegalStateException if the FillResponse + * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) + * requires authentication}. + */ + // TODO(b/69796626): make it sticky / update javadoc + public Builder setFooter(@NonNull RemoteViews footer) { + throwIfDestroyed(); + throwIfAuthenticationCalled(); + mFooter = Preconditions.checkNotNull(footer); return this; } /** * Builds a new {@link FillResponse} instance. * - * <p>You must provide at least one dataset or some savable ids or an authentication with a - * presentation view. + * @throws IllegalStateException if any of the following conditions occur: + * <ol> + * <li>{@link #build()} was already called. + * <li>No call was made to {@link #addDataset(Dataset)}, + * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, + * {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)}, + * {@link #setClientState(Bundle)}, + * or {link #setFieldClassificationIds(AutofillId...)}. + * <li>{@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} is called + * without any previous calls to {@link #addDataset(Dataset)}. + * </ol> * * @return A built response. */ + // TODO(b/67867469): add @ to {link setFieldClassificationIds} once it's public public FillResponse build() { throwIfDestroyed(); - - if (mAuthentication == null && mDatasets == null && mSaveInfo == null) { - throw new IllegalArgumentException("need to provide at least one DataSet or a " - + "SaveInfo or an authentication with a presentation"); + if (mAuthentication == null && mDatasets == null && mSaveInfo == null + && mDisableDuration == 0 && mFieldClassificationIds == null + && mClientState == null) { + throw new IllegalStateException("need to provide: at least one DataSet, or a " + + "SaveInfo, or an authentication with a presentation, " + + "or a FieldsDetection, or a client state, or disable autofill"); + } + if (mDatasets == null && (mHeader != null || mFooter != null)) { + throw new IllegalStateException( + "must add at least 1 dataset when using header or footer"); } mDestroyed = true; return new FillResponse(this); @@ -292,6 +536,18 @@ public final class FillResponse implements Parcelable { throw new IllegalStateException("Already called #build()"); } } + + private void throwIfDisableAutofillCalled() { + if (mDisableDuration > 0) { + throw new IllegalStateException("Already called #disableAutofill()"); + } + } + + private void throwIfAuthenticationCalled() { + if (mAuthentication != null) { + throw new IllegalStateException("Already called #setAuthentication()"); + } + } } ///////////////////////////////////// @@ -308,9 +564,15 @@ public final class FillResponse implements Parcelable { .append(", saveInfo=").append(mSaveInfo) .append(", clientState=").append(mClientState != null) .append(", hasPresentation=").append(mPresentation != null) + .append(", hasHeader=").append(mHeader != null) + .append(", hasFooter=").append(mFooter != null) .append(", hasAuthentication=").append(mAuthentication != null) .append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds)) .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds)) + .append(", disableDuration=").append(mDisableDuration) + .append(", flags=").append(mFlags) + .append(", fieldClassificationIds=") + .append(Arrays.toString(mFieldClassificationIds)) .append("]") .toString(); } @@ -332,7 +594,12 @@ public final class FillResponse implements Parcelable { parcel.writeParcelableArray(mAuthenticationIds, flags); parcel.writeParcelable(mAuthentication, flags); parcel.writeParcelable(mPresentation, flags); + parcel.writeParcelable(mHeader, flags); + parcel.writeParcelable(mFooter, flags); parcel.writeParcelableArray(mIgnoredIds, flags); + parcel.writeLong(mDisableDuration); + parcel.writeParcelableArray(mFieldClassificationIds, flags); + parcel.writeInt(mFlags); parcel.writeInt(mRequestId); } @@ -361,10 +628,28 @@ public final class FillResponse implements Parcelable { if (authenticationIds != null) { builder.setAuthentication(authenticationIds, authentication, presentation); } + final RemoteViews header = parcel.readParcelable(null); + if (header != null) { + builder.setHeader(header); + } + final RemoteViews footer = parcel.readParcelable(null); + if (footer != null) { + builder.setFooter(footer); + } builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class)); - final FillResponse response = builder.build(); + final long disableDuration = parcel.readLong(); + if (disableDuration > 0) { + builder.disableAutofill(disableDuration); + } + final AutofillId[] fieldClassifactionIds = + parcel.readParcelableArray(null, AutofillId.class); + if (fieldClassifactionIds != null) { + builder.setFieldClassificationIds(fieldClassifactionIds); + } + builder.setFlags(parcel.readInt()); + final FillResponse response = builder.build(); response.setRequestId(parcel.readInt()); return response; diff --git a/core/java/android/service/autofill/ISaveCallback.aidl b/core/java/android/service/autofill/ISaveCallback.aidl index e260c7375cc5..a9364fe5ccba 100644 --- a/core/java/android/service/autofill/ISaveCallback.aidl +++ b/core/java/android/service/autofill/ISaveCallback.aidl @@ -16,12 +16,14 @@ package android.service.autofill; +import android.content.IntentSender; + /** * Interface to receive the result of a save request. * * @hide */ interface ISaveCallback { - void onSuccess(); + void onSuccess(in IntentSender intentSender); void onFailure(CharSequence message); } diff --git a/core/java/android/service/autofill/ImageTransformation.java b/core/java/android/service/autofill/ImageTransformation.java index 2151f74fbe5b..4afda249afea 100644 --- a/core/java/android/service/autofill/ImageTransformation.java +++ b/core/java/android/service/autofill/ImageTransformation.java @@ -20,11 +20,12 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.DrawableRes; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import android.view.autofill.AutofillId; import android.widget.ImageView; import android.widget.RemoteViews; @@ -43,9 +44,9 @@ import java.util.regex.Pattern; * * <pre class="prettyprint"> * new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"), - * R.drawable.ic_credit_card_logo1) - * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2) - * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3) + * R.drawable.ic_credit_card_logo1, "Brand 1") + * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2, "Brand 2") + * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3, "Brand 3") * .build(); * </pre> * @@ -59,7 +60,7 @@ public final class ImageTransformation extends InternalTransformation implements private static final String TAG = "ImageTransformation"; private final AutofillId mId; - private final ArrayList<Pair<Pattern, Integer>> mOptions; + private final ArrayList<Option> mOptions; private ImageTransformation(Builder builder) { mId = builder.mId; @@ -82,17 +83,21 @@ public final class ImageTransformation extends InternalTransformation implements } for (int i = 0; i < size; i++) { - final Pair<Pattern, Integer> option = mOptions.get(i); + final Option option = mOptions.get(i); try { - if (option.first.matcher(value).matches()) { + if (option.pattern.matcher(value).matches()) { Log.d(TAG, "Found match at " + i + ": " + option); - parentTemplate.setImageViewResource(childViewId, option.second); + parentTemplate.setImageViewResource(childViewId, option.resId); + if (option.contentDescription != null) { + parentTemplate.setContentDescription(childViewId, + option.contentDescription); + } return; } } catch (Exception e) { // Do not log full exception to avoid PII leaking - Log.w(TAG, "Error matching regex #" + i + "(" + option.first.pattern() + ") on id " - + option.second + ": " + e.getClass()); + Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id " + + option.resId + ": " + e.getClass()); throw e; } @@ -105,25 +110,44 @@ public final class ImageTransformation extends InternalTransformation implements */ public static class Builder { private final AutofillId mId; - private final ArrayList<Pair<Pattern, Integer>> mOptions = new ArrayList<>(); + private final ArrayList<Option> mOptions = new ArrayList<>(); private boolean mDestroyed; /** - * Create a new builder for a autofill id and add a first option. + * Creates a new builder for a autofill id and add a first option. * * @param id id of the screen field that will be used to evaluate whether the image should * be used. * @param regex regular expression defining what should be matched to use this image. * @param resId resource id of the image (in the autofill service's package). The * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * + * @deprecated use + * {@link #ImageTransformation.Builder(AutofillId, Pattern, int, CharSequence)} instead. */ + @Deprecated public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) { mId = Preconditions.checkNotNull(id); - addOption(regex, resId); } /** + * Creates a new builder for a autofill id and add a first option. + * + * @param id id of the screen field that will be used to evaluate whether the image should + * be used. + * @param regex regular expression defining what should be matched to use this image. + * @param resId resource id of the image (in the autofill service's package). The + * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * @param contentDescription content description to be applied in the child view. + */ + public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId, + @NonNull CharSequence contentDescription) { + mId = Preconditions.checkNotNull(id); + addOption(regex, resId, contentDescription); + } + + /** * Adds an option to replace the child view with a different image when the regex matches. * * @param regex regular expression defining what should be matched to use this image. @@ -131,17 +155,43 @@ public final class ImageTransformation extends InternalTransformation implements * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. * * @return this build + * + * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead. */ + @Deprecated public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) { + addOptionInternal(regex, resId, null); + return this; + } + + /** + * Adds an option to replace the child view with a different image and content description + * when the regex matches. + * + * @param regex regular expression defining what should be matched to use this image. + * @param resId resource id of the image (in the autofill service's package). The + * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * @param contentDescription content description to be applied in the child view. + * + * @return this build + */ + public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId, + @NonNull CharSequence contentDescription) { + addOptionInternal(regex, resId, Preconditions.checkNotNull(contentDescription)); + return this; + } + + private void addOptionInternal(@NonNull Pattern regex, @DrawableRes int resId, + @Nullable CharSequence contentDescription) { throwIfDestroyed(); Preconditions.checkNotNull(regex); Preconditions.checkArgument(resId != 0); - mOptions.add(new Pair<>(regex, resId)); - return this; + mOptions.add(new Option(regex, resId, contentDescription)); } + /** * Creates a new {@link ImageTransformation} instance. */ @@ -178,15 +228,18 @@ public final class ImageTransformation extends InternalTransformation implements parcel.writeParcelable(mId, flags); final int size = mOptions.size(); - final Pattern[] regexs = new Pattern[size]; + final Pattern[] patterns = new Pattern[size]; final int[] resIds = new int[size]; + final CharSequence[] contentDescriptions = new String[size]; for (int i = 0; i < size; i++) { - Pair<Pattern, Integer> regex = mOptions.get(i); - regexs[i] = regex.first; - resIds[i] = regex.second; + final Option option = mOptions.get(i); + patterns[i] = option.pattern; + resIds[i] = option.resId; + contentDescriptions[i] = option.contentDescription; } - parcel.writeSerializable(regexs); + parcel.writeSerializable(patterns); parcel.writeIntArray(resIds); + parcel.writeCharSequenceArray(contentDescriptions); } public static final Parcelable.Creator<ImageTransformation> CREATOR = @@ -197,15 +250,22 @@ public final class ImageTransformation extends InternalTransformation implements final Pattern[] regexs = (Pattern[]) parcel.readSerializable(); final int[] resIds = parcel.createIntArray(); + final CharSequence[] contentDescriptions = parcel.readCharSequenceArray(); // 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. - final ImageTransformation.Builder builder = new ImageTransformation.Builder(id, - regexs[0], resIds[0]); + final CharSequence contentDescription = contentDescriptions[0]; + final ImageTransformation.Builder builder = (contentDescription != null) + ? new ImageTransformation.Builder(id, regexs[0], resIds[0], contentDescription) + : new ImageTransformation.Builder(id, regexs[0], resIds[0]); final int size = regexs.length; for (int i = 1; i < size; i++) { - builder.addOption(regexs[i], resIds[i]); + if (contentDescriptions[i] != null) { + builder.addOption(regexs[i], resIds[i], contentDescriptions[i]); + } else { + builder.addOption(regexs[i], resIds[i]); + } } return builder.build(); @@ -216,4 +276,16 @@ public final class ImageTransformation extends InternalTransformation implements return new ImageTransformation[size]; } }; + + private static final class Option { + public final Pattern pattern; + public final int resId; + public final CharSequence contentDescription; + + Option(Pattern pattern, int resId, CharSequence contentDescription) { + this.pattern = pattern; + this.resId = resId; + this.contentDescription = TextUtils.trimNoCopySpans(contentDescription); + } + } } diff --git a/core/java/android/service/autofill/InternalSanitizer.java b/core/java/android/service/autofill/InternalSanitizer.java new file mode 100644 index 000000000000..d77e41e3f022 --- /dev/null +++ b/core/java/android/service/autofill/InternalSanitizer.java @@ -0,0 +1,43 @@ +/* + * 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.service.autofill; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; +import android.os.Parcelable; +import android.view.autofill.AutofillValue; + +/** + * Superclass of all sanitizers the system understands. As this is not public all public subclasses + * have to implement {@link Sanitizer} again. + * + * @hide + */ +@TestApi +public abstract class InternalSanitizer implements Sanitizer, Parcelable { + + /** + * Sanitizes an {@link AutofillValue}. + * + * @return sanitized value or {@code null} if value could not be sanitized (for example: didn't + * match regex, it's an invalid type, regex failed, etc). + * + * @hide + */ + @Nullable + public abstract AutofillValue sanitize(@NonNull AutofillValue value); +} diff --git a/core/java/android/service/autofill/InternalScorer.java b/core/java/android/service/autofill/InternalScorer.java new file mode 100644 index 000000000000..0da5afc2331d --- /dev/null +++ b/core/java/android/service/autofill/InternalScorer.java @@ -0,0 +1,40 @@ +/* + * 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.service.autofill; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcelable; +import android.view.autofill.AutofillValue; + +/** + * Superclass of all scorer the system understands. As this is not public all + * subclasses have to implement {@link Scorer} again. + * + * @hide + */ +@TestApi +public abstract class InternalScorer implements Scorer, Parcelable { + + /** + * Returns the classification score between an actual {@link AutofillValue} filled + * by the user and the expected value predicted by an autofill service. + * + * <p>A full-match is {@code 1.0} (representing 100%), a full mismatch is {@code 0.0} and + * partial mathces are something in between, typically using edit-distance algorithms. + */ + public abstract float getScore(@NonNull AutofillValue actualValue, @NonNull String userData); +} diff --git a/core/java/android/service/autofill/InternalTransformation.java b/core/java/android/service/autofill/InternalTransformation.java index 974397b3f048..c9864a0e5711 100644 --- a/core/java/android/service/autofill/InternalTransformation.java +++ b/core/java/android/service/autofill/InternalTransformation.java @@ -15,17 +15,27 @@ */ package android.service.autofill; +import static android.view.autofill.Helper.sDebug; + import android.annotation.NonNull; +import android.annotation.TestApi; import android.os.Parcelable; +import android.util.Log; +import android.util.Pair; import android.widget.RemoteViews; +import java.util.ArrayList; + /** * Superclass of all transformation the system understands. As this is not public all * subclasses have to implement {@link Transformation} again. * * @hide */ -abstract class InternalTransformation implements Transformation, Parcelable { +@TestApi +public abstract class InternalTransformation implements Transformation, Parcelable { + + private static final String TAG = "InternalTransformation"; /** * Applies this transformation to a child view of a {@link android.widget.RemoteViews @@ -39,4 +49,37 @@ abstract class InternalTransformation implements Transformation, Parcelable { */ abstract void apply(@NonNull ValueFinder finder, @NonNull RemoteViews template, int childViewId) throws Exception; + + /** + * Applies multiple transformations to the children views of a + * {@link android.widget.RemoteViews presentation template}. + * + * @param finder object used to find the value of a field in the screen. + * @param template the {@link RemoteViews presentation template}. + * @param transformations map of resource id of the child view inside the template to + * transformation. + * + * @hide + */ + public static boolean batchApply(@NonNull ValueFinder finder, @NonNull RemoteViews template, + @NonNull ArrayList<Pair<Integer, InternalTransformation>> transformations) { + final int size = transformations.size(); + if (sDebug) Log.d(TAG, "getPresentation(): applying " + size + " transformations"); + for (int i = 0; i < size; i++) { + final Pair<Integer, InternalTransformation> pair = transformations.get(i); + final int id = pair.first; + final InternalTransformation transformation = pair.second; + if (sDebug) Log.d(TAG, "#" + i + ": " + transformation); + + try { + transformation.apply(finder, template, id); + } catch (Exception e) { + // Do not log full exception to avoid PII leaking + Log.e(TAG, "Could not apply transformation " + transformation + ": " + + e.getClass()); + return false; + } + } + return true; + } } diff --git a/core/java/android/service/autofill/InternalValidator.java b/core/java/android/service/autofill/InternalValidator.java index e11cf6ad72e1..e08bb6c1a2e0 100644 --- a/core/java/android/service/autofill/InternalValidator.java +++ b/core/java/android/service/autofill/InternalValidator.java @@ -16,6 +16,7 @@ package android.service.autofill; import android.annotation.NonNull; +import android.annotation.TestApi; import android.os.Parcelable; /** @@ -24,6 +25,7 @@ import android.os.Parcelable; * * @hide */ +@TestApi public abstract class InternalValidator implements Validator, Parcelable { /** @@ -34,5 +36,6 @@ public abstract class InternalValidator implements Validator, Parcelable { * * @hide */ + @TestApi public abstract boolean isValid(@NonNull ValueFinder finder); } diff --git a/core/java/android/service/autofill/LuhnChecksumValidator.java b/core/java/android/service/autofill/LuhnChecksumValidator.java index 0b5930dfe5b7..c56ae84b4ae3 100644 --- a/core/java/android/service/autofill/LuhnChecksumValidator.java +++ b/core/java/android/service/autofill/LuhnChecksumValidator.java @@ -27,6 +27,8 @@ import android.view.autofill.AutofillId; import com.android.internal.util.Preconditions; +import java.util.Arrays; + /** * Validator that returns {@code true} if the number created by concatenating all given fields * pass a Luhn algorithm checksum. All non-digits are ignored. @@ -86,17 +88,27 @@ public final class LuhnChecksumValidator extends InternalValidator implements Va public boolean isValid(@NonNull ValueFinder finder) { if (mIds == null || mIds.length == 0) return false; - final StringBuilder number = new StringBuilder(); + final StringBuilder builder = new StringBuilder(); for (AutofillId id : mIds) { final String partialNumber = finder.findByAutofillId(id); if (partialNumber == null) { if (sDebug) Log.d(TAG, "No partial number for id " + id); return false; } - number.append(partialNumber); + builder.append(partialNumber); } - return isLuhnChecksumValid(number.toString()); + final String number = builder.toString(); + boolean valid = isLuhnChecksumValid(number); + if (sDebug) Log.d(TAG, "isValid(" + number.length() + " chars): " + valid); + return valid; + } + + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "LuhnChecksumValidator: [ids=" + Arrays.toString(mIds) + "]"; } ///////////////////////////////////// diff --git a/core/java/android/service/autofill/NegationValidator.java b/core/java/android/service/autofill/NegationValidator.java new file mode 100644 index 000000000000..a963f9f94346 --- /dev/null +++ b/core/java/android/service/autofill/NegationValidator.java @@ -0,0 +1,79 @@ +/* + * 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.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +/** + * Validator used to implement a {@code NOT} logical operation. + * + * @hide + */ +final class NegationValidator extends InternalValidator { + @NonNull private final InternalValidator mValidator; + + NegationValidator(@NonNull InternalValidator validator) { + mValidator = Preconditions.checkNotNull(validator); + } + + @Override + public boolean isValid(@NonNull ValueFinder finder) { + return !mValidator.isValid(finder); + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "NegationValidator: [validator=" + mValidator + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mValidator, flags); + } + + public static final Parcelable.Creator<NegationValidator> CREATOR = + new Parcelable.Creator<NegationValidator>() { + @Override + public NegationValidator createFromParcel(Parcel parcel) { + return new NegationValidator(parcel.readParcelable(null)); + } + + @Override + public NegationValidator[] newArray(int size) { + return new NegationValidator[size]; + } + }; +} diff --git a/core/java/android/service/autofill/OptionalValidators.java b/core/java/android/service/autofill/OptionalValidators.java index f7edd6e4e8e8..7aec59f6267d 100644 --- a/core/java/android/service/autofill/OptionalValidators.java +++ b/core/java/android/service/autofill/OptionalValidators.java @@ -21,6 +21,7 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; import com.android.internal.util.Preconditions; @@ -34,6 +35,8 @@ import com.android.internal.util.Preconditions; */ final class OptionalValidators extends InternalValidator { + private static final String TAG = "OptionalValidators"; + @NonNull private final InternalValidator[] mValidators; OptionalValidators(@NonNull InternalValidator[] validators) { @@ -44,6 +47,7 @@ final class OptionalValidators extends InternalValidator { public boolean isValid(@NonNull ValueFinder finder) { for (InternalValidator validator : mValidators) { final boolean valid = validator.isValid(finder); + if (sDebug) Log.d(TAG, "isValid(" + validator + "): " + valid); if (valid) return true; } diff --git a/core/java/android/service/autofill/RequiredValidators.java b/core/java/android/service/autofill/RequiredValidators.java index ac85c28404f8..9e1db2bca5df 100644 --- a/core/java/android/service/autofill/RequiredValidators.java +++ b/core/java/android/service/autofill/RequiredValidators.java @@ -21,6 +21,7 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; import com.android.internal.util.Preconditions; @@ -34,6 +35,8 @@ import com.android.internal.util.Preconditions; */ final class RequiredValidators extends InternalValidator { + private static final String TAG = "RequiredValidators"; + @NonNull private final InternalValidator[] mValidators; RequiredValidators(@NonNull InternalValidator[] validators) { @@ -44,6 +47,7 @@ final class RequiredValidators extends InternalValidator { public boolean isValid(@NonNull ValueFinder finder) { for (InternalValidator validator : mValidators) { final boolean valid = validator.isValid(finder); + if (sDebug) Log.d(TAG, "isValid(" + validator + "): " + valid); if (!valid) return false; } return true; diff --git a/core/java/android/service/autofill/Sanitizer.java b/core/java/android/service/autofill/Sanitizer.java new file mode 100644 index 000000000000..38757ac7408b --- /dev/null +++ b/core/java/android/service/autofill/Sanitizer.java @@ -0,0 +1,26 @@ +/* + * 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.service.autofill; + +/** + * Helper class used to sanitize user input before using it in a save request. + * + * <p>Typically used to avoid displaying the save UI for values that are autofilled but reformatted + * by the app—for example, if the autofill service sends a credit card number + * value as "004815162342108" and the app automatically changes it to "0048 1516 2342 108". + */ +public interface Sanitizer { +} diff --git a/core/java/android/service/autofill/SaveCallback.java b/core/java/android/service/autofill/SaveCallback.java index 7207f1df3ee5..855981a544fd 100644 --- a/core/java/android/service/autofill/SaveCallback.java +++ b/core/java/android/service/autofill/SaveCallback.java @@ -16,9 +16,14 @@ package android.service.autofill; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Activity; +import android.content.IntentSender; import android.os.RemoteException; +import com.android.internal.util.Preconditions; + /** * Handles save requests from the {@link AutofillService} into the {@link Activity} being * autofilled. @@ -36,18 +41,33 @@ public final class SaveCallback { * Notifies the Android System that an * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully handled * by the service. + */ + public void onSuccess() { + onSuccessInternal(null); + } + + /** + * Notifies the Android System that an + * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully handled + * by the service. * - * <p>If the service could not handle the request right away—for example, because it must - * launch an activity asking the user to authenticate first or because the network is - * down—it should still call {@link #onSuccess()}. + * <p>This method is useful when the service requires extra work—for example, launching an + * activity asking the user to authenticate first —before it can process the request, + * as the intent will be launched from the context of the activity being autofilled and hence + * will be part of that activity's stack. * - * @throws RuntimeException if an error occurred while calling the Android System. + * @param intentSender intent that will be launched from the context of activity being + * autofilled. */ - public void onSuccess() { + public void onSuccess(@NonNull IntentSender intentSender) { + onSuccessInternal(Preconditions.checkNotNull(intentSender)); + } + + private void onSuccessInternal(@Nullable IntentSender intentSender) { assertNotCalled(); mCalled = true; try { - mCallback.onSuccess(); + mCallback.onSuccess(intentSender); } catch (RemoteException e) { e.rethrowAsRuntimeException(); } @@ -63,11 +83,10 @@ public final class SaveCallback { * the {@link SaveRequest} and call {@link #onSuccess()} instead. * * <p><b>Note:</b> The Android System displays an UI with the supplied error message; if - * you prefer to show your own message, call {@link #onSuccess()} instead. + * you prefer to show your own message, call {@link #onSuccess()} or + * {@link #onSuccess(IntentSender)} instead. * * @param message error message to be displayed to the user. - * - * @throws RuntimeException if an error occurred while calling the Android System. */ public void onFailure(CharSequence message) { assertNotCalled(); diff --git a/core/java/android/service/autofill/SaveInfo.java b/core/java/android/service/autofill/SaveInfo.java index 630e4500e695..0b50f074c4c1 100644 --- a/core/java/android/service/autofill/SaveInfo.java +++ b/core/java/android/service/autofill/SaveInfo.java @@ -25,6 +25,8 @@ import android.app.Activity; import android.content.IntentSender; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.DebugUtils; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; @@ -66,7 +68,7 @@ import java.util.Arrays; * .build(); * </pre> * - * <p>The save type flags are used to display the appropriate strings in the save UI affordance. + * <p>The save type flags are used to display the appropriate strings in the autofill save UI. * You can pass multiple values, but try to keep it short if possible. In the above example, just * {@code SaveInfo.SAVE_DATA_TYPE_PASSWORD} would be enough. * @@ -101,13 +103,17 @@ import java.util.Arrays; * .build(); * </pre> * + * <a name="TriggeringSaveRequest"></a> + * <h3>Triggering a save request</h3> + * * <p>The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after * any of the following events: * <ul> * <li>The {@link Activity} finishes. - * <li>The app explicitly called {@link AutofillManager#commit()}. - * <li>All required views became invisible (if the {@link SaveInfo} was created with the + * <li>The app explicitly calls {@link AutofillManager#commit()}. + * <li>All required views become invisible (if the {@link SaveInfo} was created with the * {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag). + * <li>The user clicks a specific view (defined by {@link Builder#setTriggerId(AutofillId)}. * </ul> * * <p>But it is only triggered when all conditions below are met: @@ -121,10 +127,13 @@ import java.util.Arrays; * <li>There is no {@link Dataset} in the last {@link FillResponse} that completely matches the * screen state (i.e., all required and optional fields in the dataset have the same value as * the fields in the screen). - * <li>The user explicitly tapped the UI affordance asking to save data for autofill. + * <li>The user explicitly tapped the autofill save UI asking to save data for autofill. * </ul> * - * <p>The service can also customize some aspects of the save UI affordance: + * <a name="CustomizingSaveUI"></a> + * <h3>Customizing the autofill save UI</h3> + * + * <p>The service can also customize some aspects of the autofill save UI: * <ul> * <li>Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}. * <li>Add a customized subtitle by calling @@ -210,16 +219,25 @@ public final class SaveInfo implements Parcelable { @interface SaveDataType{} /** - * Usually {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} - * is called once the {@link Activity} finishes. If this flag is set it is called once all - * saved views become invisible. + * Usually, a save request is only automatically <a href="#TriggeringSaveRequest">triggered</a> + * once the {@link Activity} finishes. If this flag is set, it is triggered once all saved views + * become invisible. */ public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 0x1; + /** + * By default, a save request is automatically <a href="#TriggeringSaveRequest">triggered</a> + * once the {@link Activity} finishes. If this flag is set, finishing the activity doesn't + * trigger a save request. + * + * <p>This flag is typically used in conjunction with {@link Builder#setTriggerId(AutofillId)}. + */ + public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2; + /** @hide */ @IntDef( flag = true, - value = {FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE}) + value = {FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, FLAG_DONT_SAVE_ON_FINISH}) @Retention(RetentionPolicy.SOURCE) @interface SaveInfoFlags{} @@ -232,6 +250,9 @@ public final class SaveInfo implements Parcelable { private final int mFlags; private final CustomDescription mCustomDescription; private final InternalValidator mValidator; + private final InternalSanitizer[] mSanitizerKeys; + private final AutofillId[][] mSanitizerValues; + private final AutofillId mTriggerId; private SaveInfo(Builder builder) { mType = builder.mType; @@ -243,6 +264,19 @@ public final class SaveInfo implements Parcelable { mFlags = builder.mFlags; mCustomDescription = builder.mCustomDescription; mValidator = builder.mValidator; + if (builder.mSanitizers == null) { + mSanitizerKeys = null; + mSanitizerValues = null; + } else { + final int size = builder.mSanitizers.size(); + mSanitizerKeys = new InternalSanitizer[size]; + mSanitizerValues = new AutofillId[size][]; + for (int i = 0; i < size; i++) { + mSanitizerKeys[i] = builder.mSanitizers.keyAt(i); + mSanitizerValues[i] = builder.mSanitizers.valueAt(i); + } + } + mTriggerId = builder.mTriggerId; } /** @hide */ @@ -292,6 +326,24 @@ public final class SaveInfo implements Parcelable { return mValidator; } + /** @hide */ + @Nullable + public InternalSanitizer[] getSanitizerKeys() { + return mSanitizerKeys; + } + + /** @hide */ + @Nullable + public AutofillId[][] getSanitizerValues() { + return mSanitizerValues; + } + + /** @hide */ + @Nullable + public AutofillId getTriggerId() { + return mTriggerId; + } + /** * A builder for {@link SaveInfo} objects. */ @@ -307,6 +359,10 @@ public final class SaveInfo implements Parcelable { private int mFlags; private CustomDescription mCustomDescription; private InternalValidator mValidator; + private ArrayMap<InternalSanitizer, AutofillId[]> mSanitizers; + // Set used to validate against duplicate ids. + private ArraySet<AutofillId> mSanitizerIds; + private AutofillId mTriggerId; /** * Creates a new builder. @@ -363,13 +419,15 @@ public final class SaveInfo implements Parcelable { /** * Sets flags changing the save behavior. * - * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} or {@code 0}. + * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE}, + * {@link #FLAG_DONT_SAVE_ON_FINISH}, or {@code 0}. * @return This builder. */ public @NonNull Builder setFlags(@SaveInfoFlags int flags) { throwIfDestroyed(); - mFlags = Preconditions.checkFlagsArgument(flags, FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE); + mFlags = Preconditions.checkFlagsArgument(flags, + FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE | FLAG_DONT_SAVE_ON_FINISH); return this; } @@ -462,8 +520,8 @@ public final class SaveInfo implements Parcelable { } /** - * Sets an object used to validate the user input - if the input is not valid, the Save UI - * affordance is not shown. + * Sets an object used to validate the user input - if the input is not valid, the + * autofill save UI is not shown. * * <p>Typically used to validate credit card numbers. Examples: * @@ -490,7 +548,7 @@ public final class SaveInfo implements Parcelable { * ); * </pre> * - * <p><b>NOTE: </b>the example above is just for illustrative purposes; the same validator + * <p><b>Note:</b> the example above is just for illustrative purposes; the same validator * could be created using a single regex for the {@code OR} part: * * <pre class="prettyprint"> @@ -531,6 +589,87 @@ public final class SaveInfo implements Parcelable { } /** + * Adds a sanitizer for one or more field. + * + * <p>When a sanitizer is set for a field, the {@link AutofillValue} is sent to the + * sanitizer before a save request is <a href="#TriggeringSaveRequest">triggered</a>. + * + * <p>Typically used to avoid displaying the save UI for values that are autofilled but + * reformattedby the app. For example, to remove spaces between every 4 digits of a + * credit card number: + * + * <pre class="prettyprint"> + * builder.addSanitizer(new TextValueSanitizer( + * Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$", "$1$2$3$4")), + * ccNumberId); + * </pre> + * + * <p>The same sanitizer can be reused to sanitize multiple fields. For example, to trim + * both the username and password fields: + * + * <pre class="prettyprint"> + * builder.addSanitizer( + * new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"), + * usernameId, passwordId); + * </pre> + * + * <p>The sanitizer can also be used as an alternative for a + * {@link #setValidator(Validator) validator}. If any of the {@code ids} is a + * {@link #SaveInfo.Builder(int, AutofillId[]) required id} and the {@code sanitizer} fails + * because of it, then the save UI is not shown. + * + * @param sanitizer an implementation provided by the Android System. + * @param ids id of fields whose value will be sanitized. + * @return this builder. + * + * @throws IllegalArgumentException if a sanitizer for any of the {@code ids} has already + * been added or if {@code ids} is empty. + */ + public @NonNull Builder addSanitizer(@NonNull Sanitizer sanitizer, + @NonNull AutofillId... ids) { + throwIfDestroyed(); + Preconditions.checkArgument(!ArrayUtils.isEmpty(ids), "ids cannot be empty or null"); + Preconditions.checkArgument((sanitizer instanceof InternalSanitizer), + "not provided by Android System: " + sanitizer); + + if (mSanitizers == null) { + mSanitizers = new ArrayMap<>(); + mSanitizerIds = new ArraySet<>(ids.length); + } + + // Check for duplicates first. + for (AutofillId id : ids) { + Preconditions.checkArgument(!mSanitizerIds.contains(id), "already added %s", id); + mSanitizerIds.add(id); + } + + mSanitizers.put((InternalSanitizer) sanitizer, ids); + + return this; + } + + /** + * Explicitly defines the view that should commit the autofill context when clicked. + * + * <p>Usually, the save request is only automatically + * <a href="#TriggeringSaveRequest">triggered</a> after the activity is + * finished or all relevant views become invisible, but there are scenarios where the + * autofill context is automatically commited too late + * —for example, when the activity manually clears the autofillable views when a + * button is tapped. This method can be used to trigger the autofill save UI earlier in + * these scenarios. + * + * <p><b>Note:</b> This method should only be used in scenarios where the automatic workflow + * is not enough, otherwise it could trigger the autofill save UI when it should not— + * for example, when the user entered invalid credentials for the autofillable views. + */ + public @NonNull Builder setTriggerId(@NonNull AutofillId id) { + throwIfDestroyed(); + mTriggerId = Preconditions.checkNotNull(id); + return this; + } + + /** * Builds a new {@link SaveInfo} instance. * * @throws IllegalStateException if no @@ -567,9 +706,14 @@ public final class SaveInfo implements Parcelable { .append(", description=").append(mDescription) .append(DebugUtils.flagsToString(SaveInfo.class, "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle)) - .append(", mFlags=").append(mFlags) - .append(", mCustomDescription=").append(mCustomDescription) - .append(", validation=").append(mValidator) + .append(", flags=").append(mFlags) + .append(", customDescription=").append(mCustomDescription) + .append(", validator=").append(mValidator) + .append(", sanitizerKeys=") + .append(mSanitizerKeys == null ? "N/A:" : mSanitizerKeys.length) + .append(", sanitizerValues=") + .append(mSanitizerValues == null ? "N/A:" : mSanitizerValues.length) + .append(", triggerId=").append(mTriggerId) .append("]").toString(); } @@ -592,6 +736,13 @@ public final class SaveInfo implements Parcelable { parcel.writeCharSequence(mDescription); parcel.writeParcelable(mCustomDescription, flags); parcel.writeParcelable(mValidator, flags); + parcel.writeParcelableArray(mSanitizerKeys, flags); + if (mSanitizerKeys != null) { + for (int i = 0; i < mSanitizerValues.length; i++) { + parcel.writeParcelableArray(mSanitizerValues[i], flags); + } + } + parcel.writeParcelable(mTriggerId, flags); parcel.writeInt(mFlags); } @@ -622,6 +773,20 @@ public final class SaveInfo implements Parcelable { if (validator != null) { builder.setValidator(validator); } + final InternalSanitizer[] sanitizers = + parcel.readParcelableArray(null, InternalSanitizer.class); + if (sanitizers != null) { + final int size = sanitizers.length; + for (int i = 0; i < size; i++) { + final AutofillId[] autofillIds = + parcel.readParcelableArray(null, AutofillId.class); + builder.addSanitizer(sanitizers[i], autofillIds); + } + } + final AutofillId triggerId = parcel.readParcelable(null); + if (triggerId != null) { + builder.setTriggerId(triggerId); + } builder.setFlags(parcel.readInt()); return builder.build(); } diff --git a/core/java/android/service/autofill/SaveRequest.java b/core/java/android/service/autofill/SaveRequest.java index 9de931542cb9..4f85e6b9b23c 100644 --- a/core/java/android/service/autofill/SaveRequest.java +++ b/core/java/android/service/autofill/SaveRequest.java @@ -19,9 +19,9 @@ package android.service.autofill; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Bundle; -import android.os.CancellationSignal; import android.os.Parcel; import android.os.Parcelable; + import com.android.internal.util.Preconditions; import java.util.ArrayList; @@ -36,16 +36,19 @@ import java.util.List; public final class SaveRequest implements Parcelable { private final @NonNull ArrayList<FillContext> mFillContexts; private final @Nullable Bundle mClientState; + private final @Nullable ArrayList<String> mDatasetIds; /** @hide */ public SaveRequest(@NonNull ArrayList<FillContext> fillContexts, - @Nullable Bundle clientState) { + @Nullable Bundle clientState, @Nullable ArrayList<String> datasetIds) { mFillContexts = Preconditions.checkNotNull(fillContexts, "fillContexts"); mClientState = clientState; + mDatasetIds = datasetIds; } private SaveRequest(@NonNull Parcel parcel) { - this(parcel.readTypedArrayList(null), parcel.readBundle()); + this(parcel.createTypedArrayList(FillContext.CREATOR), + parcel.readBundle(), parcel.createStringArrayList()); } /** @@ -56,9 +59,15 @@ public final class SaveRequest implements Parcelable { } /** - * Gets the extra client state returned from the last {@link - * AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)} - * fill request}. + * Gets the latest client state bundle set by the service in a + * {@link FillResponse.Builder#setClientState(Bundle) fill response}. + * + * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state + * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On + * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of + * an authenticated request through the + * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are + * also considered (and take precedence when set). * * @return The client state. */ @@ -66,6 +75,14 @@ public final class SaveRequest implements Parcelable { return mClientState; } + /** + * Gets the ids of the datasets selected by the user, in the order in which they were selected. + */ + @Nullable + public List<String> getDatasetIds() { + return mDatasetIds; + } + @Override public int describeContents() { return 0; @@ -73,8 +90,9 @@ public final class SaveRequest implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { - parcel.writeTypedArrayList(mFillContexts, flags); + parcel.writeTypedList(mFillContexts, flags); parcel.writeBundle(mClientState); + parcel.writeStringList(mDatasetIds); } public static final Creator<SaveRequest> CREATOR = diff --git a/core/java/android/service/autofill/Scorer.java b/core/java/android/service/autofill/Scorer.java new file mode 100644 index 000000000000..f6a802a33e14 --- /dev/null +++ b/core/java/android/service/autofill/Scorer.java @@ -0,0 +1,35 @@ +/* + * 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.service.autofill; + +import android.annotation.TestApi; + +/** + * Helper class used to calculate a score. + * + * <p>Typically used to calculate the field classification score between an actual + * {@link android.view.autofill.AutofillValue} filled by the user and the expected value predicted + * by an autofill service. + * + * TODO(b/67867469): + * - improve javadoc + * - unhide / remove testApi + * @hide + */ +@TestApi +public interface Scorer { + +} diff --git a/core/java/android/service/autofill/TextValueSanitizer.java b/core/java/android/service/autofill/TextValueSanitizer.java new file mode 100644 index 000000000000..e5ad77a1e8d6 --- /dev/null +++ b/core/java/android/service/autofill/TextValueSanitizer.java @@ -0,0 +1,131 @@ +/* + * 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.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Slog; +import android.view.autofill.AutofillValue; + +import com.android.internal.util.Preconditions; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Sanitizes a text {@link AutofillValue} using a regular expression (regex) substitution. + * + * <p>For example, to remove spaces from groups of 4-digits in a credit card: + * + * <pre class="prettyprint"> + * new TextValueSanitizer(Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$", + * "$1$2$3$4") + * </pre> + */ +public final class TextValueSanitizer extends InternalSanitizer implements + Sanitizer, Parcelable { + private static final String TAG = "TextValueSanitizer"; + + private final Pattern mRegex; + private final String mSubst; + + /** + * Default constructor. + * + * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that + * are used to substitute parts of the {@link AutofillValue#getTextValue() text value}. + * @param subst the string that substitutes the matched regex, using {@code $} for + * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc). + */ + public TextValueSanitizer(@NonNull Pattern regex, @NonNull String subst) { + mRegex = Preconditions.checkNotNull(regex); + mSubst = Preconditions.checkNotNull(subst); + } + + /** @hide */ + @Override + @TestApi + @Nullable + public AutofillValue sanitize(@NonNull AutofillValue value) { + if (value == null) { + Slog.w(TAG, "sanitize() called with null value"); + return null; + } + if (!value.isText()) { + if (sDebug) Slog.d(TAG, "sanitize() called with non-text value: " + value); + return null; + } + + final CharSequence text = value.getTextValue(); + + try { + final Matcher matcher = mRegex.matcher(text); + if (!matcher.matches()) { + if (sDebug) Slog.d(TAG, "sanitize(): " + mRegex + " failed for " + value); + return null; + } + + final CharSequence sanitized = matcher.replaceAll(mSubst); + return AutofillValue.forText(sanitized); + } catch (Exception e) { + Slog.w(TAG, "Exception evaluating " + mRegex + "/" + mSubst + ": " + e); + return null; + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "TextValueSanitizer: [regex=" + mRegex + ", subst=" + mSubst + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeSerializable(mRegex); + parcel.writeString(mSubst); + } + + public static final Parcelable.Creator<TextValueSanitizer> CREATOR = + new Parcelable.Creator<TextValueSanitizer>() { + @Override + public TextValueSanitizer createFromParcel(Parcel parcel) { + return new TextValueSanitizer((Pattern) parcel.readSerializable(), parcel.readString()); + } + + @Override + public TextValueSanitizer[] newArray(int size) { + return new TextValueSanitizer[size]; + } + }; +} diff --git a/core/java/android/service/autofill/Transformation.java b/core/java/android/service/autofill/Transformation.java index 4cef261dd389..aa8bc9b9500f 100644 --- a/core/java/android/service/autofill/Transformation.java +++ b/core/java/android/service/autofill/Transformation.java @@ -19,7 +19,7 @@ package android.service.autofill; * Helper class used to change a child view of a {@link android.widget.RemoteViews presentation * template} at runtime, using the values of fields contained in the screen. * - * <p>Typically used by {@link CustomDescription} to provide a customized Save UI affordance. + * <p>Typically used by {@link CustomDescription} to provide a customized autofill save UI. */ public interface Transformation { } diff --git a/core/java/android/service/autofill/SaveInfo.aidl b/core/java/android/service/autofill/UserData.aidl index 8cda608e1814..76016ded424a 100644 --- a/core/java/android/service/autofill/SaveInfo.aidl +++ b/core/java/android/service/autofill/UserData.aidl @@ -16,4 +16,5 @@ package android.service.autofill; -parcelable SaveInfo; +parcelable UserData; +parcelable UserData.Constraints; diff --git a/core/java/android/service/autofill/UserData.java b/core/java/android/service/autofill/UserData.java new file mode 100644 index 000000000000..0d378153aa0d --- /dev/null +++ b/core/java/android/service/autofill/UserData.java @@ -0,0 +1,317 @@ +/* + * 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 static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH; +import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH; +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; +import android.app.ActivityThread; +import android.content.ContentResolver; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.Settings; +import android.util.Log; +import android.view.autofill.Helper; + +import com.android.internal.util.Preconditions; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Class used 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): + * - improve javadoc / add link to section on AutofillService + * - unhide / remove testApi + * @hide + */ +@TestApi +public final class UserData implements Parcelable { + + private static final String TAG = "UserData"; + + private static final int DEFAULT_MAX_USER_DATA_SIZE = 10; + private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10; + private static final int DEFAULT_MIN_VALUE_LENGTH = 5; + private static final int DEFAULT_MAX_VALUE_LENGTH = 100; + + private final InternalScorer mScorer; + private final String[] mRemoteIds; + private final String[] mValues; + + private UserData(Builder builder) { + mScorer = builder.mScorer; + mRemoteIds = new String[builder.mRemoteIds.size()]; + builder.mRemoteIds.toArray(mRemoteIds); + mValues = new String[builder.mValues.size()]; + builder.mValues.toArray(mValues); + } + + /** @hide */ + public InternalScorer getScorer() { + return mScorer; + } + + /** @hide */ + public String[] getRemoteIds() { + return mRemoteIds; + } + + /** @hide */ + public String[] getValues() { + return mValues; + } + + /** @hide */ + public void dump(String prefix, PrintWriter pw) { + pw.print(prefix); pw.print("Scorer: "); pw.println(mScorer); + // Cannot disclose remote ids or values because they could contain PII + pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length); + for (int i = 0; i < mRemoteIds.length; i++) { + pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); + pw.println(Helper.getRedacted(mRemoteIds[i])); + } + pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length); + for (int i = 0; i < mValues.length; i++) { + pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); + pw.println(Helper.getRedacted(mValues[i])); + } + } + + /** @hide */ + public static void dumpConstraints(String prefix, PrintWriter pw) { + pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize()); + pw.print(prefix); pw.print("maxFieldClassificationIdsSize: "); + pw.println(getMaxFieldClassificationIdsSize()); + pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength()); + pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength()); + } + + /** + * A builder for {@link UserData} objects. + * + * TODO(b/67867469): unhide / remove testApi + * + * @hide + */ + @TestApi + public static final class Builder { + private final InternalScorer mScorer; + private final ArrayList<String> mRemoteIds; + private final ArrayList<String> mValues; + private boolean mDestroyed; + + /** + * Creates a new builder for the user data used for <a href="#FieldsClassification">fields + * classification</a>. + * + * @throws IllegalArgumentException if any of the following occurs: + * <ol> + * <li>{@code remoteId} is empty + * <li>{@code value} is empty + * <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()} + * <li>the length of {@code value} is higher than {@link UserData#getMaxValueLength()} + * <li>{@code scorer} is not instance of a class provided by the Android System. + * </ol> + */ + public Builder(@NonNull Scorer scorer, @NonNull String remoteId, @NonNull String value) { + Preconditions.checkArgument((scorer instanceof InternalScorer), + "not provided by Android System: " + scorer); + mScorer = (InternalScorer) scorer; + checkValidRemoteId(remoteId); + checkValidValue(value); + final int capacity = getMaxUserDataSize(); + mRemoteIds = new ArrayList<>(capacity); + mValues = new ArrayList<>(capacity); + mRemoteIds.add(remoteId); + mValues.add(value); + } + + /** + * Adds a new value for user data. + * + * @param remoteId unique string used to identify the user data. + * @param value value of the user data. + * + * @throws IllegalStateException if {@link #build()} or + * {@link #add(String, String)} with the same {@code remoteId} has already + * been called, or if the number of values add (i.e., calls made to this method plus + * constructor) is more than {@link UserData#getMaxUserDataSize()}. + * + * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the + * length of {@code value} is lower than {@link UserData#getMinValueLength()} + * or higher than {@link UserData#getMaxValueLength()}. + */ + public Builder add(@NonNull String remoteId, @NonNull String value) { + throwIfDestroyed(); + checkValidRemoteId(remoteId); + checkValidValue(value); + + Preconditions.checkState(!mRemoteIds.contains(remoteId), + // Don't include remoteId on message because it could contain PII + "already has entry with same remoteId"); + Preconditions.checkState(!mValues.contains(value), + // Don't include remoteId on message because it could contain PII + "already has entry with same value"); + Preconditions.checkState(mRemoteIds.size() < getMaxUserDataSize(), + "already added " + mRemoteIds.size() + " elements"); + mRemoteIds.add(remoteId); + mValues.add(value); + + return this; + } + + private void checkValidRemoteId(@Nullable String remoteId) { + Preconditions.checkNotNull(remoteId); + Preconditions.checkArgument(!remoteId.isEmpty(), "remoteId cannot be empty"); + } + + private void checkValidValue(@Nullable String value) { + Preconditions.checkNotNull(value); + final int length = value.length(); + Preconditions.checkArgumentInRange(length, getMinValueLength(), + getMaxValueLength(), "value length (" + length + ")"); + } + + /** + * Creates a new {@link UserData} instance. + * + * <p>You should not interact with this builder once this method is called. + * + * @throws IllegalStateException if {@link #build()} was already called. + * + * @return The built dataset. + */ + public UserData build() { + throwIfDestroyed(); + mDestroyed = true; + return new UserData(this); + } + + private void throwIfDestroyed() { + if (mDestroyed) { + throw new IllegalStateException("Already called #build()"); + } + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + final StringBuilder builder = new StringBuilder("UserData: [scorer=").append(mScorer); + // Cannot disclose remote ids or values because they could contain PII + builder.append(", remoteIds="); + Helper.appendRedacted(builder, mRemoteIds); + builder.append(", values="); + Helper.appendRedacted(builder, mValues); + return builder.append("]").toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mScorer, flags); + parcel.writeStringArray(mRemoteIds); + parcel.writeStringArray(mValues); + } + + public static final Parcelable.Creator<UserData> CREATOR = + new Parcelable.Creator<UserData>() { + @Override + public UserData createFromParcel(Parcel parcel) { + // 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. + final InternalScorer scorer = parcel.readParcelable(null); + final String[] remoteIds = parcel.readStringArray(); + final String[] values = parcel.readStringArray(); + final Builder builder = new Builder(scorer, remoteIds[0], values[0]); + for (int i = 1; i < remoteIds.length; i++) { + builder.add(remoteIds[i], values[i]); + } + return builder.build(); + } + + @Override + public UserData[] newArray(int size) { + return new UserData[size]; + } + }; + + /** + * Gets the maximum number of values that can be added to a {@link UserData}. + */ + public static int getMaxUserDataSize() { + return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE); + } + + /** + * Gets the maximum number of ids that can be passed to {@link + * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}. + */ + public static int getMaxFieldClassificationIdsSize() { + return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, + DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE); + } + + /** + * Gets the minimum length of values passed to {@link Builder#Builder(Scorer, String, String)}. + */ + public static int getMinValueLength() { + return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH); + } + + /** + * Gets the maximum length of values passed to {@link Builder#Builder(Scorer, String, String)}. + */ + public static int getMaxValueLength() { + return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH); + } + + private static int getInt(String settings, int defaultValue) { + ContentResolver cr = null; + final ActivityThread at = ActivityThread.currentActivityThread(); + if (at != null) { + cr = at.getApplication().getContentResolver(); + } + + if (cr == null) { + Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue); + return defaultValue; + } + return Settings.Secure.getInt(cr, settings, defaultValue); + } +} diff --git a/core/java/android/service/autofill/Validator.java b/core/java/android/service/autofill/Validator.java index 854aa1e69db7..a4036f25af21 100644 --- a/core/java/android/service/autofill/Validator.java +++ b/core/java/android/service/autofill/Validator.java @@ -16,9 +16,9 @@ package android.service.autofill; /** - * Helper class used to define whether the contents of a screen are valid. + * Class used to define whether a condition is satisfied. * - * <p>Typically used to avoid displaying the Save UI affordance when the user input is invalid. + * <p>Typically used to avoid displaying the save UI when the user input is invalid. */ public interface Validator { } diff --git a/core/java/android/service/autofill/Validators.java b/core/java/android/service/autofill/Validators.java index 51b503c21690..0f1ba9891a99 100644 --- a/core/java/android/service/autofill/Validators.java +++ b/core/java/android/service/autofill/Validators.java @@ -33,6 +33,8 @@ public final class Validators { /** * Creates a validator that is only valid if all {@code validators} are valid. * + * <p>Used to represent an {@code AND} boolean operation in a chain of validators. + * * @throws IllegalArgumentException if any element of {@code validators} is an instance of a * class that is not provided by the Android System. */ @@ -44,6 +46,8 @@ public final class Validators { /** * Creates a validator that is valid if any of the {@code validators} is valid. * + * <p>Used to represent an {@code OR} boolean operation in a chain of validators. + * * @throws IllegalArgumentException if any element of {@code validators} is an instance of a * class that is not provided by the Android System. */ @@ -52,6 +56,21 @@ public final class Validators { return new OptionalValidators(getInternalValidators(validators)); } + /** + * Creates a validator that is valid when {@code validator} is not, and vice versa. + * + * <p>Used to represent a {@code NOT} boolean operation in a chain of validators. + * + * @throws IllegalArgumentException if {@code validator} is an instance of a class that is not + * provided by the Android System. + */ + @NonNull + public static Validator not(@NonNull Validator validator) { + Preconditions.checkArgument(validator instanceof InternalValidator, + "validator not provided by Android System: " + validator); + return new NegationValidator((InternalValidator) validator); + } + private static InternalValidator[] getInternalValidators(Validator[] validators) { Preconditions.checkArrayElementsNotNull(validators, "validators"); diff --git a/core/java/android/service/carrier/CarrierService.java b/core/java/android/service/carrier/CarrierService.java index 813acc232289..2707f1467bcf 100644 --- a/core/java/android/service/carrier/CarrierService.java +++ b/core/java/android/service/carrier/CarrierService.java @@ -17,10 +17,13 @@ package android.service.carrier; import android.annotation.CallSuper; import android.app.Service; import android.content.Intent; +import android.os.Bundle; import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.ServiceManager; +import android.util.Log; import com.android.internal.telephony.ITelephonyRegistry; @@ -48,6 +51,8 @@ import com.android.internal.telephony.ITelephonyRegistry; */ public abstract class CarrierService extends Service { + private static final String LOG_TAG = "CarrierService"; + public static final String CARRIER_SERVICE_INTERFACE = "android.service.carrier.CarrierService"; private static ITelephonyRegistry sRegistry; @@ -133,11 +138,26 @@ public abstract class CarrierService extends Service { /** * A wrapper around ICarrierService that forwards calls to implementations of * {@link CarrierService}. + * @hide */ - private class ICarrierServiceWrapper extends ICarrierService.Stub { + public class ICarrierServiceWrapper extends ICarrierService.Stub { + /** @hide */ + public static final int RESULT_OK = 0; + /** @hide */ + public static final int RESULT_ERROR = 1; + /** @hide */ + public static final String KEY_CONFIG_BUNDLE = "config_bundle"; + @Override - public PersistableBundle getCarrierConfig(CarrierIdentifier id) { - return CarrierService.this.onLoadConfig(id); + public void getCarrierConfig(CarrierIdentifier id, ResultReceiver result) { + try { + Bundle data = new Bundle(); + data.putParcelable(KEY_CONFIG_BUNDLE, CarrierService.this.onLoadConfig(id)); + result.send(RESULT_OK, data); + } catch (Exception e) { + Log.e(LOG_TAG, "Error in onLoadConfig: " + e.getMessage(), e); + result.send(RESULT_ERROR, null); + } } } } diff --git a/core/java/android/service/carrier/ICarrierService.aidl b/core/java/android/service/carrier/ICarrierService.aidl index 4c875851cfc8..ac6f9614d8f5 100644 --- a/core/java/android/service/carrier/ICarrierService.aidl +++ b/core/java/android/service/carrier/ICarrierService.aidl @@ -17,6 +17,7 @@ package android.service.carrier; import android.os.PersistableBundle; +import android.os.ResultReceiver; import android.service.carrier.CarrierIdentifier; /** @@ -28,5 +29,5 @@ import android.service.carrier.CarrierIdentifier; interface ICarrierService { /** @see android.service.carrier.CarrierService#onLoadConfig */ - PersistableBundle getCarrierConfig(in CarrierIdentifier id); + oneway void getCarrierConfig(in CarrierIdentifier id, in ResultReceiver result); } diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 6a15adeda9ab..2a245d046486 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/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/core/java/android/service/euicc/EuiccService.java b/core/java/android/service/euicc/EuiccService.java index cd233b831522..df0842f7fb0d 100644 --- a/core/java/android/service/euicc/EuiccService.java +++ b/core/java/android/service/euicc/EuiccService.java @@ -105,6 +105,13 @@ public abstract class EuiccService extends Service { public static final String EXTRA_RESOLUTION_CALLING_PACKAGE = "android.service.euicc.extra.RESOLUTION_CALLING_PACKAGE"; + /** + * Intent extra set for resolution requests containing a boolean indicating whether to ask the + * user to retry another confirmation code. + */ + public static final String EXTRA_RESOLUTION_CONFIRMATION_CODE_RETRIED = + "android.service.euicc.extra.RESOLUTION_CONFIRMATION_CODE_RETRIED"; + /** Result code for a successful operation. */ public static final int RESULT_OK = 0; /** Result code indicating that an active SIM must be deactivated to perform the operation. */ diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java index ce678fc80587..7348cf6848f9 100644 --- a/core/java/android/service/notification/Adjustment.java +++ b/core/java/android/service/notification/Adjustment.java @@ -56,6 +56,15 @@ public final class Adjustment implements Parcelable { public static final String KEY_GROUP_KEY = "key_group_key"; /** + * Data type: int, one of {@link NotificationListenerService.Ranking#USER_SENTIMENT_POSITIVE}, + * {@link NotificationListenerService.Ranking#USER_SENTIMENT_NEUTRAL}, + * {@link NotificationListenerService.Ranking#USER_SENTIMENT_NEGATIVE}. Used to express how + * a user feels about notifications in the same {@link android.app.NotificationChannel} as + * the notification represented by {@link #getKey()}. + */ + public static final String KEY_USER_SENTIMENT = "key_user_sentiment"; + + /** * Create a notification adjustment. * * @param pkg The package of the notification. diff --git a/core/java/android/service/notification/ConditionProviderService.java b/core/java/android/service/notification/ConditionProviderService.java index 3e992ec36426..6fc689ab07cf 100644 --- a/core/java/android/service/notification/ConditionProviderService.java +++ b/core/java/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/core/java/android/service/notification/INotificationListener.aidl b/core/java/android/service/notification/INotificationListener.aidl index ed44f2599695..c388367649c4 100644 --- a/core/java/android/service/notification/INotificationListener.aidl +++ b/core/java/android/service/notification/INotificationListener.aidl @@ -19,6 +19,7 @@ package android.service.notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.os.UserHandle; +import android.service.notification.NotificationStats; import android.service.notification.IStatusBarNotificationHolder; import android.service.notification.StatusBarNotification; import android.service.notification.NotificationRankingUpdate; @@ -26,12 +27,13 @@ import android.service.notification.NotificationRankingUpdate; /** @hide */ oneway interface INotificationListener { - // listeners and rankers + // listeners and assistant void onListenerConnected(in NotificationRankingUpdate update); void onNotificationPosted(in IStatusBarNotificationHolder notificationHolder, in NotificationRankingUpdate update); + // stats only for assistant void onNotificationRemoved(in IStatusBarNotificationHolder notificationHolder, - in NotificationRankingUpdate update, int reason); + in NotificationRankingUpdate update, in NotificationStats stats, int reason); void onNotificationRankingUpdate(in NotificationRankingUpdate update); void onListenerHintsChanged(int hints); void onInterruptionFilterChanged(int interruptionFilter); @@ -40,7 +42,7 @@ oneway interface INotificationListener void onNotificationChannelModification(String pkgName, in UserHandle user, in NotificationChannel channel, int modificationType); void onNotificationChannelGroupModification(String pkgName, in UserHandle user, in NotificationChannelGroup group, int modificationType); - // rankers only + // assistants only void onNotificationEnqueued(in IStatusBarNotificationHolder notificationHolder); void onNotificationSnoozedUntilContext(in IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId); } diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java index d94017cdb2bf..8e52bfa80eda 100644 --- a/core/java/android/service/notification/NotificationAssistantService.java +++ b/core/java/android/service/notification/NotificationAssistantService.java @@ -16,12 +16,9 @@ package android.service.notification; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.NotificationChannel; import android.content.Context; import android.content.Intent; import android.os.Handler; @@ -30,9 +27,9 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; + import com.android.internal.os.SomeArgs; -import java.util.ArrayList; import java.util.List; /** @@ -79,7 +76,7 @@ public abstract class NotificationAssistantService extends NotificationListenerS String snoozeCriterionId); /** - * A notification was posted by an app. Called before alert. + * A notification was posted by an app. Called before post. * * @param sbn the new notification * @return an adjustment or null to take no action, within 100ms. @@ -87,6 +84,34 @@ public abstract class NotificationAssistantService extends NotificationListenerS abstract public Adjustment onNotificationEnqueued(StatusBarNotification sbn); /** + * Implement this method to learn when notifications are removed, how they were interacted with + * before removal, and why they were removed. + * <p> + * This might occur because the user has dismissed the notification using system UI (or another + * notification listener) or because the app has withdrawn the notification. + * <p> + * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the + * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight + * fields such as {@link android.app.Notification#contentView} and + * {@link android.app.Notification#largeIcon}. However, all other fields on + * {@link StatusBarNotification}, sufficient to match this call with a prior call to + * {@link #onNotificationPosted(StatusBarNotification)}, will be intact. + * + ** @param sbn A data structure encapsulating at least the original information (tag and id) + * and source (package name) used to post the {@link android.app.Notification} that + * was just removed. + * @param rankingMap The current ranking map that can be used to retrieve ranking information + * for active notifications. + * @param stats Stats about how the user interacted with the notification before it was removed. + * @param reason see {@link #REASON_LISTENER_CANCEL}, etc. + */ + @Override + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, + NotificationStats stats, int reason) { + onNotificationRemoved(sbn, rankingMap, reason); + } + + /** * Updates a notification. N.B. this won’t cause * an existing notification to alert, but might allow a future update to * this notification to alert. diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index a5223fd8acd5..dac663e765ea 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -20,6 +20,8 @@ import android.annotation.IntDef; 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; @@ -81,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 { @@ -265,7 +269,10 @@ public abstract class NotificationListenerService extends Service { @GuardedBy("mLock") private RankingMap mRankingMap; - private INotificationManager mNoMan; + /** + * @hide + */ + protected INotificationManager mNoMan; /** * Only valid after a successful call to (@link registerAsService}. @@ -389,6 +396,18 @@ public abstract class NotificationListenerService extends Service { } /** + * NotificationStats are not populated for notification listeners, so fall back to + * {@link #onNotificationRemoved(StatusBarNotification, RankingMap, int)}. + * + * @hide + */ + @TestApi + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, + NotificationStats stats, int reason) { + onNotificationRemoved(sbn, rankingMap, reason); + } + + /** * Implement this method to learn about when the listener is enabled and connected to * the notification manager. You are safe to call {@link #getActiveNotifications()} * at this time. @@ -1200,7 +1219,7 @@ public abstract class NotificationListenerService extends Service { @Override public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder, - NotificationRankingUpdate update, int reason) { + NotificationRankingUpdate update, NotificationStats stats, int reason) { StatusBarNotification sbn; try { sbn = sbnHolder.get(); @@ -1215,6 +1234,7 @@ public abstract class NotificationListenerService extends Service { args.arg1 = sbn; args.arg2 = mRankingMap; args.arg3 = reason; + args.arg4 = stats; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_REMOVED, args).sendToTarget(); } @@ -1324,6 +1344,26 @@ public abstract class NotificationListenerService extends Service { * @hide */ public static final int VISIBILITY_NO_OVERRIDE = NotificationManager.VISIBILITY_NO_OVERRIDE; + /** + * The user is likely to have a negative reaction to this notification. + */ + public static final int USER_SENTIMENT_NEGATIVE = -1; + /** + * It is not known how the user will react to this notification. + */ + public static final int USER_SENTIMENT_NEUTRAL = 0; + /** + * The user is likely to have a positive reaction to this notification. + */ + public static final int USER_SENTIMENT_POSITIVE = 1; + + /** @hide */ + @IntDef(prefix = { "USER_SENTIMENT_" }, value = { + USER_SENTIMENT_NEGATIVE, USER_SENTIMENT_NEUTRAL, USER_SENTIMENT_POSITIVE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface UserSentiment {} + private String mKey; private int mRank = -1; private boolean mIsAmbient; @@ -1341,6 +1381,7 @@ public abstract class NotificationListenerService extends Service { // Notification assistant snooze criteria. private ArrayList<SnoozeCriterion> mSnoozeCriteria; private boolean mShowBadge; + private @UserSentiment int mUserSentiment = USER_SENTIMENT_NEUTRAL; public Ranking() {} @@ -1436,6 +1477,17 @@ public abstract class NotificationListenerService extends Service { } /** + * Returns how the system thinks the user feels about notifications from the + * channel provided by {@link #getChannel()}. You can use this information to expose + * controls to help the user block this channel's notifications, if the sentiment is + * {@link #USER_SENTIMENT_NEGATIVE}, or emphasize this notification if the sentiment is + * {@link #USER_SENTIMENT_POSITIVE}. + */ + public int getUserSentiment() { + return mUserSentiment; + } + + /** * If the {@link NotificationAssistantService} has added people to this notification, then * this will be non-null. * @hide @@ -1471,7 +1523,8 @@ public abstract class NotificationListenerService extends Service { int visibilityOverride, int suppressedVisualEffects, int importance, CharSequence explanation, String overrideGroupKey, NotificationChannel channel, ArrayList<String> overridePeople, - ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge) { + ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge, + int userSentiment) { mKey = key; mRank = rank; mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; @@ -1485,6 +1538,7 @@ public abstract class NotificationListenerService extends Service { mOverridePeople = overridePeople; mSnoozeCriteria = snoozeCriteria; mShowBadge = showBadge; + mUserSentiment = userSentiment; } /** @@ -1532,6 +1586,7 @@ public abstract class NotificationListenerService extends Service { private ArrayMap<String, ArrayList<String>> mOverridePeople; private ArrayMap<String, ArrayList<SnoozeCriterion>> mSnoozeCriteria; private ArrayMap<String, Boolean> mShowBadge; + private ArrayMap<String, Integer> mUserSentiment; private RankingMap(NotificationRankingUpdate rankingUpdate) { mRankingUpdate = rankingUpdate; @@ -1560,7 +1615,7 @@ public abstract class NotificationListenerService extends Service { getVisibilityOverride(key), getSuppressedVisualEffects(key), getImportance(key), getImportanceExplanation(key), getOverrideGroupKey(key), getChannel(key), getOverridePeople(key), getSnoozeCriteria(key), - getShowBadge(key)); + getShowBadge(key), getUserSentiment(key)); return rank >= 0; } @@ -1677,6 +1732,17 @@ public abstract class NotificationListenerService extends Service { return showBadge == null ? false : showBadge.booleanValue(); } + private int getUserSentiment(String key) { + synchronized (this) { + if (mUserSentiment == null) { + buildUserSentimentLocked(); + } + } + Integer userSentiment = mUserSentiment.get(key); + return userSentiment == null + ? Ranking.USER_SENTIMENT_NEUTRAL : userSentiment.intValue(); + } + // Locked by 'this' private void buildRanksLocked() { String[] orderedKeys = mRankingUpdate.getOrderedKeys(); @@ -1776,6 +1842,15 @@ public abstract class NotificationListenerService extends Service { } } + // Locked by 'this' + private void buildUserSentimentLocked() { + Bundle userSentiment = mRankingUpdate.getUserSentiment(); + mUserSentiment = new ArrayMap<>(userSentiment.size()); + for (String key : userSentiment.keySet()) { + mUserSentiment.put(key, userSentiment.getInt(key)); + } + } + // ----------- Parcelable @Override @@ -1835,8 +1910,9 @@ public abstract class NotificationListenerService extends Service { StatusBarNotification sbn = (StatusBarNotification) args.arg1; RankingMap rankingMap = (RankingMap) args.arg2; int reason = (int) args.arg3; + NotificationStats stats = (NotificationStats) args.arg4; args.recycle(); - onNotificationRemoved(sbn, rankingMap, reason); + onNotificationRemoved(sbn, rankingMap, stats, reason); } break; case MSG_ON_LISTENER_CONNECTED: { diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java index 326b212a9417..6d51db096a27 100644 --- a/core/java/android/service/notification/NotificationRankingUpdate.java +++ b/core/java/android/service/notification/NotificationRankingUpdate.java @@ -35,12 +35,13 @@ public class NotificationRankingUpdate implements Parcelable { private final Bundle mOverridePeople; private final Bundle mSnoozeCriteria; private final Bundle mShowBadge; + private final Bundle mUserSentiment; public NotificationRankingUpdate(String[] keys, String[] interceptedKeys, Bundle visibilityOverrides, Bundle suppressedVisualEffects, int[] importance, Bundle explanation, Bundle overrideGroupKeys, Bundle channels, Bundle overridePeople, Bundle snoozeCriteria, - Bundle showBadge) { + Bundle showBadge, Bundle userSentiment) { mKeys = keys; mInterceptedKeys = interceptedKeys; mVisibilityOverrides = visibilityOverrides; @@ -52,6 +53,7 @@ public class NotificationRankingUpdate implements Parcelable { mOverridePeople = overridePeople; mSnoozeCriteria = snoozeCriteria; mShowBadge = showBadge; + mUserSentiment = userSentiment; } public NotificationRankingUpdate(Parcel in) { @@ -67,6 +69,7 @@ public class NotificationRankingUpdate implements Parcelable { mOverridePeople = in.readBundle(); mSnoozeCriteria = in.readBundle(); mShowBadge = in.readBundle(); + mUserSentiment = in.readBundle(); } @Override @@ -87,6 +90,7 @@ public class NotificationRankingUpdate implements Parcelable { out.writeBundle(mOverridePeople); out.writeBundle(mSnoozeCriteria); out.writeBundle(mShowBadge); + out.writeBundle(mUserSentiment); } public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR @@ -143,4 +147,8 @@ public class NotificationRankingUpdate implements Parcelable { public Bundle getShowBadge() { return mShowBadge; } + + public Bundle getUserSentiment() { + return mUserSentiment; + } } diff --git a/core/java/android/view/autofill/AutoFillValue.aidl b/core/java/android/service/notification/NotificationStats.aidl index 05b75622c273..40f5548700c6 100644 --- a/core/java/android/view/autofill/AutoFillValue.aidl +++ b/core/java/android/service/notification/NotificationStats.aidl @@ -1,5 +1,5 @@ /** - * Copyright (c) 2016, The Android Open Source Project + * 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. @@ -14,7 +14,6 @@ * limitations under the License. */ -package android.view.autofill; +package android.service.notification; -// @deprecated TODO(b/35956626): remove once clients use AutofillValue -parcelable AutoFillValue;
\ No newline at end of file +parcelable NotificationStats;
\ No newline at end of file diff --git a/core/java/android/service/notification/NotificationStats.java b/core/java/android/service/notification/NotificationStats.java new file mode 100644 index 000000000000..76d5328d2fc5 --- /dev/null +++ b/core/java/android/service/notification/NotificationStats.java @@ -0,0 +1,256 @@ +/** + * 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.service.notification; + +import android.annotation.IntDef; +import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.app.RemoteInput; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @hide + */ +@TestApi +@SystemApi +public final class NotificationStats implements Parcelable { + + private boolean mSeen; + private boolean mExpanded; + private boolean mDirectReplied; + private boolean mSnoozed; + private boolean mViewedSettings; + private boolean mInteracted; + + /** @hide */ + @IntDef(prefix = { "DISMISSAL_SURFACE_" }, value = { + DISMISSAL_NOT_DISMISSED, DISMISSAL_OTHER, DISMISSAL_PEEK, DISMISSAL_AOD, DISMISSAL_SHADE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DismissalSurface {} + + + private @DismissalSurface int mDismissalSurface = DISMISSAL_NOT_DISMISSED; + + /** + * Notification has not been dismissed yet. + */ + public static final int DISMISSAL_NOT_DISMISSED = -1; + /** + * Notification has been dismissed from a {@link NotificationListenerService} or the app + * itself. + */ + public static final int DISMISSAL_OTHER = 0; + /** + * Notification has been dismissed while peeking. + */ + public static final int DISMISSAL_PEEK = 1; + /** + * Notification has been dismissed from always on display. + */ + public static final int DISMISSAL_AOD = 2; + /** + * Notification has been dismissed from the notification shade. + */ + public static final int DISMISSAL_SHADE = 3; + + public NotificationStats() { + } + + protected NotificationStats(Parcel in) { + mSeen = in.readByte() != 0; + mExpanded = in.readByte() != 0; + mDirectReplied = in.readByte() != 0; + mSnoozed = in.readByte() != 0; + mViewedSettings = in.readByte() != 0; + mInteracted = in.readByte() != 0; + mDismissalSurface = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte((byte) (mSeen ? 1 : 0)); + dest.writeByte((byte) (mExpanded ? 1 : 0)); + dest.writeByte((byte) (mDirectReplied ? 1 : 0)); + dest.writeByte((byte) (mSnoozed ? 1 : 0)); + dest.writeByte((byte) (mViewedSettings ? 1 : 0)); + dest.writeByte((byte) (mInteracted ? 1 : 0)); + dest.writeInt(mDismissalSurface); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<NotificationStats> CREATOR = new Creator<NotificationStats>() { + @Override + public NotificationStats createFromParcel(Parcel in) { + return new NotificationStats(in); + } + + @Override + public NotificationStats[] newArray(int size) { + return new NotificationStats[size]; + } + }; + + /** + * Returns whether the user has seen this notification at least once. + */ + public boolean hasSeen() { + return mSeen; + } + + /** + * Records that the user as seen this notification at least once. + */ + public void setSeen() { + mSeen = true; + } + + /** + * Returns whether the user has expanded this notification at least once. + */ + public boolean hasExpanded() { + return mExpanded; + } + + /** + * Records that the user has expanded this notification at least once. + */ + public void setExpanded() { + mExpanded = true; + mInteracted = true; + } + + /** + * Returns whether the user has replied to a notification that has a + * {@link android.app.Notification.Action.Builder#addRemoteInput(RemoteInput) direct reply} at + * least once. + */ + public boolean hasDirectReplied() { + return mDirectReplied; + } + + /** + * Records that the user has replied to a notification that has a + * {@link android.app.Notification.Action.Builder#addRemoteInput(RemoteInput) direct reply} + * at least once. + */ + public void setDirectReplied() { + mDirectReplied = true; + mInteracted = true; + } + + /** + * Returns whether the user has snoozed this notification at least once. + */ + public boolean hasSnoozed() { + return mSnoozed; + } + + /** + * Records that the user has snoozed this notification at least once. + */ + public void setSnoozed() { + mSnoozed = true; + mInteracted = true; + } + + /** + * Returns whether the user has viewed the in-shade settings for this notification at least + * once. + */ + public boolean hasViewedSettings() { + return mViewedSettings; + } + + /** + * Records that the user has viewed the in-shade settings for this notification at least once. + */ + public void setViewedSettings() { + mViewedSettings = true; + mInteracted = true; + } + + /** + * Returns whether the user has interacted with this notification beyond having viewed it. + */ + public boolean hasInteracted() { + return mInteracted; + } + + /** + * Returns from which surface the notification was dismissed. + */ + public @DismissalSurface int getDismissalSurface() { + return mDismissalSurface; + } + + /** + * Returns from which surface the notification was dismissed. + */ + public void setDismissalSurface(@DismissalSurface int dismissalSurface) { + mDismissalSurface = dismissalSurface; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NotificationStats that = (NotificationStats) o; + + if (mSeen != that.mSeen) return false; + if (mExpanded != that.mExpanded) return false; + if (mDirectReplied != that.mDirectReplied) return false; + if (mSnoozed != that.mSnoozed) return false; + if (mViewedSettings != that.mViewedSettings) return false; + if (mInteracted != that.mInteracted) return false; + return mDismissalSurface == that.mDismissalSurface; + } + + @Override + public int hashCode() { + int result = (mSeen ? 1 : 0); + result = 31 * result + (mExpanded ? 1 : 0); + result = 31 * result + (mDirectReplied ? 1 : 0); + result = 31 * result + (mSnoozed ? 1 : 0); + result = 31 * result + (mViewedSettings ? 1 : 0); + result = 31 * result + (mInteracted ? 1 : 0); + result = 31 * result + mDismissalSurface; + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("NotificationStats{"); + sb.append("mSeen=").append(mSeen); + sb.append(", mExpanded=").append(mExpanded); + sb.append(", mDirectReplied=").append(mDirectReplied); + sb.append(", mSnoozed=").append(mSnoozed); + sb.append(", mViewedSettings=").append(mViewedSettings); + sb.append(", mInteracted=").append(mInteracted); + sb.append(", mDismissalSurface=").append(mDismissalSurface); + sb.append('}'); + return sb.toString(); + } +} diff --git a/core/java/android/service/notification/ScheduleCalendar.java b/core/java/android/service/notification/ScheduleCalendar.java new file mode 100644 index 000000000000..8a7ff4da26e3 --- /dev/null +++ b/core/java/android/service/notification/ScheduleCalendar.java @@ -0,0 +1,177 @@ +/* + * 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.service.notification; + +import android.service.notification.ZenModeConfig.ScheduleInfo; +import android.util.ArraySet; +import android.util.Log; + +import java.util.Calendar; +import java.util.Objects; +import java.util.TimeZone; + +/** + * @hide + */ +public class ScheduleCalendar { + public static final String TAG = "ScheduleCalendar"; + public static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG); + private final ArraySet<Integer> mDays = new ArraySet<Integer>(); + private final Calendar mCalendar = Calendar.getInstance(); + + private ScheduleInfo mSchedule; + + @Override + public String toString() { + return "ScheduleCalendar[mDays=" + mDays + ", mSchedule=" + mSchedule + "]"; + } + + /** + * @return true if schedule will exit on alarm, else false + */ + public boolean exitAtAlarm() { + return mSchedule.exitAtAlarm; + } + + /** + * Sets schedule information + */ + public void setSchedule(ScheduleInfo schedule) { + if (Objects.equals(mSchedule, schedule)) return; + mSchedule = schedule; + updateDays(); + } + + /** + * Sets next alarm of the schedule if the saved next alarm has passed or is further + * in the future than given nextAlarm + * @param now current time in milliseconds + * @param nextAlarm time of next alarm in milliseconds + */ + public void maybeSetNextAlarm(long now, long nextAlarm) { + if (mSchedule != null && mSchedule.exitAtAlarm) { + // alarm canceled + if (nextAlarm == 0) { + mSchedule.nextAlarm = 0; + } + // only allow alarms in the future + if (nextAlarm > now) { + // store earliest alarm + if (mSchedule.nextAlarm == 0) { + mSchedule.nextAlarm = nextAlarm; + } else { + mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm); + } + } else if (mSchedule.nextAlarm < now) { + if (DEBUG) { + Log.d(TAG, "All alarms are in the past " + mSchedule.nextAlarm); + } + mSchedule.nextAlarm = 0; + } + } + } + + /** + * Set calendar time zone to tz + * @param tz current time zone + */ + public void setTimeZone(TimeZone tz) { + mCalendar.setTimeZone(tz); + } + + /** + * @param now current time in milliseconds + * @return next time this rule changes (starts or ends) + */ + public long getNextChangeTime(long now) { + if (mSchedule == null) return 0; + final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute); + final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute); + long nextScheduleTime = Math.min(nextStart, nextEnd); + + return nextScheduleTime; + } + + private long getNextTime(long now, int hr, int min) { + final long time = getTime(now, hr, min); + return time <= now ? addDays(time, 1) : time; + } + + private long getTime(long millis, int hour, int min) { + mCalendar.setTimeInMillis(millis); + mCalendar.set(Calendar.HOUR_OF_DAY, hour); + mCalendar.set(Calendar.MINUTE, min); + mCalendar.set(Calendar.SECOND, 0); + mCalendar.set(Calendar.MILLISECOND, 0); + return mCalendar.getTimeInMillis(); + } + + /** + * @param time milliseconds since Epoch + * @return true if time is within the schedule, else false + */ + public boolean isInSchedule(long time) { + if (mSchedule == null || mDays.size() == 0) return false; + final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute); + long end = getTime(time, mSchedule.endHour, mSchedule.endMinute); + if (end <= start) { + end = addDays(end, 1); + } + return isInSchedule(-1, time, start, end) || isInSchedule(0, time, start, end); + } + + /** + * @param time milliseconds since Epoch + * @return true if should exit at time for next alarm, else false + */ + public boolean shouldExitForAlarm(long time) { + if (mSchedule == null) { + return false; + } + return mSchedule.exitAtAlarm + && mSchedule.nextAlarm != 0 + && time >= mSchedule.nextAlarm; + } + + private boolean isInSchedule(int daysOffset, long time, long start, long end) { + final int n = Calendar.SATURDAY; + final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1; + start = addDays(start, daysOffset); + end = addDays(end, daysOffset); + return mDays.contains(day) && time >= start && time < end; + } + + private int getDayOfWeek(long time) { + mCalendar.setTimeInMillis(time); + return mCalendar.get(Calendar.DAY_OF_WEEK); + } + + private void updateDays() { + mDays.clear(); + if (mSchedule != null && mSchedule.days != null) { + for (int i = 0; i < mSchedule.days.length; i++) { + mDays.add(mSchedule.days[i]); + } + } + } + + private long addDays(long time, int days) { + mCalendar.setTimeInMillis(time); + mCalendar.add(Calendar.DATE, days); + return mCalendar.getTimeInMillis(); + } +} diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 7bec898ac347..f658ae03c927 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -46,8 +46,10 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; +import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.TimeZone; import java.util.UUID; /** @@ -64,11 +66,13 @@ public class ZenModeConfig implements Parcelable { public static final int MAX_SOURCE = SOURCE_STAR; private static final int DEFAULT_SOURCE = SOURCE_CONTACT; + public static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE"; + public static final String EVERY_NIGHT_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE"; + public static final List<String> DEFAULT_RULE_IDS = Arrays.asList(EVERY_NIGHT_DEFAULT_RULE_ID, + EVENTS_DEFAULT_RULE_ID); + public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY }; - public static final int[] WEEKNIGHT_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, - Calendar.WEDNESDAY, Calendar.THURSDAY }; - public static final int[] WEEKEND_DAYS = { Calendar.FRIDAY, Calendar.SATURDAY }; public static final int[] MINUTE_BUCKETS = generateMinuteBuckets(); private static final int SECONDS_MS = 1000; @@ -76,10 +80,13 @@ public class ZenModeConfig implements Parcelable { private static final int DAY_MINUTES = 24 * 60; private static final int ZERO_VALUE_MS = 10 * SECONDS_MS; - private static final boolean DEFAULT_ALLOW_CALLS = true; + // Default allow categories set in readXml() from default_zen_mode_config.xml, fallback values: + private static final boolean DEFAULT_ALLOW_ALARMS = true; + private static final boolean DEFAULT_ALLOW_MEDIA_SYSTEM_OTHER = true; + private static final boolean DEFAULT_ALLOW_CALLS = false; private static final boolean DEFAULT_ALLOW_MESSAGES = false; - private static final boolean DEFAULT_ALLOW_REMINDERS = true; - private static final boolean DEFAULT_ALLOW_EVENTS = true; + private static final boolean DEFAULT_ALLOW_REMINDERS = false; + private static final boolean DEFAULT_ALLOW_EVENTS = false; private static final boolean DEFAULT_ALLOW_REPEAT_CALLERS = false; private static final boolean DEFAULT_ALLOW_SCREEN_OFF = true; private static final boolean DEFAULT_ALLOW_SCREEN_ON = true; @@ -89,6 +96,8 @@ public class ZenModeConfig implements Parcelable { private static final String ZEN_ATT_VERSION = "version"; private static final String ZEN_ATT_USER = "user"; private static final String ALLOW_TAG = "allow"; + private static final String ALLOW_ATT_ALARMS = "alarms"; + private static final String ALLOW_ATT_MEDIA_SYSTEM_OTHER = "media_system_other"; private static final String ALLOW_ATT_CALLS = "calls"; private static final String ALLOW_ATT_REPEAT_CALLERS = "repeatCallers"; private static final String ALLOW_ATT_MESSAGES = "messages"; @@ -100,8 +109,6 @@ public class ZenModeConfig implements Parcelable { private static final String ALLOW_ATT_SCREEN_OFF = "visualScreenOff"; private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn"; - private static final String CONDITION_TAG = "condition"; - private static final String CONDITION_ATT_COMPONENT = "component"; private static final String CONDITION_ATT_ID = "id"; private static final String CONDITION_ATT_SUMMARY = "summary"; private static final String CONDITION_ATT_LINE1 = "line1"; @@ -123,6 +130,8 @@ public class ZenModeConfig implements Parcelable { private static final String RULE_ATT_CREATION_TIME = "creationTime"; private static final String RULE_ATT_ENABLER = "enabler"; + public boolean allowAlarms = DEFAULT_ALLOW_ALARMS; + public boolean allowMediaSystemOther = DEFAULT_ALLOW_MEDIA_SYSTEM_OTHER; public boolean allowCalls = DEFAULT_ALLOW_CALLS; public boolean allowRepeatCallers = DEFAULT_ALLOW_REPEAT_CALLERS; public boolean allowMessages = DEFAULT_ALLOW_MESSAGES; @@ -161,6 +170,8 @@ public class ZenModeConfig implements Parcelable { } allowWhenScreenOff = source.readInt() == 1; allowWhenScreenOn = source.readInt() == 1; + allowAlarms = source.readInt() == 1; + allowMediaSystemOther = source.readInt() == 1; } @Override @@ -190,19 +201,23 @@ public class ZenModeConfig implements Parcelable { } dest.writeInt(allowWhenScreenOff ? 1 : 0); dest.writeInt(allowWhenScreenOn ? 1 : 0); + dest.writeInt(allowAlarms ? 1 : 0); + dest.writeInt(allowMediaSystemOther ? 1 : 0); } @Override public String toString() { return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[') .append("user=").append(user) + .append(",allowAlarms=").append(allowAlarms) + .append(",allowMediaSystemOther=").append(allowMediaSystemOther) + .append(",allowReminders=").append(allowReminders) + .append(",allowEvents=").append(allowEvents) .append(",allowCalls=").append(allowCalls) .append(",allowRepeatCallers=").append(allowRepeatCallers) .append(",allowMessages=").append(allowMessages) .append(",allowCallsFrom=").append(sourceToString(allowCallsFrom)) .append(",allowMessagesFrom=").append(sourceToString(allowMessagesFrom)) - .append(",allowReminders=").append(allowReminders) - .append(",allowEvents=").append(allowEvents) .append(",allowWhenScreenOff=").append(allowWhenScreenOff) .append(",allowWhenScreenOn=").append(allowWhenScreenOn) .append(",automaticRules=").append(automaticRules) @@ -218,9 +233,21 @@ public class ZenModeConfig implements Parcelable { if (user != to.user) { d.addLine("user", user, to.user); } + if (allowAlarms != to.allowAlarms) { + d.addLine("allowAlarms", allowAlarms, to.allowAlarms); + } + if (allowMediaSystemOther != to.allowMediaSystemOther) { + d.addLine("allowMediaSystemOther", allowMediaSystemOther, to.allowMediaSystemOther); + } if (allowCalls != to.allowCalls) { d.addLine("allowCalls", allowCalls, to.allowCalls); } + if (allowReminders != to.allowReminders) { + d.addLine("allowReminders", allowReminders, to.allowReminders); + } + if (allowEvents != to.allowEvents) { + d.addLine("allowEvents", allowEvents, to.allowEvents); + } if (allowRepeatCallers != to.allowRepeatCallers) { d.addLine("allowRepeatCallers", allowRepeatCallers, to.allowRepeatCallers); } @@ -233,12 +260,6 @@ public class ZenModeConfig implements Parcelable { if (allowMessagesFrom != to.allowMessagesFrom) { d.addLine("allowMessagesFrom", allowMessagesFrom, to.allowMessagesFrom); } - if (allowReminders != to.allowReminders) { - d.addLine("allowReminders", allowReminders, to.allowReminders); - } - if (allowEvents != to.allowEvents) { - d.addLine("allowEvents", allowEvents, to.allowEvents); - } if (allowWhenScreenOff != to.allowWhenScreenOff) { d.addLine("allowWhenScreenOff", allowWhenScreenOff, to.allowWhenScreenOff); } @@ -335,7 +356,9 @@ public class ZenModeConfig implements Parcelable { if (!(o instanceof ZenModeConfig)) return false; if (o == this) return true; final ZenModeConfig other = (ZenModeConfig) o; - return other.allowCalls == allowCalls + return other.allowAlarms == allowAlarms + && other.allowMediaSystemOther == allowMediaSystemOther + && other.allowCalls == allowCalls && other.allowRepeatCallers == allowRepeatCallers && other.allowMessages == allowMessages && other.allowCallsFrom == allowCallsFrom @@ -351,10 +374,10 @@ public class ZenModeConfig implements Parcelable { @Override public int hashCode() { - return Objects.hash(allowCalls, allowRepeatCallers, allowMessages, allowCallsFrom, - allowMessagesFrom, allowReminders, allowEvents, allowWhenScreenOff, - allowWhenScreenOn, - user, automaticRules, manualRule); + return Objects.hash(allowAlarms, allowMediaSystemOther, allowCalls, + allowRepeatCallers, allowMessages, + allowCallsFrom, allowMessagesFrom, allowReminders, allowEvents, + allowWhenScreenOff, allowWhenScreenOn, user, automaticRules, manualRule); } private static String toDayList(int[] days) { @@ -413,10 +436,12 @@ public class ZenModeConfig implements Parcelable { } if (type == XmlPullParser.START_TAG) { if (ALLOW_TAG.equals(tag)) { - rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false); + rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, + DEFAULT_ALLOW_CALLS); rt.allowRepeatCallers = safeBoolean(parser, ALLOW_ATT_REPEAT_CALLERS, DEFAULT_ALLOW_REPEAT_CALLERS); - rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false); + rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, + DEFAULT_ALLOW_MESSAGES); rt.allowReminders = safeBoolean(parser, ALLOW_ATT_REMINDERS, DEFAULT_ALLOW_REMINDERS); rt.allowEvents = safeBoolean(parser, ALLOW_ATT_EVENTS, DEFAULT_ALLOW_EVENTS); @@ -438,6 +463,9 @@ public class ZenModeConfig implements Parcelable { safeBoolean(parser, ALLOW_ATT_SCREEN_OFF, DEFAULT_ALLOW_SCREEN_OFF); rt.allowWhenScreenOn = safeBoolean(parser, ALLOW_ATT_SCREEN_ON, DEFAULT_ALLOW_SCREEN_ON); + rt.allowAlarms = safeBoolean(parser, ALLOW_ATT_ALARMS, DEFAULT_ALLOW_ALARMS); + rt.allowMediaSystemOther = safeBoolean(parser, ALLOW_ATT_MEDIA_SYSTEM_OTHER, + DEFAULT_ALLOW_MEDIA_SYSTEM_OTHER); } else if (MANUAL_TAG.equals(tag)) { rt.manualRule = readRuleXml(parser); } else if (AUTOMATIC_TAG.equals(tag)) { @@ -468,6 +496,8 @@ public class ZenModeConfig implements Parcelable { out.attribute(null, ALLOW_ATT_MESSAGES_FROM, Integer.toString(allowMessagesFrom)); out.attribute(null, ALLOW_ATT_SCREEN_OFF, Boolean.toString(allowWhenScreenOff)); out.attribute(null, ALLOW_ATT_SCREEN_ON, Boolean.toString(allowWhenScreenOn)); + out.attribute(null, ALLOW_ATT_ALARMS, Boolean.toString(allowAlarms)); + out.attribute(null, ALLOW_ATT_MEDIA_SYSTEM_OTHER, Boolean.toString(allowMediaSystemOther)); out.endTag(null, ALLOW_TAG); if (manualRule != null) { @@ -503,6 +533,13 @@ public class ZenModeConfig implements Parcelable { rt.creationTime = safeLong(parser, RULE_ATT_CREATION_TIME, 0); rt.enabler = parser.getAttributeValue(null, RULE_ATT_ENABLER); rt.condition = readConditionXml(parser); + + // all default rules and user created rules updated to zenMode important interruptions + if (rt.zenMode != Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS + && Condition.isValidId(rt.conditionId, SYSTEM_AUTHORITY)) { + Slog.i(TAG, "Updating zenMode of automatic rule " + rt.name); + rt.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + } return rt; } @@ -654,12 +691,32 @@ public class ZenModeConfig implements Parcelable { if (!allowWhenScreenOn) { suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_SCREEN_ON; } + if (allowAlarms) { + priorityCategories |= Policy.PRIORITY_CATEGORY_ALARMS; + } + if (allowMediaSystemOther) { + priorityCategories |= Policy.PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER; + } priorityCallSenders = sourceToPrioritySenders(allowCallsFrom, priorityCallSenders); priorityMessageSenders = sourceToPrioritySenders(allowMessagesFrom, priorityMessageSenders); return new Policy(priorityCategories, priorityCallSenders, priorityMessageSenders, suppressedVisualEffects); } + /** + * Creates scheduleCalendar from a condition id + * @param conditionId + * @return ScheduleCalendar with info populated with conditionId + */ + public static ScheduleCalendar toScheduleCalendar(Uri conditionId) { + final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(conditionId); + if (schedule == null || schedule.days == null || schedule.days.length == 0) return null; + final ScheduleCalendar sc = new ScheduleCalendar(); + sc.setSchedule(schedule); + sc.setTimeZone(TimeZone.getDefault()); + return sc; + } + private static int sourceToPrioritySenders(int source, int def) { switch (source) { case SOURCE_ANYONE: return Policy.PRIORITY_SENDERS_ANY; @@ -680,10 +737,13 @@ public class ZenModeConfig implements Parcelable { public void applyNotificationPolicy(Policy policy) { if (policy == null) return; - allowCalls = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_CALLS) != 0; - allowMessages = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) != 0; + allowAlarms = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_ALARMS) != 0; + allowMediaSystemOther = (policy.priorityCategories + & Policy.PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER) != 0; allowEvents = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_EVENTS) != 0; allowReminders = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_REMINDERS) != 0; + allowCalls = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_CALLS) != 0; + allowMessages = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) != 0; allowRepeatCallers = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) != 0; allowCallsFrom = prioritySendersToSource(policy.priorityCallSenders, allowCallsFrom); @@ -758,7 +818,10 @@ public class ZenModeConfig implements Parcelable { Condition.FLAG_RELEVANT_NOW); } - private static CharSequence getFormattedTime(Context context, long time, boolean isSameDay, + /** + * Creates readable time from time in milliseconds + */ + public static CharSequence getFormattedTime(Context context, long time, boolean isSameDay, int userHandle) { String skeleton = (!isSameDay ? "EEE " : "") + (DateFormat.is24HourFormat(context, userHandle) ? "Hm" : "hma"); @@ -766,7 +829,10 @@ public class ZenModeConfig implements Parcelable { return DateFormat.format(pattern, time); } - private static boolean isToday(long time) { + /** + * Determines whether a time in milliseconds is today or not + */ + public static boolean isToday(long time) { GregorianCalendar now = new GregorianCalendar(); GregorianCalendar endTime = new GregorianCalendar(); endTime.setTimeInMillis(time); @@ -855,7 +921,17 @@ public class ZenModeConfig implements Parcelable { } public static boolean isValidScheduleConditionId(Uri conditionId) { - return tryParseScheduleConditionId(conditionId) != null; + ScheduleInfo info; + try { + info = tryParseScheduleConditionId(conditionId); + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + return false; + } + + if (info == null || info.days == null || info.days.length == 0) { + return false; + } + return true; } public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) { @@ -1036,7 +1112,10 @@ public class ZenModeConfig implements Parcelable { return UUID.randomUUID().toString().replace("-", ""); } - private static String getOwnerCaption(Context context, String owner) { + /** + * Gets the name of the app associated with owner + */ + public static String getOwnerCaption(Context context, String owner) { final PackageManager pm = context.getPackageManager(); try { final ApplicationInfo info = pm.getApplicationInfo(owner, 0); diff --git a/core/java/android/service/settings/suggestions/ISuggestionService.aidl b/core/java/android/service/settings/suggestions/ISuggestionService.aidl new file mode 100644 index 000000000000..8dfa9c3193d3 --- /dev/null +++ b/core/java/android/service/settings/suggestions/ISuggestionService.aidl @@ -0,0 +1,26 @@ +package android.service.settings.suggestions; + +import android.service.settings.suggestions.Suggestion; + +import java.util.List; + +/** @hide */ +interface ISuggestionService { + + /** + * Return all available suggestions. + */ + List<Suggestion> getSuggestions() = 1; + + /** + * Dismiss a suggestion. The suggestion will not be included in future {@link #getSuggestions) + * calls. + */ + void dismissSuggestion(in Suggestion suggestion) = 2; + + /** + * This is the opposite signal to {@link #dismissSuggestion}, indicating a suggestion has been + * launched. + */ + void launchSuggestion(in Suggestion suggestion) = 3; +}
\ No newline at end of file diff --git a/core/java/android/service/settings/suggestions/Suggestion.aidl b/core/java/android/service/settings/suggestions/Suggestion.aidl new file mode 100644 index 000000000000..b26f12c5f861 --- /dev/null +++ b/core/java/android/service/settings/suggestions/Suggestion.aidl @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** @hide */ +package android.service.settings.suggestions; + +parcelable Suggestion; diff --git a/core/java/android/service/settings/suggestions/Suggestion.java b/core/java/android/service/settings/suggestions/Suggestion.java new file mode 100644 index 000000000000..cfeb7fcead38 --- /dev/null +++ b/core/java/android/service/settings/suggestions/Suggestion.java @@ -0,0 +1,217 @@ +/* + * 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.service.settings.suggestions; + +import android.annotation.IntDef; +import android.annotation.SystemApi; +import android.app.PendingIntent; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Data object that has information about a device suggestion. + * + * @hide + */ +@SystemApi +public final class Suggestion implements Parcelable { + + /** + * @hide + */ + @IntDef(flag = true, value = { + FLAG_HAS_BUTTON, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Flags { + } + + /** + * Flag for suggestion type with a single button + */ + public static final int FLAG_HAS_BUTTON = 1 << 0; + + private final String mId; + private final CharSequence mTitle; + private final CharSequence mSummary; + private final Icon mIcon; + @Flags + private final int mFlags; + private final PendingIntent mPendingIntent; + + /** + * Gets the id for the suggestion object. + */ + public String getId() { + return mId; + } + + /** + * Title of the suggestion that is shown to the user. + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Optional summary describing what this suggestion controls. + */ + public CharSequence getSummary() { + return mSummary; + } + + /** + * Optional icon for this suggestion. + */ + public Icon getIcon() { + return mIcon; + } + + /** + * Optional flags for this suggestion. This will influence UI when rendering suggestion in + * different style. + */ + @Flags + public int getFlags() { + return mFlags; + } + + /** + * The Intent to launch when the suggestion is activated. + */ + public PendingIntent getPendingIntent() { + return mPendingIntent; + } + + private Suggestion(Builder builder) { + mId = builder.mId; + mTitle = builder.mTitle; + mSummary = builder.mSummary; + mIcon = builder.mIcon; + mFlags = builder.mFlags; + mPendingIntent = builder.mPendingIntent; + } + + private Suggestion(Parcel in) { + mId = in.readString(); + mTitle = in.readCharSequence(); + mSummary = in.readCharSequence(); + mIcon = in.readParcelable(Icon.class.getClassLoader()); + mFlags = in.readInt(); + mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader()); + } + + public static final Creator<Suggestion> CREATOR = new Creator<Suggestion>() { + @Override + public Suggestion createFromParcel(Parcel in) { + return new Suggestion(in); + } + + @Override + public Suggestion[] newArray(int size) { + return new Suggestion[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + dest.writeCharSequence(mTitle); + dest.writeCharSequence(mSummary); + dest.writeParcelable(mIcon, flags); + dest.writeInt(mFlags); + dest.writeParcelable(mPendingIntent, flags); + } + + /** + * Builder class for {@link Suggestion}. + */ + public static class Builder { + private final String mId; + private CharSequence mTitle; + private CharSequence mSummary; + private Icon mIcon; + @Flags + private int mFlags; + private PendingIntent mPendingIntent; + + public Builder(String id) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("Suggestion id cannot be empty"); + } + mId = id; + } + + /** + * Sets suggestion title + */ + public Builder setTitle(CharSequence title) { + mTitle = title; + return this; + } + + /** + * Sets suggestion summary + */ + public Builder setSummary(CharSequence summary) { + mSummary = summary; + return this; + } + + /** + * Sets icon for the suggestion. + */ + public Builder setIcon(Icon icon) { + mIcon = icon; + return this; + } + + /** + * Sets a UI type for this suggestion. This will influence UI when rendering suggestion in + * different style. + */ + public Builder setFlags(@Flags int flags) { + mFlags = flags; + return this; + } + + /** + * Sets suggestion intent + */ + public Builder setPendingIntent(PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + return this; + } + + /** + * Builds an immutable {@link Suggestion} object. + */ + public Suggestion build() { + return new Suggestion(this /* builder */); + } + } +} diff --git a/core/java/android/service/settings/suggestions/SuggestionService.java b/core/java/android/service/settings/suggestions/SuggestionService.java new file mode 100644 index 000000000000..ce9501d699ed --- /dev/null +++ b/core/java/android/service/settings/suggestions/SuggestionService.java @@ -0,0 +1,84 @@ +/* + * 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.service.settings.suggestions; + +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +import java.util.List; + +/** + * This is the base class for implementing suggestion service. A suggestion service is responsible + * to provide a collection of {@link Suggestion}s for the current user when queried. + * + * @hide + */ +@SystemApi +public abstract class SuggestionService extends Service { + + private static final String TAG = "SuggestionService"; + private static final boolean DEBUG = false; + + @Override + public IBinder onBind(Intent intent) { + return new ISuggestionService.Stub() { + @Override + public List<Suggestion> getSuggestions() { + if (DEBUG) { + Log.d(TAG, "getSuggestions() " + getPackageName()); + } + return onGetSuggestions(); + } + + @Override + public void dismissSuggestion(Suggestion suggestion) { + if (DEBUG) { + Log.d(TAG, "dismissSuggestion() " + getPackageName()); + } + onSuggestionDismissed(suggestion); + } + + @Override + public void launchSuggestion(Suggestion suggestion) { + if (DEBUG) { + Log.d(TAG, "launchSuggestion() " + getPackageName()); + } + onSuggestionLaunched(suggestion); + } + }; + } + + /** + * Return all available suggestions. + */ + public abstract List<Suggestion> onGetSuggestions(); + + /** + * Dismiss a suggestion. The suggestion will not be included in future + * {@link #onGetSuggestions()} calls. + */ + public abstract void onSuggestionDismissed(Suggestion suggestion); + + /** + * This is the opposite signal to {@link #onSuggestionDismissed}, indicating a suggestion has + * been launched. + */ + public abstract void onSuggestionLaunched(Suggestion suggestion); +} diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java index 625dd9ebfef8..cd177c42d6b3 100644 --- a/core/java/android/service/voice/VoiceInteractionSession.java +++ b/core/java/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/core/java/android/service/vr/IVrManager.aidl b/core/java/android/service/vr/IVrManager.aidl index fef92230e7b8..f7acfc5918a8 100644 --- a/core/java/android/service/vr/IVrManager.aidl +++ b/core/java/android/service/vr/IVrManager.aidl @@ -17,6 +17,7 @@ package android.service.vr; import android.app.Vr2dDisplayProperties; +import android.content.ComponentName; import android.service.vr.IVrStateCallbacks; import android.service.vr.IPersistentVrStateCallbacks; @@ -101,5 +102,21 @@ interface IVrManager { * application's compositor process to bind to, or null to clear the current binding. */ void setAndBindCompositor(in String componentName); + + /** + * 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 standy True if the device is entering standby, false if it's exiting standby. + */ + void setStandbyEnabled(boolean standby); + + /** + * Start VR Input method for the given packageName in {@param componentName}. + * This method notifies InputMethodManagerService to use VR IME instead of + * regular phone IME. + */ + void setVrInputMethod(in ComponentName componentName); + } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 1c6275fb8dc1..e5ab3e1caa3d 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -42,6 +42,7 @@ import android.os.SystemClock; import android.util.Log; import android.util.MergedConfiguration; import android.view.Display; +import android.view.DisplayCutout; import android.view.Gravity; import android.view.IWindowSession; import android.view.InputChannel; @@ -176,6 +177,9 @@ public abstract class WallpaperService extends Service { final Rect mFinalSystemInsets = new Rect(); final Rect mFinalStableInsets = new Rect(); final Rect mBackdropFrame = new Rect(); + final DisplayCutout.ParcelableWrapper mDisplayCutout = + new DisplayCutout.ParcelableWrapper(); + DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT; final MergedConfiguration mMergedConfiguration = new MergedConfiguration(); final WindowManager.LayoutParams mLayout @@ -302,7 +306,8 @@ public abstract class WallpaperService extends Service { public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfiguration, Rect backDropRect, boolean forceLayout, - boolean alwaysConsumeNavBar, int displayId) { + boolean alwaysConsumeNavBar, int displayId, + DisplayCutout.ParcelableWrapper displayCutout) { Message msg = mCaller.obtainMessageIO(MSG_WINDOW_RESIZED, reportDraw ? 1 : 0, outsets); mCaller.sendMessage(msg); @@ -750,7 +755,7 @@ public abstract class WallpaperService extends Service { mInputChannel = new InputChannel(); if (mSession.addToDisplay(mWindow, mWindow.mSeq, mLayout, View.VISIBLE, Display.DEFAULT_DISPLAY, mContentInsets, mStableInsets, mOutsets, - mInputChannel) < 0) { + mDisplayCutout, mInputChannel) < 0) { Log.w(TAG, "Failed to add window while updating wallpaper surface."); return; } @@ -776,7 +781,7 @@ public abstract class WallpaperService extends Service { mWindow, mWindow.mSeq, mLayout, mWidth, mHeight, View.VISIBLE, 0, mWinFrame, mOverscanInsets, mContentInsets, mVisibleInsets, mStableInsets, mOutsets, mBackdropFrame, - mMergedConfiguration, mSurfaceHolder.mSurface); + mDisplayCutout, mMergedConfiguration, mSurfaceHolder.mSurface); if (DEBUG) Log.v(TAG, "New surface: " + mSurfaceHolder.mSurface + ", frame=" + mWinFrame); @@ -800,6 +805,8 @@ public abstract class WallpaperService extends Service { mStableInsets.top += padding.top; mStableInsets.right += padding.right; mStableInsets.bottom += padding.bottom; + mDisplayCutout.set(mDisplayCutout.get().inset(-padding.left, -padding.top, + -padding.right, -padding.bottom)); } if (mCurWidth != w) { @@ -819,6 +826,7 @@ public abstract class WallpaperService extends Service { insetsChanged |= !mDispatchedContentInsets.equals(mContentInsets); insetsChanged |= !mDispatchedStableInsets.equals(mStableInsets); insetsChanged |= !mDispatchedOutsets.equals(mOutsets); + insetsChanged |= !mDispatchedDisplayCutout.equals(mDisplayCutout.get()); mSurfaceHolder.setSurfaceFrameSize(w, h); mSurfaceHolder.mSurfaceLock.unlock(); @@ -885,11 +893,13 @@ public abstract class WallpaperService extends Service { mDispatchedContentInsets.set(mContentInsets); mDispatchedStableInsets.set(mStableInsets); mDispatchedOutsets.set(mOutsets); + mDispatchedDisplayCutout = mDisplayCutout.get(); mFinalSystemInsets.set(mDispatchedOverscanInsets); mFinalStableInsets.set(mDispatchedStableInsets); WindowInsets insets = new WindowInsets(mFinalSystemInsets, null, mFinalStableInsets, - getResources().getConfiguration().isScreenRound(), false); + getResources().getConfiguration().isScreenRound(), false, + mDispatchedDisplayCutout); if (DEBUG) { Log.v(TAG, "dispatching insets=" + insets); } diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java index 1f1863c9dfbc..2900c8382dfc 100644 --- a/core/java/android/speech/tts/AudioPlaybackHandler.java +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -106,6 +106,7 @@ class AudioPlaybackHandler { final PlaybackQueueItem item = it.next(); if (item.getCallerIdentity() == callerIdentity) { it.remove(); + stop(item); } } } diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java index f52638b5a3fc..704a1daf3ec7 100644 --- a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java +++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java @@ -21,6 +21,7 @@ import android.media.AudioTrack; import android.util.Log; import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -70,6 +71,11 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem // wait for the next one. private ConcurrentLinkedQueue<ProgressMarker> markerList = new ConcurrentLinkedQueue<>(); + private static final int NOT_RUN = 0; + private static final int RUN_CALLED = 1; + private static final int STOP_CALLED = 2; + private final AtomicInteger mRunState = new AtomicInteger(NOT_RUN); + SynthesisPlaybackQueueItem(AudioOutputParams audioParams, int sampleRate, int audioFormat, int channelCount, UtteranceProgressDispatcher dispatcher, Object callerIdentity, AbstractEventLogger logger) { @@ -88,6 +94,11 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem @Override public void run() { + if (!mRunState.compareAndSet(NOT_RUN, RUN_CALLED)) { + // stop() was already called before run(). Do nothing and just finish. + return; + } + final UtteranceProgressDispatcher dispatcher = getDispatcher(); dispatcher.dispatchOnStart(); @@ -120,6 +131,12 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem mAudioTrack.waitAndRelease(); + dispatchEndStatus(); + } + + private void dispatchEndStatus() { + final UtteranceProgressDispatcher dispatcher = getDispatcher(); + if (mStatusCode == TextToSpeech.SUCCESS) { dispatcher.dispatchOnSuccess(); } else if(mStatusCode == TextToSpeech.STOPPED) { @@ -140,6 +157,13 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem mStopped = true; mStatusCode = statusCode; + if (mRunState.getAndSet(STOP_CALLED) == NOT_RUN) { + // Dispatch the status code and just finish without signaling + // if run() has not even started. + dispatchEndStatus(); + return; + } + // Wake up the audio playback thread if it was waiting on take(). // take() will return null since mStopped was true, and will then // break out of the data write loop. diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index c645f4057335..10d7911316ac 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -502,10 +502,28 @@ public abstract class TextToSpeechService extends Service { return mCurrentSpeechItem; } - private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) { - SpeechItem old = mCurrentSpeechItem; + private synchronized boolean setCurrentSpeechItem(SpeechItem speechItem) { + // Do not set as current if the item has already been flushed. The check is + // intentionally put inside this synchronized method. Specifically, the following + // racy sequence between this method and stopForApp() needs to be avoided. + // (this method) (stopForApp) + // 1. isFlushed + // 2. startFlushingSpeechItems + // 3. maybeRemoveCurrentSpeechItem + // 4. set mCurrentSpeechItem + // If it happens, stop() is never called on the item. The guard by synchornized(this) + // ensures that the step 3 cannot interrupt between 1 and 4. + if (speechItem != null && isFlushed(speechItem)) { + return false; + } mCurrentSpeechItem = speechItem; - return old; + return true; + } + + private synchronized SpeechItem removeCurrentSpeechItem() { + SpeechItem current = mCurrentSpeechItem; + mCurrentSpeechItem = null; + return current; } private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) { @@ -527,7 +545,7 @@ public abstract class TextToSpeechService extends Service { // Don't process any more speech items getLooper().quit(); // Stop the current speech item - SpeechItem current = setCurrentSpeechItem(null); + SpeechItem current = removeCurrentSpeechItem(); if (current != null) { current.stop(); } @@ -561,12 +579,12 @@ public abstract class TextToSpeechService extends Service { Runnable runnable = new Runnable() { @Override public void run() { - if (isFlushed(speechItem)) { - speechItem.stop(); - } else { - setCurrentSpeechItem(speechItem); + if (setCurrentSpeechItem(speechItem)) { speechItem.play(); - setCurrentSpeechItem(null); + removeCurrentSpeechItem(); + } else { + // The item is alreadly flushed. Stopping. + speechItem.stop(); } } }; @@ -600,7 +618,8 @@ public abstract class TextToSpeechService extends Service { return TextToSpeech.ERROR; } - // Flush pending messages from callerIdentity + // Flush pending messages from callerIdentity. + // See setCurrentSpeechItem on a subtlety around a race condition. startFlushingSpeechItems(callerIdentity); // This stops writing data to the file / or publishing @@ -634,7 +653,7 @@ public abstract class TextToSpeechService extends Service { startFlushingSpeechItems(null); // Stop the current speech item unconditionally . - SpeechItem current = setCurrentSpeechItem(null); + SpeechItem current = removeCurrentSpeechItem(); if (current != null) { current.stop(); } diff --git a/core/java/android/text/AndroidBidi.java b/core/java/android/text/AndroidBidi.java index bbe152321a38..179d545f8ccd 100644 --- a/core/java/android/text/AndroidBidi.java +++ b/core/java/android/text/AndroidBidi.java @@ -16,6 +16,11 @@ package android.text; +import android.icu.lang.UCharacter; +import android.icu.lang.UCharacterDirection; +import android.icu.lang.UProperty; +import android.icu.text.Bidi; +import android.icu.text.BidiClassifier; import android.text.Layout.Directions; import com.android.internal.annotations.VisibleForTesting; @@ -27,26 +32,57 @@ import com.android.internal.annotations.VisibleForTesting; @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public class AndroidBidi { - public static int bidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo) { + private static class EmojiBidiOverride extends BidiClassifier { + EmojiBidiOverride() { + super(null /* No persisting object needed */); + } + + // Tells ICU to use the standard Unicode value. + private static final int NO_OVERRIDE = + UCharacter.getIntPropertyMaxValue(UProperty.BIDI_CLASS) + 1; + + @Override + public int classify(int c) { + if (Emoji.isNewEmoji(c)) { + // All new emoji characters in Unicode 10.0 are of the bidi class ON. + return UCharacterDirection.OTHER_NEUTRAL; + } else { + return NO_OVERRIDE; + } + } + } + + private static final EmojiBidiOverride sEmojiBidiOverride = new EmojiBidiOverride(); + + /** + * Runs the bidi algorithm on input text. + */ + public static int bidi(int dir, char[] chs, byte[] chInfo) { if (chs == null || chInfo == null) { throw new NullPointerException(); } - if (n < 0 || chs.length < n || chInfo.length < n) { + final int length = chs.length; + if (chInfo.length < length) { throw new IndexOutOfBoundsException(); } - switch(dir) { - case Layout.DIR_REQUEST_LTR: dir = 0; break; - case Layout.DIR_REQUEST_RTL: dir = 1; break; - case Layout.DIR_REQUEST_DEFAULT_LTR: dir = -2; break; - case Layout.DIR_REQUEST_DEFAULT_RTL: dir = -1; break; - default: dir = 0; break; + final byte paraLevel; + switch (dir) { + case Layout.DIR_REQUEST_LTR: paraLevel = Bidi.LTR; break; + case Layout.DIR_REQUEST_RTL: paraLevel = Bidi.RTL; break; + case Layout.DIR_REQUEST_DEFAULT_LTR: paraLevel = Bidi.LEVEL_DEFAULT_LTR; break; + case Layout.DIR_REQUEST_DEFAULT_RTL: paraLevel = Bidi.LEVEL_DEFAULT_RTL; break; + default: paraLevel = Bidi.LTR; break; } - - int result = runBidi(dir, chs, chInfo, n, haveInfo); - result = (result & 0x1) == 0 ? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT; - return result; + final Bidi icuBidi = new Bidi(length /* maxLength */, 0 /* maxRunCount */); + icuBidi.setCustomClassifier(sEmojiBidiOverride); + icuBidi.setPara(chs, paraLevel, null /* embeddingLevels */); + for (int i = 0; i < length; i++) { + chInfo[i] = icuBidi.getLevelAt(i); + } + final byte result = icuBidi.getParaLevel(); + return (result & 0x1) == 0 ? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT; } /** @@ -178,6 +214,4 @@ public class AndroidBidi { } return new Directions(ld); } - - private native static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo); }
\ No newline at end of file diff --git a/core/java/android/text/AutoGrowArray.java b/core/java/android/text/AutoGrowArray.java new file mode 100644 index 000000000000..e428377a0a31 --- /dev/null +++ b/core/java/android/text/AutoGrowArray.java @@ -0,0 +1,374 @@ +/* + * 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.text; + +import android.annotation.IntRange; +import android.annotation.NonNull; + +import com.android.internal.util.ArrayUtils; + +import libcore.util.EmptyArray; + +/** + * Implements a growing array of int primitives. + * + * These arrays are NOT thread safe. + * + * @hide + */ +public final class AutoGrowArray { + private static final int MIN_CAPACITY_INCREMENT = 12; + private static final int MAX_CAPACITY_TO_BE_KEPT = 10000; + + /** + * Returns next capacity size. + * + * The returned capacity is larger than requested capacity. + */ + private static int computeNewCapacity(int currentSize, int requested) { + final int targetCapacity = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) + ? MIN_CAPACITY_INCREMENT : currentSize >> 1); + return targetCapacity > requested ? targetCapacity : requested; + } + + /** + * An auto growing byte array. + */ + public static class ByteArray { + + private @NonNull byte[] mValues; + private @IntRange(from = 0) int mSize; + + /** + * Creates an empty ByteArray with the default initial capacity. + */ + public ByteArray() { + this(10); + } + + /** + * Creates an empty ByteArray with the specified initial capacity. + */ + public ByteArray(@IntRange(from = 0) int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.BYTE; + } else { + mValues = ArrayUtils.newUnpaddedByteArray(initialCapacity); + } + mSize = 0; + } + + /** + * Changes the size of this ByteArray. If this ByteArray is shrinked, the backing array + * capacity is unchanged. + */ + public void resize(@IntRange(from = 0) int newSize) { + if (newSize > mValues.length) { + ensureCapacity(newSize - mSize); + } + mSize = newSize; + } + + /** + * Appends the specified value to the end of this array. + */ + public void append(byte value) { + ensureCapacity(1); + mValues[mSize++] = value; + } + + /** + * Ensures capacity to append at least <code>count</code> values. + */ + private void ensureCapacity(@IntRange int count) { + final int requestedSize = mSize + count; + if (requestedSize >= mValues.length) { + final int newCapacity = computeNewCapacity(mSize, requestedSize); + final byte[] newValues = ArrayUtils.newUnpaddedByteArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, mSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + /** + * Removes all values from this array and release the internal array object if it is too + * large. + */ + public void clearWithReleasingLargeArray() { + clear(); + if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) { + mValues = EmptyArray.BYTE; + } + } + + /** + * Returns the value at the specified position in this array. + */ + public byte get(@IntRange(from = 0) int index) { + return mValues[index]; + } + + /** + * Sets the value at the specified position in this array. + */ + public void set(@IntRange(from = 0) int index, byte value) { + mValues[index] = value; + } + + /** + * Returns the number of values in this array. + */ + public @IntRange(from = 0) int size() { + return mSize; + } + + /** + * Returns internal raw array. + * + * Note that this array may have larger size than you requested. + * Use size() instead for getting the actual array size. + */ + public @NonNull byte[] getRawArray() { + return mValues; + } + } + + /** + * An auto growing int array. + */ + public static class IntArray { + + private @NonNull int[] mValues; + private @IntRange(from = 0) int mSize; + + /** + * Creates an empty IntArray with the default initial capacity. + */ + public IntArray() { + this(10); + } + + /** + * Creates an empty IntArray with the specified initial capacity. + */ + public IntArray(@IntRange(from = 0) int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.INT; + } else { + mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity); + } + mSize = 0; + } + + /** + * Changes the size of this IntArray. If this IntArray is shrinked, the backing array + * capacity is unchanged. + */ + public void resize(@IntRange(from = 0) int newSize) { + if (newSize > mValues.length) { + ensureCapacity(newSize - mSize); + } + mSize = newSize; + } + + /** + * Appends the specified value to the end of this array. + */ + public void append(int value) { + ensureCapacity(1); + mValues[mSize++] = value; + } + + /** + * Ensures capacity to append at least <code>count</code> values. + */ + private void ensureCapacity(@IntRange(from = 0) int count) { + final int requestedSize = mSize + count; + if (requestedSize >= mValues.length) { + final int newCapacity = computeNewCapacity(mSize, requestedSize); + final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, mSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + /** + * Removes all values from this array and release the internal array object if it is too + * large. + */ + public void clearWithReleasingLargeArray() { + clear(); + if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) { + mValues = EmptyArray.INT; + } + } + + /** + * Returns the value at the specified position in this array. + */ + public int get(@IntRange(from = 0) int index) { + return mValues[index]; + } + + /** + * Sets the value at the specified position in this array. + */ + public void set(@IntRange(from = 0) int index, int value) { + mValues[index] = value; + } + + /** + * Returns the number of values in this array. + */ + public @IntRange(from = 0) int size() { + return mSize; + } + + /** + * Returns internal raw array. + * + * Note that this array may have larger size than you requested. + * Use size() instead for getting the actual array size. + */ + public @NonNull int[] getRawArray() { + return mValues; + } + } + + /** + * An auto growing float array. + */ + public static class FloatArray { + + private @NonNull float[] mValues; + private @IntRange(from = 0) int mSize; + + /** + * Creates an empty FloatArray with the default initial capacity. + */ + public FloatArray() { + this(10); + } + + /** + * Creates an empty FloatArray with the specified initial capacity. + */ + public FloatArray(@IntRange(from = 0) int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.FLOAT; + } else { + mValues = ArrayUtils.newUnpaddedFloatArray(initialCapacity); + } + mSize = 0; + } + + /** + * Changes the size of this FloatArray. If this FloatArray is shrinked, the backing array + * capacity is unchanged. + */ + public void resize(@IntRange(from = 0) int newSize) { + if (newSize > mValues.length) { + ensureCapacity(newSize - mSize); + } + mSize = newSize; + } + + /** + * Appends the specified value to the end of this array. + */ + public void append(float value) { + ensureCapacity(1); + mValues[mSize++] = value; + } + + /** + * Ensures capacity to append at least <code>count</code> values. + */ + private void ensureCapacity(int count) { + final int requestedSize = mSize + count; + if (requestedSize >= mValues.length) { + final int newCapacity = computeNewCapacity(mSize, requestedSize); + final float[] newValues = ArrayUtils.newUnpaddedFloatArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, mSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + /** + * Removes all values from this array and release the internal array object if it is too + * large. + */ + public void clearWithReleasingLargeArray() { + clear(); + if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) { + mValues = EmptyArray.FLOAT; + } + } + + /** + * Returns the value at the specified position in this array. + */ + public float get(@IntRange(from = 0) int index) { + return mValues[index]; + } + + /** + * Sets the value at the specified position in this array. + */ + public void set(@IntRange(from = 0) int index, float value) { + mValues[index] = value; + } + + /** + * Returns the number of values in this array. + */ + public @IntRange(from = 0) int size() { + return mSize; + } + + /** + * Returns internal raw array. + * + * Note that this array may have larger size than you requested. + * Use size() instead for getting the actual array size. + */ + public @NonNull float[] getRawArray() { + return mValues; + } + } +} diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index eddfe033713e..ce38ebb9bea7 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -35,46 +35,80 @@ import android.text.style.ParagraphStyle; * Canvas.drawText()} directly.</p> */ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback { - public static BoringLayout make(CharSequence source, - TextPaint paint, int outerwidth, - Alignment align, - float spacingmult, float spacingadd, - BoringLayout.Metrics metrics, boolean includepad) { - return new BoringLayout(source, paint, outerwidth, align, - spacingmult, spacingadd, metrics, - includepad); + + /** + * Utility function to construct a BoringLayout instance. + * + * @param source the text to render + * @param paint the default paint for the layout + * @param outerWidth the wrapping width for the text + * @param align whether to left, right, or center the text + * @param spacingMult this value is no longer used by BoringLayout + * @param spacingAdd this value is no longer used by BoringLayout + * @param metrics {@code #Metrics} instance that contains information about FontMetrics and + * line width + * @param includePad set whether to include extra space beyond font ascent and descent which is + * needed to avoid clipping in some scripts + */ + public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth, + Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, + boolean includePad) { + return new BoringLayout(source, paint, outerWidth, align, spacingMult, spacingAdd, metrics, + includePad); } - public static BoringLayout make(CharSequence source, - TextPaint paint, int outerwidth, - Alignment align, - float spacingmult, float spacingadd, - BoringLayout.Metrics metrics, boolean includepad, - TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { - return new BoringLayout(source, paint, outerwidth, align, - spacingmult, spacingadd, metrics, - includepad, ellipsize, ellipsizedWidth); + /** + * Utility function to construct a BoringLayout instance. + * + * @param source the text to render + * @param paint the default paint for the layout + * @param outerWidth the wrapping width for the text + * @param align whether to left, right, or center the text + * @param spacingmult this value is no longer used by BoringLayout + * @param spacingadd this value is no longer used by BoringLayout + * @param metrics {@code #Metrics} instance that contains information about FontMetrics and + * line width + * @param includePad set whether to include extra space beyond font ascent and descent which is + * needed to avoid clipping in some scripts + * @param ellipsize whether to ellipsize the text if width of the text is longer than the + * requested width + * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is + * {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is + * not used, {@code outerWidth} is used instead + */ + public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth, + Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, + boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics, + includePad, ellipsize, ellipsizedWidth); } /** * Returns a BoringLayout for the specified text, potentially reusing * this one if it is already suitable. The caller must make sure that * no one is still using this Layout. + * + * @param source the text to render + * @param paint the default paint for the layout + * @param outerwidth the wrapping width for the text + * @param align whether to left, right, or center the text + * @param spacingMult this value is no longer used by BoringLayout + * @param spacingAdd this value is no longer used by BoringLayout + * @param metrics {@code #Metrics} instance that contains information about FontMetrics and + * line width + * @param includePad set whether to include extra space beyond font ascent and descent which is + * needed to avoid clipping in some scripts */ - public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, - int outerwidth, Alignment align, - float spacingmult, float spacingadd, - BoringLayout.Metrics metrics, - boolean includepad) { - replaceWith(source, paint, outerwidth, align, spacingmult, - spacingadd); + public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, + Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, + boolean includePad) { + replaceWith(source, paint, outerwidth, align, spacingMult, spacingAdd); mEllipsizedWidth = outerwidth; mEllipsizedStart = 0; mEllipsizedCount = 0; - init(source, paint, outerwidth, align, spacingmult, spacingadd, - metrics, includepad, true); + init(source, paint, align, metrics, includePad, true); return this; } @@ -82,95 +116,118 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback * Returns a BoringLayout for the specified text, potentially reusing * this one if it is already suitable. The caller must make sure that * no one is still using this Layout. + * + * @param source the text to render + * @param paint the default paint for the layout + * @param outerWidth the wrapping width for the text + * @param align whether to left, right, or center the text + * @param spacingMult this value is no longer used by BoringLayout + * @param spacingAdd this value is no longer used by BoringLayout + * @param metrics {@code #Metrics} instance that contains information about FontMetrics and + * line width + * @param includePad set whether to include extra space beyond font ascent and descent which is + * needed to avoid clipping in some scripts + * @param ellipsize whether to ellipsize the text if width of the text is longer than the + * requested width + * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is + * {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is + * not used, {@code outerwidth} is used instead */ - public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, - int outerwidth, Alignment align, - float spacingmult, float spacingadd, - BoringLayout.Metrics metrics, - boolean includepad, - TextUtils.TruncateAt ellipsize, - int ellipsizedWidth) { + public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth, + Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, + boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { boolean trust; if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { - replaceWith(source, paint, outerwidth, align, spacingmult, - spacingadd); + replaceWith(source, paint, outerWidth, align, spacingMult, spacingAdd); - mEllipsizedWidth = outerwidth; + mEllipsizedWidth = outerWidth; mEllipsizedStart = 0; mEllipsizedCount = 0; trust = true; } else { - replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, - ellipsize, true, this), - paint, outerwidth, align, spacingmult, - spacingadd); + replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this), + paint, outerWidth, align, spacingMult, spacingAdd); mEllipsizedWidth = ellipsizedWidth; trust = false; } - init(getText(), paint, outerwidth, align, spacingmult, spacingadd, - metrics, includepad, trust); + init(getText(), paint, align, metrics, includePad, trust); return this; } - public BoringLayout(CharSequence source, - TextPaint paint, int outerwidth, - Alignment align, - float spacingmult, float spacingadd, - BoringLayout.Metrics metrics, boolean includepad) { - super(source, paint, outerwidth, align, spacingmult, spacingadd); + /** + * @param source the text to render + * @param paint the default paint for the layout + * @param outerwidth the wrapping width for the text + * @param align whether to left, right, or center the text + * @param spacingMult this value is no longer used by BoringLayout + * @param spacingAdd this value is no longer used by BoringLayout + * @param metrics {@code #Metrics} instance that contains information about FontMetrics and + * line width + * @param includePad set whether to include extra space beyond font ascent and descent which is + * needed to avoid clipping in some scripts + */ + public BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, + float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad) { + super(source, paint, outerwidth, align, spacingMult, spacingAdd); mEllipsizedWidth = outerwidth; mEllipsizedStart = 0; mEllipsizedCount = 0; - init(source, paint, outerwidth, align, spacingmult, spacingadd, - metrics, includepad, true); + init(source, paint, align, metrics, includePad, true); } - public BoringLayout(CharSequence source, - TextPaint paint, int outerwidth, - Alignment align, - float spacingmult, float spacingadd, - BoringLayout.Metrics metrics, boolean includepad, - TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + /** + * + * @param source the text to render + * @param paint the default paint for the layout + * @param outerWidth the wrapping width for the text + * @param align whether to left, right, or center the text + * @param spacingMult this value is no longer used by BoringLayout + * @param spacingAdd this value is no longer used by BoringLayout + * @param metrics {@code #Metrics} instance that contains information about FontMetrics and + * line width + * @param includePad set whether to include extra space beyond font ascent and descent which is + * needed to avoid clipping in some scripts + * @param ellipsize whether to ellipsize the text if width of the text is longer than the + * requested {@code outerwidth} + * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is + * {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is + * not used, {@code outerwidth} is used instead + */ + public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align, + float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad, + TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { /* * It is silly to have to call super() and then replaceWith(), * but we can't use "this" for the callback until the call to * super() finishes. */ - super(source, paint, outerwidth, align, spacingmult, spacingadd); + super(source, paint, outerWidth, align, spacingMult, spacingAdd); boolean trust; if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { - mEllipsizedWidth = outerwidth; + mEllipsizedWidth = outerWidth; mEllipsizedStart = 0; mEllipsizedCount = 0; trust = true; } else { - replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, - ellipsize, true, this), - paint, outerwidth, align, spacingmult, - spacingadd); - + replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this), + paint, outerWidth, align, spacingMult, spacingAdd); mEllipsizedWidth = ellipsizedWidth; trust = false; } - init(getText(), paint, outerwidth, align, spacingmult, spacingadd, - metrics, includepad, trust); + init(getText(), paint, align, metrics, includePad, trust); } - /* package */ void init(CharSequence source, - TextPaint paint, int outerwidth, - Alignment align, - float spacingmult, float spacingadd, - BoringLayout.Metrics metrics, boolean includepad, - boolean trustWidth) { + /* package */ void init(CharSequence source, TextPaint paint, Alignment align, + BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) { int spacing; if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) { @@ -181,7 +238,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback mPaint = paint; - if (includepad) { + if (includePad) { spacing = metrics.bottom - metrics.top; mDesc = metrics.bottom; } else { @@ -206,7 +263,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback TextLine.recycle(line); } - if (includepad) { + if (includePad) { mTopPadding = metrics.top - metrics.ascent; mBottomPadding = metrics.bottom - metrics.descent; } @@ -215,8 +272,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback /** * Returns null if not boring; the width, ascent, and descent if boring. */ - public static Metrics isBoring(CharSequence text, - TextPaint paint) { + public static Metrics isBoring(CharSequence text, TextPaint paint) { return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null); } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index c7a5fce26c3e..fba358cf4c1b 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -16,12 +16,17 @@ package android.text; +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Paint; import android.graphics.Rect; import android.text.style.ReplacementSpan; import android.text.style.UpdateLayout; import android.text.style.WrapTogetherSpan; import android.util.ArraySet; +import android.util.Pools.SynchronizedPool; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -43,129 +48,418 @@ public class DynamicLayout extends Layout private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400; /** - * Make a layout for the specified text that will be updated as - * the text is changed. + * Builder for dynamic layouts. The builder is the preferred pattern for constructing + * DynamicLayout objects and should be preferred over the constructors, particularly to access + * newer features. To build a dynamic layout, first call {@link #obtain} with the required + * arguments (base, paint, and width), then call setters for optional parameters, and finally + * {@link #build} to build the DynamicLayout object. Parameters not explicitly set will get + * default values. */ - public DynamicLayout(CharSequence base, - TextPaint paint, - int width, Alignment align, - float spacingmult, float spacingadd, + public static final class Builder { + private Builder() { + } + + /** + * Obtain a builder for constructing DynamicLayout objects. + */ + @NonNull + public static Builder obtain(@NonNull CharSequence base, @NonNull TextPaint paint, + @IntRange(from = 0) int width) { + Builder b = sPool.acquire(); + if (b == null) { + b = new Builder(); + } + + // set default initial values + b.mBase = base; + b.mDisplay = base; + b.mPaint = paint; + b.mWidth = width; + b.mAlignment = Alignment.ALIGN_NORMAL; + b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; + b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER; + b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION; + b.mIncludePad = true; + b.mFallbackLineSpacing = false; + b.mEllipsizedWidth = width; + b.mEllipsize = null; + b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; + b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; + b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; + return b; + } + + /** + * This method should be called after the layout is finished getting constructed and the + * builder needs to be cleaned up and returned to the pool. + */ + private static void recycle(@NonNull Builder b) { + b.mBase = null; + b.mDisplay = null; + b.mPaint = null; + sPool.release(b); + } + + /** + * Set the transformed text (password transformation being the primary example of a + * transformation) that will be updated as the base text is changed. The default is the + * 'base' text passed to the builder's constructor. + * + * @param display the transformed text + * @return this builder, useful for chaining + */ + @NonNull + public Builder setDisplayText(@NonNull CharSequence display) { + mDisplay = display; + return this; + } + + /** + * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}. + * + * @param alignment Alignment for the resulting {@link DynamicLayout} + * @return this builder, useful for chaining + */ + @NonNull + public Builder setAlignment(@NonNull Alignment alignment) { + mAlignment = alignment; + return this; + } + + /** + * Set the text direction heuristic. The text direction heuristic is used to resolve text + * direction per-paragraph based on the input text. The default is + * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. + * + * @param textDir text direction heuristic for resolving bidi behavior. + * @return this builder, useful for chaining + */ + @NonNull + public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { + mTextDir = textDir; + return this; + } + + /** + * Set line spacing parameters. Each line will have its line spacing multiplied by + * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for + * {@code spacingAdd} and 1.0 for {@code spacingMult}. + * + * @param spacingAdd the amount of line spacing addition + * @param spacingMult the line spacing multiplier + * @return this builder, useful for chaining + * @see android.widget.TextView#setLineSpacing + */ + @NonNull + public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) { + mSpacingAdd = spacingAdd; + mSpacingMult = spacingMult; + return this; + } + + /** + * Set whether to include extra space beyond font ascent and descent (which is needed to + * avoid clipping in some languages, such as Arabic and Kannada). The default is + * {@code true}. + * + * @param includePad whether to include padding + * @return this builder, useful for chaining + * @see android.widget.TextView#setIncludeFontPadding + */ + @NonNull + public Builder setIncludePad(boolean includePad) { + mIncludePad = includePad; + return this; + } + + /** + * Set whether to respect the ascent and descent of the fallback fonts that are used in + * displaying the text (which is needed to avoid text from consecutive lines running into + * each other). If set, fallback fonts that end up getting used can increase the ascent + * and descent of the lines that they are used on. + * + * <p>For backward compatibility reasons, the default is {@code false}, but setting this to + * true is strongly recommended. It is required to be true if text could be in languages + * like Burmese or Tibetan where text is typically much taller or deeper than Latin text. + * + * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts + * @return this builder, useful for chaining + */ + @NonNull + public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) { + mFallbackLineSpacing = useLineSpacingFromFallbacks; + return this; + } + + /** + * Set the width as used for ellipsizing purposes, if it differs from the normal layout + * width. The default is the {@code width} passed to {@link #obtain}. + * + * @param ellipsizedWidth width used for ellipsizing, in pixels + * @return this builder, useful for chaining + * @see android.widget.TextView#setEllipsize + */ + @NonNull + public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) { + mEllipsizedWidth = ellipsizedWidth; + return this; + } + + /** + * Set ellipsizing on the layout. Causes words that are longer than the view is wide, or + * exceeding the number of lines (see #setMaxLines) in the case of + * {@link android.text.TextUtils.TruncateAt#END} or + * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead of broken. + * The default is {@code null}, indicating no ellipsis is to be applied. + * + * @param ellipsize type of ellipsis behavior + * @return this builder, useful for chaining + * @see android.widget.TextView#setEllipsize + */ + public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { + mEllipsize = ellipsize; + return this; + } + + /** + * Set break strategy, useful for selecting high quality or balanced paragraph layout + * options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}. + * + * @param breakStrategy break strategy for paragraph layout + * @return this builder, useful for chaining + * @see android.widget.TextView#setBreakStrategy + */ + @NonNull + public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { + mBreakStrategy = breakStrategy; + return this; + } + + /** + * Set hyphenation frequency, to control the amount of automatic hyphenation used. The + * possible values are defined in {@link Layout}, by constants named with the pattern + * {@code HYPHENATION_FREQUENCY_*}. The default is + * {@link Layout#HYPHENATION_FREQUENCY_NONE}. + * + * @param hyphenationFrequency hyphenation frequency for the paragraph + * @return this builder, useful for chaining + * @see android.widget.TextView#setHyphenationFrequency + */ + @NonNull + public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { + mHyphenationFrequency = hyphenationFrequency; + return this; + } + + /** + * Set paragraph justification mode. The default value is + * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification, + * the last line will be displayed with the alignment set by {@link #setAlignment}. + * + * @param justificationMode justification mode for the paragraph. + * @return this builder, useful for chaining. + */ + @NonNull + public Builder setJustificationMode(@JustificationMode int justificationMode) { + mJustificationMode = justificationMode; + return this; + } + + /** + * Build the {@link DynamicLayout} after options have been set. + * + * <p>Note: the builder object must not be reused in any way after calling this method. + * Setting parameters after calling this method, or calling it a second time on the same + * builder object, will likely lead to unexpected results. + * + * @return the newly constructed {@link DynamicLayout} object + */ + @NonNull + public DynamicLayout build() { + final DynamicLayout result = new DynamicLayout(this); + Builder.recycle(this); + return result; + } + + private CharSequence mBase; + private CharSequence mDisplay; + private TextPaint mPaint; + private int mWidth; + private Alignment mAlignment; + private TextDirectionHeuristic mTextDir; + private float mSpacingMult; + private float mSpacingAdd; + private boolean mIncludePad; + private boolean mFallbackLineSpacing; + private int mBreakStrategy; + private int mHyphenationFrequency; + private int mJustificationMode; + private TextUtils.TruncateAt mEllipsize; + private int mEllipsizedWidth; + + private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); + + private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3); + } + + /** + * Make a layout for the specified text that will be updated as the text is changed. + */ + public DynamicLayout(@NonNull CharSequence base, + @NonNull TextPaint paint, + @IntRange(from = 0) int width, @NonNull Alignment align, + @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad) { this(base, base, paint, width, align, spacingmult, spacingadd, includepad); } /** - * Make a layout for the transformed text (password transformation - * being the primary example of a transformation) - * that will be updated as the base text is changed. + * Make a layout for the transformed text (password transformation being the primary example of + * a transformation) that will be updated as the base text is changed. */ - public DynamicLayout(CharSequence base, CharSequence display, - TextPaint paint, - int width, Alignment align, - float spacingmult, float spacingadd, + public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, + @NonNull TextPaint paint, + @IntRange(from = 0) int width, @NonNull Alignment align, + @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad) { this(base, display, paint, width, align, spacingmult, spacingadd, includepad, null, 0); } /** - * Make a layout for the transformed text (password transformation - * being the primary example of a transformation) - * that will be updated as the base text is changed. - * If ellipsize is non-null, the Layout will ellipsize the text - * down to ellipsizedWidth. + * Make a layout for the transformed text (password transformation being the primary example of + * a transformation) that will be updated as the base text is changed. If ellipsize is non-null, + * the Layout will ellipsize the text down to ellipsizedWidth. */ - public DynamicLayout(CharSequence base, CharSequence display, - TextPaint paint, - int width, Alignment align, - float spacingmult, float spacingadd, + public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, + @NonNull TextPaint paint, + @IntRange(from = 0) int width, @NonNull Alignment align, + @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad, - TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + @Nullable TextUtils.TruncateAt ellipsize, + @IntRange(from = 0) int ellipsizedWidth) { this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingmult, spacingadd, includepad, - StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE, + Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE, Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth); } /** - * Make a layout for the transformed text (password transformation - * being the primary example of a transformation) - * that will be updated as the base text is changed. - * If ellipsize is non-null, the Layout will ellipsize the text - * down to ellipsizedWidth. - * * - * *@hide + * Make a layout for the transformed text (password transformation being the primary example of + * a transformation) that will be updated as the base text is changed. If ellipsize is non-null, + * the Layout will ellipsize the text down to ellipsizedWidth. + * + * @hide */ - public DynamicLayout(CharSequence base, CharSequence display, - TextPaint paint, - int width, Alignment align, TextDirectionHeuristic textDir, - float spacingmult, float spacingadd, - boolean includepad, int breakStrategy, int hyphenationFrequency, - int justificationMode, TextUtils.TruncateAt ellipsize, - int ellipsizedWidth) { - super((ellipsize == null) - ? display - : (display instanceof Spanned) - ? new SpannedEllipsizer(display) - : new Ellipsizer(display), + public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, + @NonNull TextPaint paint, + @IntRange(from = 0) int width, + @NonNull Alignment align, @NonNull TextDirectionHeuristic textDir, + @FloatRange(from = 0.0) float spacingmult, float spacingadd, + boolean includepad, @BreakStrategy int breakStrategy, + @HyphenationFrequency int hyphenationFrequency, + @JustificationMode int justificationMode, + @Nullable TextUtils.TruncateAt ellipsize, + @IntRange(from = 0) int ellipsizedWidth) { + super(createEllipsizer(ellipsize, display), paint, width, align, textDir, spacingmult, spacingadd); - mBase = base; + final Builder b = Builder.obtain(base, paint, width) + .setAlignment(align) + .setTextDirection(textDir) + .setLineSpacing(spacingadd, spacingmult) + .setEllipsizedWidth(ellipsizedWidth) + .setEllipsize(ellipsize); mDisplay = display; - - if (ellipsize != null) { - mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); - mEllipsizedWidth = ellipsizedWidth; - mEllipsizeAt = ellipsize; - } else { - mInts = new PackedIntVector(COLUMNS_NORMAL); - mEllipsizedWidth = width; - mEllipsizeAt = null; - } - - mObjects = new PackedObjectVector<Directions>(1); - mIncludePad = includepad; mBreakStrategy = breakStrategy; mJustificationMode = justificationMode; mHyphenationFrequency = hyphenationFrequency; - /* - * This is annoying, but we can't refer to the layout until - * superclass construction is finished, and the superclass - * constructor wants the reference to the display text. - * - * This will break if the superclass constructor ever actually - * cares about the content instead of just holding the reference. - */ - if (ellipsize != null) { - Ellipsizer e = (Ellipsizer) getText(); + generate(b); + + Builder.recycle(b); + } + + private DynamicLayout(@NonNull Builder b) { + super(createEllipsizer(b.mEllipsize, b.mDisplay), + b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd); + + mDisplay = b.mDisplay; + mIncludePad = b.mIncludePad; + mBreakStrategy = b.mBreakStrategy; + mJustificationMode = b.mJustificationMode; + mHyphenationFrequency = b.mHyphenationFrequency; + + generate(b); + } + @NonNull + private static CharSequence createEllipsizer(@Nullable TextUtils.TruncateAt ellipsize, + @NonNull CharSequence display) { + if (ellipsize == null) { + return display; + } else if (display instanceof Spanned) { + return new SpannedEllipsizer(display); + } else { + return new Ellipsizer(display); + } + } + + private void generate(@NonNull Builder b) { + mBase = b.mBase; + mFallbackLineSpacing = b.mFallbackLineSpacing; + if (b.mEllipsize != null) { + mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); + mEllipsizedWidth = b.mEllipsizedWidth; + mEllipsizeAt = b.mEllipsize; + + /* + * This is annoying, but we can't refer to the layout until superclass construction is + * finished, and the superclass constructor wants the reference to the display text. + * + * In other words, the two Ellipsizer classes in Layout.java need a + * (Dynamic|Static)Layout as a parameter to do their calculations, but the Ellipsizers + * also need to be the input to the superclass's constructor (Layout). In order to go + * around the circular dependency, we construct the Ellipsizer with only one of the + * parameters, the text (in createEllipsizer). And we fill in the rest of the needed + * information (layout, width, and method) later, here. + * + * This will break if the superclass constructor ever actually cares about the content + * instead of just holding the reference. + */ + final Ellipsizer e = (Ellipsizer) getText(); e.mLayout = this; - e.mWidth = ellipsizedWidth; - e.mMethod = ellipsize; + e.mWidth = b.mEllipsizedWidth; + e.mMethod = b.mEllipsize; mEllipsize = true; + } else { + mInts = new PackedIntVector(COLUMNS_NORMAL); + mEllipsizedWidth = b.mWidth; + mEllipsizeAt = null; } - // Initial state is a single line with 0 characters (0 to 0), - // with top at 0 and bottom at whatever is natural, and - // undefined ellipsis. + mObjects = new PackedObjectVector<>(1); + + // Initial state is a single line with 0 characters (0 to 0), with top at 0 and bottom at + // whatever is natural, and undefined ellipsis. int[] start; - if (ellipsize != null) { + if (b.mEllipsize != null) { start = new int[COLUMNS_ELLIPSIZE]; start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; } else { start = new int[COLUMNS_NORMAL]; } - Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; + final Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; - Paint.FontMetricsInt fm = paint.getFontMetricsInt(); - int asc = fm.ascent; - int desc = fm.descent; + final Paint.FontMetricsInt fm = b.mFontMetricsInt; + b.mPaint.getFontMetricsInt(fm); + final int asc = fm.ascent; + final int desc = fm.descent; start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT; start[TOP] = 0; @@ -177,20 +471,22 @@ public class DynamicLayout extends Layout mObjects.insertAt(0, dirs); + final int baseLength = mBase.length(); // Update from 0 characters to whatever the real text is - reflow(base, 0, 0, base.length()); + reflow(mBase, 0, 0, baseLength); - if (base instanceof Spannable) { + if (mBase instanceof Spannable) { if (mWatcher == null) mWatcher = new ChangeWatcher(this); // Strip out any watchers for other DynamicLayouts. - Spannable sp = (Spannable) base; - ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class); - for (int i = 0; i < spans.length; i++) + final Spannable sp = (Spannable) mBase; + final ChangeWatcher[] spans = sp.getSpans(0, baseLength, ChangeWatcher.class); + for (int i = 0; i < spans.length; i++) { sp.removeSpan(spans[i]); + } - sp.setSpan(mWatcher, 0, base.length(), + sp.setSpan(mWatcher, 0, baseLength, Spannable.SPAN_INCLUSIVE_INCLUSIVE | (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT)); } @@ -299,12 +595,15 @@ public class DynamicLayout extends Layout .setWidth(getWidth()) .setTextDirection(getTextDirectionHeuristic()) .setLineSpacing(getSpacingAdd(), getSpacingMultiplier()) + .setUseLineSpacingFromFallbacks(mFallbackLineSpacing) .setEllipsizedWidth(mEllipsizedWidth) .setEllipsize(mEllipsizeAt) .setBreakStrategy(mBreakStrategy) .setHyphenationFrequency(mHyphenationFrequency) - .setJustificationMode(mJustificationMode); - reflowed.generate(b, false, true); + .setJustificationMode(mJustificationMode) + .setAddLastLineLineSpacing(!islast); + + reflowed.generate(b, false /*includepad*/, true /*trackpad*/); int n = reflowed.getLineCount(); // If the new layout has a blank line at the end, but it is not // the very end of the buffer, then we already have a line that @@ -365,6 +664,7 @@ public class DynamicLayout extends Layout desc += botpad; ints[DESCENT] = desc; + ints[EXTRA] = reflowed.getLineExtra(i); objects[0] = reflowed.getLineDirections(i); final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1); @@ -692,6 +992,14 @@ public class DynamicLayout extends Layout return mInts.getValue(line, DESCENT); } + /** + * @hide + */ + @Override + public int getLineExtra(int line) { + return mInts.getValue(line, EXTRA); + } + @Override public int getLineStart(int line) { return mInts.getValue(line, START) & START_MASK; @@ -742,16 +1050,17 @@ public class DynamicLayout extends Layout private static class ChangeWatcher implements TextWatcher, SpanWatcher { public ChangeWatcher(DynamicLayout layout) { - mLayout = new WeakReference<DynamicLayout>(layout); + mLayout = new WeakReference<>(layout); } private void reflow(CharSequence s, int where, int before, int after) { DynamicLayout ml = mLayout.get(); - if (ml != null) + if (ml != null) { ml.reflow(s, where, before, after); - else if (s instanceof Spannable) + } else if (s instanceof Spannable) { ((Spannable) s).removeSpan(this); + } } public void beforeTextChanged(CharSequence s, int where, int before, int after) { @@ -808,6 +1117,7 @@ public class DynamicLayout extends Layout private CharSequence mDisplay; private ChangeWatcher mWatcher; private boolean mIncludePad; + private boolean mFallbackLineSpacing; private boolean mEllipsize; private int mEllipsizedWidth; private TextUtils.TruncateAt mEllipsizeAt; @@ -851,14 +1161,15 @@ public class DynamicLayout extends Layout private static final int TAB = START; private static final int TOP = 1; private static final int DESCENT = 2; + private static final int EXTRA = 3; // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry. - private static final int HYPHEN = 3; + private static final int HYPHEN = 4; private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN; - private static final int COLUMNS_NORMAL = 4; + private static final int COLUMNS_NORMAL = 5; - private static final int ELLIPSIS_START = 4; - private static final int ELLIPSIS_COUNT = 5; - private static final int COLUMNS_ELLIPSIZE = 6; + private static final int ELLIPSIS_START = 5; + private static final int ELLIPSIS_COUNT = 6; + private static final int COLUMNS_ELLIPSIZE = 7; private static final int START_MASK = 0x1FFFFFFF; private static final int DIR_SHIFT = 30; diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java index ed583907123b..4654e83c1af6 100644 --- a/core/java/android/text/FontConfig.java +++ b/core/java/android/text/FontConfig.java @@ -64,17 +64,19 @@ public final class FontConfig { private final int mWeight; private final boolean mIsItalic; private Uri mUri; + private final String mFallbackFor; /** * @hide */ public Font(@NonNull String fontName, int ttcIndex, @NonNull FontVariationAxis[] axes, - int weight, boolean isItalic) { + int weight, boolean isItalic, String fallbackFor) { mFontName = fontName; mTtcIndex = ttcIndex; mAxes = axes; mWeight = weight; mIsItalic = isItalic; + mFallbackFor = fallbackFor; } /** @@ -125,6 +127,10 @@ public final class FontConfig { public void setUri(@NonNull Uri uri) { mUri = uri; } + + public String getFallbackFor() { + return mFallbackFor; + } } /** @@ -169,7 +175,7 @@ public final class FontConfig { public static final class Family { private final @NonNull String mName; private final @NonNull Font[] mFonts; - private final @NonNull String mLanguage; + private final @NonNull String[] mLanguages; /** @hide */ @Retention(SOURCE) @@ -203,11 +209,11 @@ public final class FontConfig { // See frameworks/minikin/include/minikin/FontFamily.h private final @Variant int mVariant; - public Family(@NonNull String name, @NonNull Font[] fonts, @NonNull String language, + public Family(@NonNull String name, @NonNull Font[] fonts, @NonNull String[] languages, @Variant int variant) { mName = name; mFonts = fonts; - mLanguage = language; + mLanguages = languages; mVariant = variant; } @@ -226,10 +232,10 @@ public final class FontConfig { } /** - * Returns the language for this family. May be null. + * Returns the languages for this family. May be null. */ - public @Nullable String getLanguage() { - return mLanguage; + public @Nullable String[] getLanguages() { + return mLanguages; } /** diff --git a/core/java/android/text/Hyphenator.java b/core/java/android/text/Hyphenator.java index ea1100eabf8c..4f1488e1029f 100644 --- a/core/java/android/text/Hyphenator.java +++ b/core/java/android/text/Hyphenator.java @@ -16,254 +16,15 @@ package android.text; -import android.annotation.Nullable; -import android.util.Log; - -import com.android.internal.annotations.GuardedBy; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.util.HashMap; -import java.util.Locale; - /** - * Hyphenator is a wrapper class for a native implementation of automatic hyphenation, + * Hyphenator just initializes the native implementation of automatic hyphenation, * in essence finding valid hyphenation opportunities in a word. * * @hide */ public class Hyphenator { - // This class has deliberately simple lifetime management (no finalizer) because in - // the common case a process will use a very small number of locales. - - private static String TAG = "Hyphenator"; - - // TODO: Confirm that these are the best values. Various sources suggest (1, 1), but - // that appears too small. - private static final int INDIC_MIN_PREFIX = 2; - private static final int INDIC_MIN_SUFFIX = 2; - - private final static Object sLock = new Object(); - - @GuardedBy("sLock") - final static HashMap<Locale, Hyphenator> sMap = new HashMap<Locale, Hyphenator>(); - - // Reasonable enough values for cases where we have no hyphenation patterns but may be able to - // do some automatic hyphenation based on characters. These values would be used very rarely. - private static final int DEFAULT_MIN_PREFIX = 2; - private static final int DEFAULT_MIN_SUFFIX = 2; - final static Hyphenator sEmptyHyphenator = - new Hyphenator(StaticLayout.nLoadHyphenator( - null, 0, DEFAULT_MIN_PREFIX, DEFAULT_MIN_SUFFIX), - null); - - final private long mNativePtr; - - // We retain a reference to the buffer to keep the memory mapping valid - @SuppressWarnings("unused") - final private ByteBuffer mBuffer; - - private Hyphenator(long nativePtr, ByteBuffer b) { - mNativePtr = nativePtr; - mBuffer = b; - } - - public long getNativePtr() { - return mNativePtr; - } - - public static Hyphenator get(@Nullable Locale locale) { - synchronized (sLock) { - Hyphenator result = sMap.get(locale); - if (result != null) { - return result; - } - - // If there's a variant, fall back to language+variant only, if available - final String variant = locale.getVariant(); - if (!variant.isEmpty()) { - final Locale languageAndVariantOnlyLocale = - new Locale(locale.getLanguage(), "", variant); - result = sMap.get(languageAndVariantOnlyLocale); - if (result != null) { - sMap.put(locale, result); - return result; - } - } - - // Fall back to language-only, if available - final Locale languageOnlyLocale = new Locale(locale.getLanguage()); - result = sMap.get(languageOnlyLocale); - if (result != null) { - sMap.put(locale, result); - return result; - } - - // Fall back to script-only, if available - final String script = locale.getScript(); - if (!script.equals("")) { - final Locale scriptOnlyLocale = new Locale.Builder() - .setLanguage("und") - .setScript(script) - .build(); - result = sMap.get(scriptOnlyLocale); - if (result != null) { - sMap.put(locale, result); - return result; - } - } - - sMap.put(locale, sEmptyHyphenator); // To remember we found nothing. - } - return sEmptyHyphenator; - } - - private static class HyphenationData { - final String mLanguageTag; - final int mMinPrefix, mMinSuffix; - HyphenationData(String languageTag, int minPrefix, int minSuffix) { - this.mLanguageTag = languageTag; - this.mMinPrefix = minPrefix; - this.mMinSuffix = minSuffix; - } - } - - private static Hyphenator loadHyphenator(HyphenationData data) { - String patternFilename = "hyph-" + data.mLanguageTag.toLowerCase(Locale.US) + ".hyb"; - File patternFile = new File(getSystemHyphenatorLocation(), patternFilename); - if (!patternFile.canRead()) { - Log.e(TAG, "hyphenation patterns for " + patternFile + " not found or unreadable"); - return null; - } - try { - RandomAccessFile f = new RandomAccessFile(patternFile, "r"); - try { - FileChannel fc = f.getChannel(); - MappedByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); - long nativePtr = StaticLayout.nLoadHyphenator( - buf, 0, data.mMinPrefix, data.mMinSuffix); - return new Hyphenator(nativePtr, buf); - } finally { - f.close(); - } - } catch (IOException e) { - Log.e(TAG, "error loading hyphenation " + patternFile, e); - return null; - } - } - - private static File getSystemHyphenatorLocation() { - return new File("/system/usr/hyphen-data"); - } - - // This array holds pairs of language tags that are used to prefill the map from locale to - // hyphenation data: The hyphenation data for the first field will be prefilled from the - // hyphenation data for the second field. - // - // The aliases that are computable by the get() method above are not included. - private static final String[][] LOCALE_FALLBACK_DATA = { - // English locales that fall back to en-US. The data is - // from CLDR. It's all English locales, minus the locales whose - // parent is en-001 (from supplementalData.xml, under <parentLocales>). - // TODO: Figure out how to get this from ICU. - {"en-AS", "en-US"}, // English (American Samoa) - {"en-GU", "en-US"}, // English (Guam) - {"en-MH", "en-US"}, // English (Marshall Islands) - {"en-MP", "en-US"}, // English (Northern Mariana Islands) - {"en-PR", "en-US"}, // English (Puerto Rico) - {"en-UM", "en-US"}, // English (United States Minor Outlying Islands) - {"en-VI", "en-US"}, // English (Virgin Islands) - - // All English locales other than those falling back to en-US are mapped to en-GB. - {"en", "en-GB"}, - - // For German, we're assuming the 1996 (and later) orthography by default. - {"de", "de-1996"}, - // Liechtenstein uses the Swiss hyphenation rules for the 1901 orthography. - {"de-LI-1901", "de-CH-1901"}, - - // Norwegian is very probably Norwegian Bokmål. - {"no", "nb"}, - - // Use mn-Cyrl. According to CLDR's likelySubtags.xml, mn is most likely to be mn-Cyrl. - {"mn", "mn-Cyrl"}, // Mongolian - - // Fall back to Ethiopic script for languages likely to be written in Ethiopic. - // Data is from CLDR's likelySubtags.xml. - // TODO: Convert this to a mechanism using ICU4J's ULocale#addLikelySubtags(). - {"am", "und-Ethi"}, // Amharic - {"byn", "und-Ethi"}, // Blin - {"gez", "und-Ethi"}, // Geʻez - {"ti", "und-Ethi"}, // Tigrinya - {"wal", "und-Ethi"}, // Wolaytta - }; - - private static final HyphenationData[] AVAILABLE_LANGUAGES = { - new HyphenationData("as", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Assamese - new HyphenationData("bg", 2, 2), // Bulgarian - new HyphenationData("bn", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Bengali - new HyphenationData("cu", 1, 2), // Church Slavonic - new HyphenationData("cy", 2, 3), // Welsh - new HyphenationData("da", 2, 2), // Danish - new HyphenationData("de-1901", 2, 2), // German 1901 orthography - new HyphenationData("de-1996", 2, 2), // German 1996 orthography - new HyphenationData("de-CH-1901", 2, 2), // Swiss High German 1901 orthography - new HyphenationData("en-GB", 2, 3), // British English - new HyphenationData("en-US", 2, 3), // American English - new HyphenationData("es", 2, 2), // Spanish - new HyphenationData("et", 2, 3), // Estonian - new HyphenationData("eu", 2, 2), // Basque - new HyphenationData("fr", 2, 3), // French - new HyphenationData("ga", 2, 3), // Irish - new HyphenationData("gu", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Gujarati - new HyphenationData("hi", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Hindi - new HyphenationData("hr", 2, 2), // Croatian - new HyphenationData("hu", 2, 2), // Hungarian - // texhyphen sources say Armenian may be (1, 2), but that it needs confirmation. - // Going with a more conservative value of (2, 2) for now. - new HyphenationData("hy", 2, 2), // Armenian - new HyphenationData("kn", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Kannada - new HyphenationData("ml", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Malayalam - new HyphenationData("mn-Cyrl", 2, 2), // Mongolian in Cyrillic script - new HyphenationData("mr", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Marathi - new HyphenationData("nb", 2, 2), // Norwegian Bokmål - new HyphenationData("nn", 2, 2), // Norwegian Nynorsk - new HyphenationData("or", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Oriya - new HyphenationData("pa", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Punjabi - new HyphenationData("pt", 2, 3), // Portuguese - new HyphenationData("sl", 2, 2), // Slovenian - new HyphenationData("ta", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Tamil - new HyphenationData("te", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Telugu - new HyphenationData("tk", 2, 2), // Turkmen - new HyphenationData("und-Ethi", 1, 1), // Any language in Ethiopic script - }; - - /** - * Load hyphenation patterns at initialization time. We want to have patterns - * for all locales loaded and ready to use so we don't have to do any file IO - * on the UI thread when drawing text in different locales. - * - * @hide - */ public static void init() { - sMap.put(null, null); - - for (int i = 0; i < AVAILABLE_LANGUAGES.length; i++) { - HyphenationData data = AVAILABLE_LANGUAGES[i]; - Hyphenator h = loadHyphenator(data); - if (h != null) { - sMap.put(Locale.forLanguageTag(data.mLanguageTag), h); - } - } - - for (int i = 0; i < LOCALE_FALLBACK_DATA.length; i++) { - String language = LOCALE_FALLBACK_DATA[i][0]; - String fallback = LOCALE_FALLBACK_DATA[i][1]; - sMap.put(Locale.forLanguageTag(language), sMap.get(Locale.forLanguageTag(fallback))); - } + nInit(); } + private static native void nInit(); } diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index a233ba118e7d..2a693a1841e6 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -17,6 +17,7 @@ package android.text; import android.annotation.IntDef; +import android.annotation.IntRange; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; @@ -118,6 +119,16 @@ public abstract class Layout { */ public static final int JUSTIFICATION_MODE_INTER_WORD = 1; + /* + * Line spacing multiplier for default line spacing. + */ + public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f; + + /* + * Line spacing addition for default line spacing. + */ + public static final float DEFAULT_LINESPACING_ADDITION = 0.0f; + /** * Return how wide a layout must be in order to display the specified text with one line per * paragraph. @@ -151,6 +162,17 @@ public abstract class Layout { */ public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir) { + return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE); + } + /** + * Return how wide a layout must be in order to display the + * specified text slice with one line per paragraph. + * + * If the measured width exceeds given limit, returns limit value instead. + * @hide + */ + public static float getDesiredWidthWithLimit(CharSequence source, int start, int end, + TextPaint paint, TextDirectionHeuristic textDir, float upperLimit) { float need = 0; int next; @@ -162,6 +184,9 @@ public abstract class Layout { // note, omits trailing paragraph char float w = measurePara(paint, source, i, next, textDir); + if (w > upperLimit) { + return upperLimit; + } if (w > need) need = w; @@ -294,8 +319,6 @@ public abstract class Layout { private float getJustifyWidth(int lineNum) { Alignment paraAlign = mAlignment; - TabStops tabStops = null; - boolean tabStopsIsInitialized = false; int left = 0; int right = mWidth; @@ -346,10 +369,6 @@ public abstract class Layout { } } - if (getLineContainsTab(lineNum)) { - tabStops = new TabStops(TAB_INCREMENT, spans); - } - final Alignment align; if (paraAlign == Alignment.ALIGN_LEFT) { align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; @@ -387,7 +406,8 @@ public abstract class Layout { int previousLineEnd = getLineStart(firstLine); ParagraphStyle[] spans = NO_PARA_SPANS; int spanEnd = 0; - final TextPaint paint = mPaint; + final TextPaint paint = mWorkPaint; + paint.set(mPaint); CharSequence buf = mText; Alignment paraAlign = mAlignment; @@ -403,6 +423,7 @@ public abstract class Layout { previousLineEnd = getLineStart(lineNum + 1); final boolean justify = isJustificationRequired(lineNum); int end = getLineVisibleEnd(lineNum, start, previousLineEnd); + paint.setHyphenEdit(getHyphen(lineNum)); int ltop = previousLineBottom; int lbottom = getLineTop(lineNum + 1); @@ -526,7 +547,6 @@ public abstract class Layout { } } - paint.setHyphenEdit(getHyphen(lineNum)); Directions directions = getLineDirections(lineNum); if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) { // XXX: assumes there's nothing additional to be done @@ -538,7 +558,6 @@ public abstract class Layout { } tl.draw(canvas, x, ltop, lbaseline, lbottom); } - paint.setHyphenEdit(0); } TextLine.recycle(tl); @@ -1193,10 +1212,10 @@ public abstract class Layout { * @return the extent of the line */ private float getLineExtent(int line, boolean full) { - int start = getLineStart(line); - int end = full ? getLineEnd(line) : getLineVisibleEnd(line); + final int start = getLineStart(line); + final int end = full ? getLineEnd(line) : getLineVisibleEnd(line); - boolean hasTabs = getLineContainsTab(line); + final boolean hasTabs = getLineContainsTab(line); TabStops tabStops = null; if (hasTabs && mText instanceof Spanned) { // Just checking this line should be good enough, tabs should be @@ -1206,21 +1225,22 @@ public abstract class Layout { tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse } } - Directions directions = getLineDirections(line); + final Directions directions = getLineDirections(line); // Returned directions can actually be null if (directions == null) { return 0f; } - int dir = getParagraphDirection(line); + final int dir = getParagraphDirection(line); - TextLine tl = TextLine.obtain(); - mPaint.setHyphenEdit(getHyphen(line)); - tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops); + final TextLine tl = TextLine.obtain(); + final TextPaint paint = mWorkPaint; + paint.set(mPaint); + paint.setHyphenEdit(getHyphen(line)); + tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops); if (isJustificationRequired(line)) { tl.justify(getJustifyWidth(line)); } - float width = tl.metrics(null); - mPaint.setHyphenEdit(0); + final float width = tl.metrics(null); TextLine.recycle(tl); return width; } @@ -1234,20 +1254,21 @@ public abstract class Layout { * @return the extent of the text on this line */ private float getLineExtent(int line, TabStops tabStops, boolean full) { - int start = getLineStart(line); - int end = full ? getLineEnd(line) : getLineVisibleEnd(line); - boolean hasTabs = getLineContainsTab(line); - Directions directions = getLineDirections(line); - int dir = getParagraphDirection(line); - - TextLine tl = TextLine.obtain(); - mPaint.setHyphenEdit(getHyphen(line)); - tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops); + final int start = getLineStart(line); + final int end = full ? getLineEnd(line) : getLineVisibleEnd(line); + final boolean hasTabs = getLineContainsTab(line); + final Directions directions = getLineDirections(line); + final int dir = getParagraphDirection(line); + + final TextLine tl = TextLine.obtain(); + final TextPaint paint = mWorkPaint; + paint.set(mPaint); + paint.setHyphenEdit(getHyphen(line)); + tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops); if (isJustificationRequired(line)) { tl.justify(getJustifyWidth(line)); } - float width = tl.metrics(null); - mPaint.setHyphenEdit(0); + final float width = tl.metrics(null); TextLine.recycle(tl); return width; } @@ -1396,7 +1417,6 @@ public abstract class Layout { float dist = Math.abs(getHorizontal(max, primary) - horiz); if (dist <= bestdist) { - bestdist = dist; best = max; } @@ -1450,6 +1470,16 @@ public abstract class Layout { } /** + * Return the vertical position of the bottom of the specified line without the line spacing + * added. + * + * @hide + */ + public final int getLineBottomWithoutSpacing(int line) { + return getLineTop(line + 1) - getLineExtra(line); + } + + /** * Return the vertical position of the baseline of the specified line. */ public final int getLineBaseline(int line) { @@ -1466,6 +1496,17 @@ public abstract class Layout { return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); } + /** + * Return the extra space added as a result of line spacing attributes + * {@link #getSpacingAdd()} and {@link #getSpacingMultiplier()}. Default value is {@code zero}. + * + * @param line the index of the line, the value should be equal or greater than {@code zero} + * @hide + */ + public int getLineExtra(@IntRange(from = 0) int line) { + return 0; + } + public int getOffsetToLeftOf(int offset) { return getOffsetToLeftRightOf(offset, true); } @@ -1522,7 +1563,7 @@ public abstract class Layout { // XXX: we don't care about tabs tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); - tl = TextLine.recycle(tl); + TextLine.recycle(tl); return caret; } @@ -1581,13 +1622,12 @@ public abstract class Layout { * but can be multiple discontinuous lines in text with multiple * directionalities. */ - public void getCursorPath(int point, Path dest, - CharSequence editingBuffer) { + public void getCursorPath(final int point, final Path dest, final CharSequence editingBuffer) { dest.reset(); int line = getLineForOffset(point); int top = getLineTop(line); - int bottom = getLineTop(line+1); + int bottom = getLineBottomWithoutSpacing(line); boolean clamped = shouldClampCursor(line); float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; @@ -1657,20 +1697,22 @@ public abstract class Layout { } private void addSelection(int line, int start, int end, - int top, int bottom, Path dest) { + int top, int bottom, SelectionRectangleConsumer consumer) { int linestart = getLineStart(line); int lineend = getLineEnd(line); Directions dirs = getLineDirections(line); - if (lineend > linestart && mText.charAt(lineend - 1) == '\n') + if (lineend > linestart && mText.charAt(lineend - 1) == '\n') { lineend--; + } for (int i = 0; i < dirs.mDirections.length; i += 2) { int here = linestart + dirs.mDirections[i]; - int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); + int there = here + (dirs.mDirections[i + 1] & RUN_LENGTH_MASK); - if (there > lineend) + if (there > lineend) { there = lineend; + } if (start <= there && end >= here) { int st = Math.max(start, here); @@ -1683,7 +1725,12 @@ public abstract class Layout { float left = Math.min(h1, h2); float right = Math.max(h1, h2); - dest.addRect(left, top, right, bottom, Path.Direction.CW); + final @TextSelectionLayout int layout = + ((dirs.mDirections[i + 1] & RUN_RTL_FLAG) != 0) + ? TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT + : TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT; + + consumer.accept(left, top, right, bottom, layout); } } } @@ -1697,9 +1744,25 @@ public abstract class Layout { */ public void getSelectionPath(int start, int end, Path dest) { dest.reset(); + getSelection(start, end, (left, top, right, bottom, textSelectionLayout) -> + dest.addRect(left, top, right, bottom, Path.Direction.CW)); + } - if (start == end) + /** + * Calculates the rectangles which should be highlighted to indicate a selection between start + * and end and feeds them into the given {@link SelectionRectangleConsumer}. + * + * @param start the starting index of the selection + * @param end the ending index of the selection + * @param consumer the {@link SelectionRectangleConsumer} which will receive the generated + * rectangles. It will be called every time a rectangle is generated. + * @hide + * @see #getSelectionPath(int, int, Path) + */ + public final void getSelection(int start, int end, final SelectionRectangleConsumer consumer) { + if (start == end) { return; + } if (end < start) { int temp = end; @@ -1707,43 +1770,50 @@ public abstract class Layout { start = temp; } - int startline = getLineForOffset(start); - int endline = getLineForOffset(end); + final int startline = getLineForOffset(start); + final int endline = getLineForOffset(end); int top = getLineTop(startline); - int bottom = getLineBottom(endline); + int bottom = getLineBottomWithoutSpacing(endline); if (startline == endline) { - addSelection(startline, start, end, top, bottom, dest); + addSelection(startline, start, end, top, bottom, consumer); } else { final float width = mWidth; addSelection(startline, start, getLineEnd(startline), - top, getLineBottom(startline), dest); + top, getLineBottom(startline), consumer); - if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) - dest.addRect(getLineLeft(startline), top, - 0, getLineBottom(startline), Path.Direction.CW); - else - dest.addRect(getLineRight(startline), top, - width, getLineBottom(startline), Path.Direction.CW); + if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) { + consumer.accept(getLineLeft(startline), top, 0, getLineBottom(startline), + TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); + } else { + consumer.accept(getLineRight(startline), top, width, getLineBottom(startline), + TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); + } for (int i = startline + 1; i < endline; i++) { top = getLineTop(i); bottom = getLineBottom(i); - dest.addRect(0, top, width, bottom, Path.Direction.CW); + if (getParagraphDirection(i) == DIR_RIGHT_TO_LEFT) { + consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); + } else { + consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); + } } top = getLineTop(endline); - bottom = getLineBottom(endline); + bottom = getLineBottomWithoutSpacing(endline); - addSelection(endline, getLineStart(endline), end, - top, bottom, dest); + addSelection(endline, getLineStart(endline), end, top, bottom, consumer); - if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) - dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); - else - dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); + if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) { + consumer.accept(width, top, getLineRight(endline), bottom, + TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); + } else { + consumer.accept(0, top, getLineLeft(endline), bottom, + TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); + } } } @@ -1817,10 +1887,7 @@ public abstract class Layout { int margin = 0; - boolean isFirstParaLine = lineStart == 0 || - spanned.charAt(lineStart - 1) == '\n'; - - boolean useFirstLineMargin = isFirstParaLine; + boolean useFirstLineMargin = lineStart == 0 || spanned.charAt(lineStart - 1) == '\n'; for (int i = 0; i < spans.length; i++) { if (spans[i] instanceof LeadingMarginSpan2) { int spStart = spanned.getSpanStart(spans[i]); @@ -1838,25 +1905,16 @@ public abstract class Layout { return margin; } - /* package */ - static float measurePara(TextPaint paint, CharSequence text, int start, int end, + private static float measurePara(TextPaint paint, CharSequence text, int start, int end, TextDirectionHeuristic textDir) { - MeasuredText mt = MeasuredText.obtain(); + MeasuredText mt = null; TextLine tl = TextLine.obtain(); try { - mt.setPara(text, start, end, textDir, null); - Directions directions; - int dir; - if (mt.mEasy) { - directions = DIRS_ALL_LEFT_TO_RIGHT; - dir = Layout.DIR_LEFT_TO_RIGHT; - } else { - directions = AndroidBidi.directions(mt.mDir, mt.mLevels, - 0, mt.mChars, 0, mt.mLen); - dir = mt.mDir; - } - char[] chars = mt.mChars; - int len = mt.mLen; + mt = MeasuredText.buildForBidi(text, start, end, textDir, mt); + final char[] chars = mt.getChars(); + final int len = chars.length; + final Directions directions = mt.getDirections(0, len); + final int dir = mt.getParagraphDir(); boolean hasTabs = false; TabStops tabStops = null; // leading margins should be taken into account when measuring a paragraph @@ -1889,7 +1947,9 @@ public abstract class Layout { return margin + Math.abs(tl.metrics(null)); } finally { TextLine.recycle(tl); - MeasuredText.recycle(mt); + if (mt != null) { + mt.recycle(); + } } } @@ -2036,35 +2096,29 @@ public abstract class Layout { } } - private char getEllipsisChar(TextUtils.TruncateAt method) { - return (method == TextUtils.TruncateAt.END_SMALL) ? - TextUtils.ELLIPSIS_TWO_DOTS[0] : - TextUtils.ELLIPSIS_NORMAL[0]; - } - private void ellipsize(int start, int end, int line, char[] dest, int destoff, TextUtils.TruncateAt method) { - int ellipsisCount = getEllipsisCount(line); - + final int ellipsisCount = getEllipsisCount(line); if (ellipsisCount == 0) { return; } - - int ellipsisStart = getEllipsisStart(line); - int linestart = getLineStart(line); - - for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { - char c; - - if (i == ellipsisStart) { - c = getEllipsisChar(method); // ellipsis + final int ellipsisStart = getEllipsisStart(line); + final int lineStart = getLineStart(line); + + final String ellipsisString = TextUtils.getEllipsisString(method); + final int ellipsisStringLen = ellipsisString.length(); + // Use the ellipsis string only if there are that at least as many characters to replace. + final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen; + for (int i = 0; i < ellipsisCount; i++) { + final char c; + if (useEllipsisString && i < ellipsisStringLen) { + c = ellipsisString.charAt(i); } else { - c = '\uFEFF'; // 0-width space + c = TextUtils.ELLIPSIS_FILLER; } - int a = i + linestart; - - if (a >= start && a < end) { + final int a = i + ellipsisStart + lineStart; + if (start <= a && a < end) { dest[destoff + a - start] = c; } } @@ -2075,18 +2129,14 @@ public abstract class Layout { * text within the layout of a line. */ public static class Directions { - // Directions represents directional runs within a line of text. - // Runs are pairs of ints listed in visual order, starting from the - // leading margin. The first int of each pair is the offset from - // the first character of the line to the start of the run. The - // second int represents both the length and level of the run. - // The length is in the lower bits, accessed by masking with - // DIR_LENGTH_MASK. The level is in the higher bits, accessed - // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. - // To simply test for an RTL direction, test the bit using - // DIR_RTL_FLAG, if set then the direction is rtl. - /** + * Directions represents directional runs within a line of text. Runs are pairs of ints + * listed in visual order, starting from the leading margin. The first int of each pair is + * the offset from the first character of the line to the start of the run. The second int + * represents both the length and level of the run. The length is in the lower bits, + * accessed by masking with RUN_LENGTH_MASK. The level is in the higher bits, accessed by + * shifting by RUN_LEVEL_SHIFT and masking by RUN_LEVEL_MASK. To simply test for an RTL + * direction, test the bit using RUN_RTL_FLAG, if set then the direction is rtl. * @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @@ -2205,6 +2255,7 @@ public abstract class Layout { private CharSequence mText; private TextPaint mPaint; + private TextPaint mWorkPaint = new TextPaint(); private int mWidth; private Alignment mAlignment = Alignment.ALIGN_NORMAL; private float mSpacingMult; @@ -2215,6 +2266,11 @@ public abstract class Layout { private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; private int mJustificationMode; + /** @hide */ + @IntDef({DIR_LEFT_TO_RIGHT, DIR_RIGHT_TO_LEFT}) + @Retention(RetentionPolicy.SOURCE) + public @interface Direction {} + public static final int DIR_LEFT_TO_RIGHT = 1; public static final int DIR_RIGHT_TO_LEFT = -1; @@ -2250,4 +2306,31 @@ public abstract class Layout { public static final Directions DIRS_ALL_RIGHT_TO_LEFT = new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT}) + public @interface TextSelectionLayout {} + + /** @hide */ + public static final int TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT = 0; + /** @hide */ + public static final int TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT = 1; + + /** @hide */ + @FunctionalInterface + public interface SelectionRectangleConsumer { + /** + * Performs this operation on the given rectangle. + * + * @param left the left edge of the rectangle + * @param top the top edge of the rectangle + * @param right the right edge of the rectangle + * @param bottom the bottom edge of the rectangle + * @param textSelectionLayout the layout (RTL or LTR) of the text covered by this + * selection rectangle + */ + void accept(float left, float top, float right, float bottom, + @TextSelectionLayout int textSelectionLayout); + } + } diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index ce3e2822fa22..14d6f9e8a9ef 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -16,133 +16,436 @@ package android.text; +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Paint; +import android.text.AutoGrowArray.ByteArray; +import android.text.AutoGrowArray.FloatArray; +import android.text.AutoGrowArray.IntArray; +import android.text.Layout.Directions; import android.text.style.MetricAffectingSpan; import android.text.style.ReplacementSpan; -import android.util.Log; +import android.util.Pools.SynchronizedPool; -import com.android.internal.util.ArrayUtils; +import dalvik.annotation.optimization.CriticalNative; + +import libcore.util.NativeAllocationRegistry; + +import java.util.Arrays; /** + * MeasuredText provides text information for rendering purpose. + * + * The first motivation of this class is identify the text directions and retrieving individual + * character widths. However retrieving character widths is slower than identifying text directions. + * Thus, this class provides several builder methods for specific purposes. + * + * - buildForBidi: + * Compute only text directions. + * - buildForMeasurement: + * Compute text direction and all character widths. + * - buildForStaticLayout: + * This is bit special. StaticLayout also needs to know text direction and character widths for + * line breaking, but all things are done in native code. Similarly, text measurement is done + * in native code. So instead of storing result to Java array, this keeps the result in native + * code since there is no good reason to move the results to Java layer. + * + * In addition to the character widths, some additional information is computed for each purposes, + * e.g. whole text length for measurement or font metrics for static layout. + * + * MeasuredText is NOT a thread safe object. * @hide */ -class MeasuredText { - private static final boolean localLOGV = false; - CharSequence mText; - int mTextStart; - float[] mWidths; - char[] mChars; - byte[] mLevels; - int mDir; - boolean mEasy; - int mLen; - - private int mPos; - private TextPaint mWorkPaint; - private StaticLayout.Builder mBuilder; - - private MeasuredText() { - mWorkPaint = new TextPaint(); +public class MeasuredText { + private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC'; + + private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry( + MeasuredText.class.getClassLoader(), nGetReleaseFunc(), 1024); + + private MeasuredText() {} // Use build static functions instead. + + private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1); + + private static @NonNull MeasuredText obtain() { // Use build static functions instead. + final MeasuredText mt = sPool.acquire(); + return mt != null ? mt : new MeasuredText(); } - private static final Object[] sLock = new Object[0]; - private static final MeasuredText[] sCached = new MeasuredText[3]; - - static MeasuredText obtain() { - MeasuredText mt; - synchronized (sLock) { - for (int i = sCached.length; --i >= 0;) { - if (sCached[i] != null) { - mt = sCached[i]; - sCached[i] = null; - return mt; - } - } + /** + * Recycle the MeasuredText. + * + * Do not call any methods after you call this method. + */ + public void recycle() { + release(); + sPool.release(this); + } + + // The casted original text. + // + // This may be null if the passed text is not a Spanned. + private @Nullable Spanned mSpanned; + + // The start offset of the target range in the original text (mSpanned); + private @IntRange(from = 0) int mTextStart; + + // The length of the target range in the original text. + private @IntRange(from = 0) int mTextLength; + + // The copied character buffer for measuring text. + // + // The length of this array is mTextLength. + private @Nullable char[] mCopiedBuffer; + + // The whole paragraph direction. + private @Layout.Direction int mParaDir; + + // True if the text is LTR direction and doesn't contain any bidi characters. + private boolean mLtrWithoutBidi; + + // The bidi level for individual characters. + // + // This is empty if mLtrWithoutBidi is true. + private @NonNull ByteArray mLevels = new ByteArray(); + + // The whole width of the text. + // See getWholeWidth comments. + private @FloatRange(from = 0.0f) float mWholeWidth; + + // Individual characters' widths. + // See getWidths comments. + private @Nullable FloatArray mWidths = new FloatArray(); + + // The span end positions. + // See getSpanEndCache comments. + private @Nullable IntArray mSpanEndCache = new IntArray(4); + + // The font metrics. + // See getFontMetrics comments. + private @Nullable IntArray mFontMetrics = new IntArray(4 * 4); + + // The native MeasuredText. + // See getNativePtr comments. + // Do not modify these members directly. Use bindNativeObject/unbindNativeObject instead. + private /* Maybe Zero */ long mNativePtr = 0; + private @Nullable Runnable mNativeObjectCleaner; + + // Associate the native object to this Java object. + private void bindNativeObject(/* Non Zero*/ long nativePtr) { + mNativePtr = nativePtr; + mNativeObjectCleaner = sRegistry.registerNativeAllocation(this, nativePtr); + } + + // Decouple the native object from this Java object and release the native object. + private void unbindNativeObject() { + if (mNativePtr != 0) { + mNativeObjectCleaner.run(); + mNativePtr = 0; } - mt = new MeasuredText(); - if (localLOGV) { - Log.v("MEAS", "new: " + mt); + } + + // Following two objects are for avoiding object allocation. + private @NonNull TextPaint mCachedPaint = new TextPaint(); + private @Nullable Paint.FontMetricsInt mCachedFm; + + /** + * Releases internal buffers. + */ + public void release() { + reset(); + mLevels.clearWithReleasingLargeArray(); + mWidths.clearWithReleasingLargeArray(); + mFontMetrics.clearWithReleasingLargeArray(); + mSpanEndCache.clearWithReleasingLargeArray(); + } + + /** + * Resets the internal state for starting new text. + */ + private void reset() { + mSpanned = null; + mCopiedBuffer = null; + mWholeWidth = 0; + mLevels.clear(); + mWidths.clear(); + mFontMetrics.clear(); + mSpanEndCache.clear(); + unbindNativeObject(); + } + + /** + * Returns the characters to be measured. + * + * This is always available. + */ + public @NonNull char[] getChars() { + return mCopiedBuffer; + } + + /** + * Returns the paragraph direction. + * + * This is always available. + */ + public @Layout.Direction int getParagraphDir() { + return mParaDir; + } + + /** + * Returns the directions. + * + * This is always available. + */ + public Directions getDirections(@IntRange(from = 0) int start, // inclusive + @IntRange(from = 0) int end) { // exclusive + if (mLtrWithoutBidi) { + return Layout.DIRS_ALL_LEFT_TO_RIGHT; } + + final int length = end - start; + return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start, + length); + } + + /** + * Returns the whole text width. + * + * This is available only if the MeasureText is computed with computeForMeasurement. + * Returns 0 in other cases. + */ + public @FloatRange(from = 0.0f) float getWholeWidth() { + return mWholeWidth; + } + + /** + * Returns the individual character's width. + * + * This is available only if the MeasureText is computed with computeForMeasurement. + * Returns empty array in other cases. + */ + public @NonNull FloatArray getWidths() { + return mWidths; + } + + /** + * Returns the MetricsAffectingSpan end indices. + * + * If the input text is not a spanned string, this has one value that is the length of the text. + * + * This is available only if the MeasureText is computed with computeForStaticLayout. + * Returns empty array in other cases. + */ + public @NonNull IntArray getSpanEndCache() { + return mSpanEndCache; + } + + /** + * Returns the int array which holds FontMetrics. + * + * This array holds the repeat of top, bottom, ascent, descent of font metrics value. + * + * This is available only if the MeasureText is computed with computeForStaticLayout. + * Returns empty array in other cases. + */ + public @NonNull IntArray getFontMetrics() { + return mFontMetrics; + } + + /** + * Returns the native ptr of the MeasuredText. + * + * This is available only if the MeasureText is computed with computeForStaticLayout. + * Returns 0 in other cases. + */ + public /* Maybe Zero */ long getNativePtr() { + return mNativePtr; + } + + /** + * Generates new MeasuredText for Bidi computation. + * + * If recycle is null, this returns new instance. If recycle is not null, this fills computed + * result to recycle and returns recycle. + * + * @param text the character sequence to be measured + * @param start the inclusive start offset of the target region in the text + * @param end the exclusive end offset of the target region in the text + * @param textDir the text direction + * @param recycle pass existing MeasuredText if you want to recycle it. + * + * @return measured text + */ + public static @NonNull MeasuredText buildForBidi(@NonNull CharSequence text, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @NonNull TextDirectionHeuristic textDir, + @Nullable MeasuredText recycle) { + final MeasuredText mt = recycle == null ? obtain() : recycle; + mt.resetAndAnalyzeBidi(text, start, end, textDir); return mt; } - static MeasuredText recycle(MeasuredText mt) { - mt.finish(); - synchronized(sLock) { - for (int i = 0; i < sCached.length; ++i) { - if (sCached[i] == null) { - sCached[i] = mt; - mt.mText = null; - break; - } + /** + * Generates new MeasuredText for measuring texts. + * + * If recycle is null, this returns new instance. If recycle is not null, this fills computed + * result to recycle and returns recycle. + * + * @param paint the paint to be used for rendering the text. + * @param text the character sequence to be measured + * @param start the inclusive start offset of the target region in the text + * @param end the exclusive end offset of the target region in the text + * @param textDir the text direction + * @param recycle pass existing MeasuredText if you want to recycle it. + * + * @return measured text + */ + public static @NonNull MeasuredText buildForMeasurement(@NonNull TextPaint paint, + @NonNull CharSequence text, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @NonNull TextDirectionHeuristic textDir, + @Nullable MeasuredText recycle) { + final MeasuredText mt = recycle == null ? obtain() : recycle; + mt.resetAndAnalyzeBidi(text, start, end, textDir); + + mt.mWidths.resize(mt.mTextLength); + if (mt.mTextLength == 0) { + return mt; + } + + if (mt.mSpanned == null) { + // No style change by MetricsAffectingSpan. Just measure all text. + mt.applyMetricsAffectingSpan( + paint, null /* spans */, start, end, 0 /* native static layout ptr */); + } else { + // There may be a MetricsAffectingSpan. Split into span transitions and apply styles. + int spanEnd; + for (int spanStart = start; spanStart < end; spanStart = spanEnd) { + spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class); + MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd, + MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class); + mt.applyMetricsAffectingSpan( + paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */); } } - return null; + return mt; } - void finish() { - mText = null; - mBuilder = null; - if (mLen > 1000) { - mWidths = null; - mChars = null; - mLevels = null; + /** + * Generates new MeasuredText for StaticLayout. + * + * If recycle is null, this returns new instance. If recycle is not null, this fills computed + * result to recycle and returns recycle. + * + * @param paint the paint to be used for rendering the text. + * @param text the character sequence to be measured + * @param start the inclusive start offset of the target region in the text + * @param end the exclusive end offset of the target region in the text + * @param textDir the text direction + * @param recycle pass existing MeasuredText if you want to recycle it. + * + * @return measured text + */ + public static @NonNull MeasuredText buildForStaticLayout( + @NonNull TextPaint paint, + @NonNull CharSequence text, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @NonNull TextDirectionHeuristic textDir, + @Nullable MeasuredText recycle) { + final MeasuredText mt = recycle == null ? obtain() : recycle; + mt.resetAndAnalyzeBidi(text, start, end, textDir); + if (mt.mTextLength == 0) { + // Need to build empty native measured text for StaticLayout. + // TODO: Stop creating empty measured text for empty lines. + long nativeBuilderPtr = nInitBuilder(); + try { + mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer)); + } finally { + nFreeBuilder(nativeBuilderPtr); + } + return mt; + } + + long nativeBuilderPtr = nInitBuilder(); + try { + if (mt.mSpanned == null) { + // No style change by MetricsAffectingSpan. Just measure all text. + mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr); + mt.mSpanEndCache.append(end); + } else { + // There may be a MetricsAffectingSpan. Split into span transitions and apply + // styles. + int spanEnd; + for (int spanStart = start; spanStart < end; spanStart = spanEnd) { + spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, + MetricAffectingSpan.class); + MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd, + MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, + MetricAffectingSpan.class); + mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd, + nativeBuilderPtr); + mt.mSpanEndCache.append(spanEnd); + } + } + mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer)); + } finally { + nFreeBuilder(nativeBuilderPtr); } - } - void setPos(int pos) { - mPos = pos - mTextStart; + return mt; } /** - * Analyzes text for bidirectional runs. Allocates working buffers. + * Reset internal state and analyzes text for bidirectional runs. + * + * @param text the character sequence to be measured + * @param start the inclusive start offset of the target region in the text + * @param end the exclusive end offset of the target region in the text + * @param textDir the text direction */ - void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir, - StaticLayout.Builder builder) { - mBuilder = builder; - mText = text; + private void resetAndAnalyzeBidi(@NonNull CharSequence text, + @IntRange(from = 0) int start, // inclusive + @IntRange(from = 0) int end, // exclusive + @NonNull TextDirectionHeuristic textDir) { + reset(); + mSpanned = text instanceof Spanned ? (Spanned) text : null; mTextStart = start; + mTextLength = end - start; - int len = end - start; - mLen = len; - mPos = 0; - - if (mWidths == null || mWidths.length < len) { - mWidths = ArrayUtils.newUnpaddedFloatArray(len); + if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) { + mCopiedBuffer = new char[mTextLength]; } - if (mChars == null || mChars.length < len) { - mChars = ArrayUtils.newUnpaddedCharArray(len); - } - TextUtils.getChars(text, start, end, mChars, 0); + TextUtils.getChars(text, start, end, mCopiedBuffer, 0); - if (text instanceof Spanned) { - Spanned spanned = (Spanned) text; - ReplacementSpan[] spans = spanned.getSpans(start, end, - ReplacementSpan.class); + // Replace characters associated with ReplacementSpan to U+FFFC. + if (mSpanned != null) { + ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class); for (int i = 0; i < spans.length; i++) { - int startInPara = spanned.getSpanStart(spans[i]) - start; - int endInPara = spanned.getSpanEnd(spans[i]) - start; - // The span interval may be larger and must be restricted to [start, end[ + int startInPara = mSpanned.getSpanStart(spans[i]) - start; + int endInPara = mSpanned.getSpanEnd(spans[i]) - start; + // The span interval may be larger and must be restricted to [start, end) if (startInPara < 0) startInPara = 0; - if (endInPara > len) endInPara = len; - for (int j = startInPara; j < endInPara; j++) { - mChars[j] = '\uFFFC'; // object replacement character - } + if (endInPara > mTextLength) endInPara = mTextLength; + Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER); } } if ((textDir == TextDirectionHeuristics.LTR || textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR || textDir == TextDirectionHeuristics.ANYRTL_LTR) && - TextUtils.doesNotNeedBidi(mChars, 0, len)) { - mDir = Layout.DIR_LEFT_TO_RIGHT; - mEasy = true; + TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) { + mLevels.clear(); + mParaDir = Layout.DIR_LEFT_TO_RIGHT; + mLtrWithoutBidi = true; } else { - if (mLevels == null || mLevels.length < len) { - mLevels = ArrayUtils.newUnpaddedByteArray(len); - } - int bidiRequest; + final int bidiRequest; if (textDir == TextDirectionHeuristics.LTR) { bidiRequest = Layout.DIR_REQUEST_LTR; } else if (textDir == TextDirectionHeuristics.RTL) { @@ -152,117 +455,147 @@ class MeasuredText { } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) { bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL; } else { - boolean isRtl = textDir.isRtl(mChars, 0, len); + final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength); bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR; } - mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false); - mEasy = false; + mLevels.resize(mTextLength); + mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray()); + mLtrWithoutBidi = false; } } - float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) { - if (fm != null) { - paint.getFontMetricsInt(fm); + private void applyReplacementRun(@NonNull ReplacementSpan replacement, + @IntRange(from = 0) int start, // inclusive, in copied buffer + @IntRange(from = 0) int end, // exclusive, in copied buffer + /* Maybe Zero */ long nativeBuilderPtr) { + // Use original text. Shouldn't matter. + // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for + // backward compatibility? or Should we initialize them for getFontMetricsInt? + final float width = replacement.getSize( + mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm); + if (nativeBuilderPtr == 0) { + // Assigns all width to the first character. This is the same behavior as minikin. + mWidths.set(start, width); + if (end > start + 1) { + Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f); + } + mWholeWidth += width; + } else { + nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end, + width); } + } - int p = mPos; - mPos = p + len; - - // try to do widths measurement in native code, but use Java if paint has been subclassed - // FIXME: may want to eliminate special case for subclass - float[] widths = null; - if (mBuilder == null || paint.getClass() != TextPaint.class) { - widths = mWidths; + private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer + @IntRange(from = 0) int end, // exclusive, in copied buffer + /* Maybe Zero */ long nativeBuilderPtr) { + if (nativeBuilderPtr != 0) { + mCachedPaint.getFontMetricsInt(mCachedFm); } - if (mEasy) { - boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT; - float width = 0; - if (widths != null) { - width = paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, widths, p); - if (mBuilder != null) { - mBuilder.addMeasuredRun(p, p + len, widths); - } + + if (mLtrWithoutBidi) { + // If the whole text is LTR direction, just apply whole region. + if (nativeBuilderPtr == 0) { + mWholeWidth += mCachedPaint.getTextRunAdvances( + mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */, + mWidths.getRawArray(), start); } else { - width = mBuilder.addStyleRun(paint, p, p + len, isRtl); + nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end, + false /* isRtl */); } - return width; - } - - float totalAdvance = 0; - int level = mLevels[p]; - for (int q = p, i = p + 1, e = p + len;; ++i) { - if (i == e || mLevels[i] != level) { - boolean isRtl = (level & 0x1) != 0; - if (widths != null) { - totalAdvance += - paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, widths, q); - if (mBuilder != null) { - mBuilder.addMeasuredRun(q, i, widths); + } else { + // If there is multiple bidi levels, split into individual bidi level and apply style. + byte level = mLevels.get(start); + // Note that the empty text or empty range won't reach this method. + // Safe to search from start + 1. + for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) { + if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point + final boolean isRtl = (level & 0x1) != 0; + if (nativeBuilderPtr == 0) { + final int levelLength = levelEnd - levelStart; + mWholeWidth += mCachedPaint.getTextRunAdvances( + mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, + isRtl, mWidths.getRawArray(), levelStart); + } else { + nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart, + levelEnd, isRtl); } - } else { - totalAdvance += mBuilder.addStyleRun(paint, q, i, isRtl); - } - if (i == e) { - break; + if (levelEnd == end) { + break; + } + levelStart = levelEnd; + level = mLevels.get(levelEnd); } - q = i; - level = mLevels[i]; } } - return totalAdvance; } - float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, - Paint.FontMetricsInt fm) { - - TextPaint workPaint = mWorkPaint; - workPaint.set(paint); + private void applyMetricsAffectingSpan( + @NonNull TextPaint paint, + @Nullable MetricAffectingSpan[] spans, + @IntRange(from = 0) int start, // inclusive, in original text buffer + @IntRange(from = 0) int end, // exclusive, in original text buffer + /* Maybe Zero */ long nativeBuilderPtr) { + mCachedPaint.set(paint); // XXX paint should not have a baseline shift, but... - workPaint.baselineShift = 0; + mCachedPaint.baselineShift = 0; + + final boolean needFontMetrics = nativeBuilderPtr != 0; + + if (needFontMetrics && mCachedFm == null) { + mCachedFm = new Paint.FontMetricsInt(); + } ReplacementSpan replacement = null; - for (int i = 0; i < spans.length; i++) { - MetricAffectingSpan span = spans[i]; - if (span instanceof ReplacementSpan) { - replacement = (ReplacementSpan)span; - } else { - span.updateMeasureState(workPaint); + if (spans != null) { + for (int i = 0; i < spans.length; i++) { + MetricAffectingSpan span = spans[i]; + if (span instanceof ReplacementSpan) { + // The last ReplacementSpan is effective for backward compatibility reasons. + replacement = (ReplacementSpan) span; + } else { + // TODO: No need to call updateMeasureState for ReplacementSpan as well? + span.updateMeasureState(mCachedPaint); + } } } - float wid; - if (replacement == null) { - wid = addStyleRun(workPaint, len, fm); + final int startInCopiedBuffer = start - mTextStart; + final int endInCopiedBuffer = end - mTextStart; + + if (replacement != null) { + applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, + nativeBuilderPtr); } else { - // Use original text. Shouldn't matter. - wid = replacement.getSize(workPaint, mText, mTextStart + mPos, - mTextStart + mPos + len, fm); - if (mBuilder == null) { - float[] w = mWidths; - w[mPos] = wid; - for (int i = mPos + 1, e = mPos + len; i < e; i++) - w[i] = 0; - } else { - mBuilder.addReplacementRun(mPos, mPos + len, wid); - } - mPos += len; + applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr); } - if (fm != null) { - if (workPaint.baselineShift < 0) { - fm.ascent += workPaint.baselineShift; - fm.top += workPaint.baselineShift; + if (needFontMetrics) { + if (mCachedPaint.baselineShift < 0) { + mCachedFm.ascent += mCachedPaint.baselineShift; + mCachedFm.top += mCachedPaint.baselineShift; } else { - fm.descent += workPaint.baselineShift; - fm.bottom += workPaint.baselineShift; + mCachedFm.descent += mCachedPaint.baselineShift; + mCachedFm.bottom += mCachedPaint.baselineShift; } - } - return wid; + mFontMetrics.append(mCachedFm.top); + mFontMetrics.append(mCachedFm.bottom); + mFontMetrics.append(mCachedFm.ascent); + mFontMetrics.append(mCachedFm.descent); + } } - int breakText(int limit, boolean forwards, float width) { - float[] w = mWidths; + /** + * Returns the maximum index that the accumulated width not exceeds the width. + * + * If forward=false is passed, returns the minimum index from the end instead. + * + * This only works if the MeasuredText is computed with computeForMeasurement. + * Undefined behavior in other case. + */ + @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) { + float[] w = mWidths.getRawArray(); if (forwards) { int i = 0; while (i < limit) { @@ -270,7 +603,7 @@ class MeasuredText { if (width < 0.0f) break; i++; } - while (i > 0 && mChars[i - 1] == ' ') i--; + while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--; return i; } else { int i = limit - 1; @@ -279,19 +612,65 @@ class MeasuredText { if (width < 0.0f) break; i--; } - while (i < limit - 1 && (mChars[i + 1] == ' ' || w[i + 1] == 0.0f)) { + while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) { i++; } return limit - i - 1; } } - float measure(int start, int limit) { + /** + * Returns the length of the substring. + * + * This only works if the MeasuredText is computed with computeForMeasurement. + * Undefined behavior in other case. + */ + @FloatRange(from = 0.0f) float measure(int start, int limit) { float width = 0; - float[] w = mWidths; + float[] w = mWidths.getRawArray(); for (int i = start; i < limit; ++i) { width += w[i]; } return width; } + + private static native /* Non Zero */ long nInitBuilder(); + + /** + * Apply style to make native measured text. + * + * @param nativeBuilderPtr The native MeasuredText builder pointer. + * @param paintPtr The native paint pointer to be applied. + * @param start The start offset in the copied buffer. + * @param end The end offset in the copied buffer. + * @param isRtl True if the text is RTL. + */ + private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr, + /* Non Zero */ long paintPtr, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + boolean isRtl); + + /** + * Apply ReplacementRun to make native measured text. + * + * @param nativeBuilderPtr The native MeasuredText builder pointer. + * @param paintPtr The native paint pointer to be applied. + * @param start The start offset in the copied buffer. + * @param end The end offset in the copied buffer. + * @param width The width of the replacement. + */ + private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr, + /* Non Zero */ long paintPtr, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @FloatRange(from = 0) float width); + + private static native long nBuildNativeMeasuredText(/* Non Zero */ long nativeBuilderPtr, + @NonNull char[] text); + + private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr); + + @CriticalNative + private static native /* Non Zero */ long nGetReleaseFunc(); } diff --git a/core/java/android/text/PremeasuredText.java b/core/java/android/text/PremeasuredText.java new file mode 100644 index 000000000000..465314dd21ac --- /dev/null +++ b/core/java/android/text/PremeasuredText.java @@ -0,0 +1,272 @@ +/* + * 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.text; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.util.IntArray; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; + +/** + * A text which has already been measured. + * + * TODO: Rename to better name? e.g. MeasuredText, FrozenText etc. + */ +public class PremeasuredText implements Spanned { + private static final char LINE_FEED = '\n'; + + // The original text. + private final @NonNull CharSequence mText; + + // The inclusive start offset of the measuring target. + private final @IntRange(from = 0) int mStart; + + // The exclusive end offset of the measuring target. + private final @IntRange(from = 0) int mEnd; + + // The TextPaint used for measurement. + private final @NonNull TextPaint mPaint; + + // The requested text direction. + private final @NonNull TextDirectionHeuristic mTextDir; + + // The measured paragraph texts. + private final @NonNull MeasuredText[] mMeasuredTexts; + + // The sorted paragraph end offsets. + private final @NonNull int[] mParagraphBreakPoints; + + /** + * Build PremeasuredText from the text. + * + * @param text The text to be measured. + * @param paint The paint to be used for drawing. + * @param textDir The text direction. + * @return The measured text. + */ + public static @NonNull PremeasuredText build(@NonNull CharSequence text, + @NonNull TextPaint paint, + @NonNull TextDirectionHeuristic textDir) { + return PremeasuredText.build(text, paint, textDir, 0, text.length()); + } + + /** + * Build PremeasuredText from the specific range of the text.. + * + * @param text The text to be measured. + * @param paint The paint to be used for drawing. + * @param textDir The text direction. + * @param start The inclusive start offset of the text. + * @param end The exclusive start offset of the text. + * @return The measured text. + */ + public static @NonNull PremeasuredText build(@NonNull CharSequence text, + @NonNull TextPaint paint, + @NonNull TextDirectionHeuristic textDir, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(paint); + Preconditions.checkNotNull(textDir); + Preconditions.checkArgumentInRange(start, 0, text.length(), "start"); + Preconditions.checkArgumentInRange(end, 0, text.length(), "end"); + + final IntArray paragraphEnds = new IntArray(); + final ArrayList<MeasuredText> measuredTexts = new ArrayList<>(); + + int paraEnd = 0; + for (int paraStart = start; paraStart < end; paraStart = paraEnd) { + paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end); + if (paraEnd < 0) { + // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph end. + paraEnd = end; + } else { + paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph. + } + + paragraphEnds.add(paraEnd); + measuredTexts.add(MeasuredText.buildForStaticLayout( + paint, text, paraStart, paraEnd, textDir, null /* no recycle */)); + } + + return new PremeasuredText(text, start, end, paint, textDir, + measuredTexts.toArray(new MeasuredText[measuredTexts.size()]), + paragraphEnds.toArray()); + } + + // Use PremeasuredText.build instead. + private PremeasuredText(@NonNull CharSequence text, + @IntRange(from = 0) int start, + @IntRange(from = 0) int end, + @NonNull TextPaint paint, + @NonNull TextDirectionHeuristic textDir, + @NonNull MeasuredText[] measuredTexts, + @NonNull int[] paragraphBreakPoints) { + mText = text; + mStart = start; + mEnd = end; + mPaint = paint; + mMeasuredTexts = measuredTexts; + mParagraphBreakPoints = paragraphBreakPoints; + mTextDir = textDir; + } + + /** + * Return the underlying text. + */ + public @NonNull CharSequence getText() { + return mText; + } + + /** + * Returns the inclusive start offset of measured region. + */ + public @IntRange(from = 0) int getStart() { + return mStart; + } + + /** + * Returns the exclusive end offset of measured region. + */ + public @IntRange(from = 0) int getEnd() { + return mEnd; + } + + /** + * Returns the text direction associated with char sequence. + */ + public @NonNull TextDirectionHeuristic getTextDir() { + return mTextDir; + } + + /** + * Returns the paint used to measure this text. + */ + public @NonNull TextPaint getPaint() { + return mPaint; + } + + /** + * Returns the length of the paragraph of this text. + */ + public @IntRange(from = 0) int getParagraphCount() { + return mParagraphBreakPoints.length; + } + + /** + * Returns the paragraph start offset of the text. + */ + public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) { + Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); + return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1]; + } + + /** + * Returns the paragraph end offset of the text. + */ + public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) { + Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); + return mParagraphBreakPoints[paraIndex]; + } + + /** @hide */ + public @NonNull MeasuredText getMeasuredText(@IntRange(from = 0) int paraIndex) { + return mMeasuredTexts[paraIndex]; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Spanned overrides + // + // Just proxy for underlying mText if appropriate. + + @Override + public <T> T[] getSpans(int start, int end, Class<T> type) { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpans(start, end, type); + } else { + return ArrayUtils.emptyArray(type); + } + } + + @Override + public int getSpanStart(Object tag) { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpanStart(tag); + } else { + return -1; + } + } + + @Override + public int getSpanEnd(Object tag) { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpanEnd(tag); + } else { + return -1; + } + } + + @Override + public int getSpanFlags(Object tag) { + if (mText instanceof Spanned) { + return ((Spanned) mText).getSpanFlags(tag); + } else { + return 0; + } + } + + @Override + public int nextSpanTransition(int start, int limit, Class type) { + if (mText instanceof Spanned) { + return ((Spanned) mText).nextSpanTransition(start, limit, type); + } else { + return mText.length(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // CharSequence overrides. + // + // Just proxy for underlying mText. + + @Override + public int length() { + return mText.length(); + } + + @Override + public char charAt(int index) { + // TODO: Should this be index + mStart ? + return mText.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + // TODO: return PremeasuredText. + // TODO: Should this be index + mStart, end + mStart ? + return mText.subSequence(start, end); + } + + @Override + public String toString() { + return mText.toString(); + } +} diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java index 3222dbf8718e..34456580ee61 100644 --- a/core/java/android/text/Selection.java +++ b/core/java/android/text/Selection.java @@ -16,6 +16,8 @@ package android.text; +import android.annotation.TestApi; + import java.text.BreakIterator; @@ -35,10 +37,10 @@ public class Selection { * there is no selection or cursor. */ public static final int getSelectionStart(CharSequence text) { - if (text instanceof Spanned) + if (text instanceof Spanned) { return ((Spanned) text).getSpanStart(SELECTION_START); - else - return -1; + } + return -1; } /** @@ -46,10 +48,17 @@ public class Selection { * there is no selection or cursor. */ public static final int getSelectionEnd(CharSequence text) { - if (text instanceof Spanned) + if (text instanceof Spanned) { return ((Spanned) text).getSpanStart(SELECTION_END); - else - return -1; + } + return -1; + } + + private static int getSelectionMemory(CharSequence text) { + if (text instanceof Spanned) { + return ((Spanned) text).getSpanStart(SELECTION_MEMORY); + } + return -1; } /* @@ -65,6 +74,14 @@ public class Selection { * to <code>stop</code>. */ public static void setSelection(Spannable text, int start, int stop) { + setSelection(text, start, stop, -1); + } + + /** + * Set the selection anchor to <code>start</code>, the selection edge + * to <code>stop</code> and the memory horizontal to <code>memory</code>. + */ + private static void setSelection(Spannable text, int start, int stop, int memory) { // int len = text.length(); // start = pin(start, 0, len); XXX remove unless we really need it // stop = pin(stop, 0, len); @@ -74,9 +91,57 @@ public class Selection { if (ostart != start || oend != stop) { text.setSpan(SELECTION_START, start, start, - Spanned.SPAN_POINT_POINT|Spanned.SPAN_INTERMEDIATE); - text.setSpan(SELECTION_END, stop, stop, - Spanned.SPAN_POINT_POINT); + Spanned.SPAN_POINT_POINT | Spanned.SPAN_INTERMEDIATE); + text.setSpan(SELECTION_END, stop, stop, Spanned.SPAN_POINT_POINT); + updateMemory(text, memory); + } + } + + /** + * Update the memory position for text. This is used to ensure vertical navigation of lines + * with different lengths behaves as expected and remembers the longest horizontal position + * seen during a vertical traversal. + */ + private static void updateMemory(Spannable text, int memory) { + if (memory > -1) { + int currentMemory = getSelectionMemory(text); + if (memory != currentMemory) { + text.setSpan(SELECTION_MEMORY, memory, memory, Spanned.SPAN_POINT_POINT); + if (currentMemory == -1) { + // This is the first value, create a watcher. + final TextWatcher watcher = new MemoryTextWatcher(); + text.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + } + } else { + removeMemory(text); + } + } + + private static void removeMemory(Spannable text) { + text.removeSpan(SELECTION_MEMORY); + MemoryTextWatcher[] watchers = text.getSpans(0, text.length(), MemoryTextWatcher.class); + for (MemoryTextWatcher watcher : watchers) { + text.removeSpan(watcher); + } + } + + /** + * @hide + */ + @TestApi + public static final class MemoryTextWatcher implements TextWatcher { + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + s.removeSpan(SELECTION_MEMORY); + s.removeSpan(this); } } @@ -98,8 +163,17 @@ public class Selection { * Move the selection edge to offset <code>index</code>. */ public static final void extendSelection(Spannable text, int index) { - if (text.getSpanStart(SELECTION_END) != index) + extendSelection(text, index, -1); + } + + /** + * Move the selection edge to offset <code>index</code> and update the memory horizontal. + */ + private static void extendSelection(Spannable text, int index, int memory) { + if (text.getSpanStart(SELECTION_END) != index) { text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT); + } + updateMemory(text, memory); } /** @@ -108,6 +182,7 @@ public class Selection { public static final void removeSelection(Spannable text) { text.removeSpan(SELECTION_START); text.removeSpan(SELECTION_END); + removeMemory(text); } /* @@ -138,17 +213,8 @@ public class Selection { int line = layout.getLineForOffset(end); if (line > 0) { - int move; - - if (layout.getParagraphDirection(line) == - layout.getParagraphDirection(line - 1)) { - float h = layout.getPrimaryHorizontal(end); - move = layout.getOffsetForHorizontal(line - 1, h); - } else { - move = layout.getLineStart(line - 1); - } - - setSelection(text, move); + setSelectionAndMemory( + text, layout, line, end, -1 /* direction */, false /* extend */); return true; } else if (end != 0) { setSelection(text, 0); @@ -160,6 +226,40 @@ public class Selection { } /** + * Calculate the movement and memory positions needed, and set or extend the selection. + */ + private static void setSelectionAndMemory(Spannable text, Layout layout, int line, int end, + int direction, boolean extend) { + int move; + int newMemory; + + if (layout.getParagraphDirection(line) + == layout.getParagraphDirection(line + direction)) { + int memory = getSelectionMemory(text); + if (memory > -1) { + // We have a memory position + float h = layout.getPrimaryHorizontal(memory); + move = layout.getOffsetForHorizontal(line + direction, h); + newMemory = memory; + } else { + // Create a new memory position + float h = layout.getPrimaryHorizontal(end); + move = layout.getOffsetForHorizontal(line + direction, h); + newMemory = end; + } + } else { + move = layout.getLineStart(line + direction); + newMemory = -1; + } + + if (extend) { + extendSelection(text, move, newMemory); + } else { + setSelection(text, move, move, newMemory); + } + } + + /** * Move the cursor to the buffer offset physically below the current * offset, to the end of the buffer if it is on the bottom line but * not at the end, or return false if the cursor is already at the @@ -184,17 +284,8 @@ public class Selection { int line = layout.getLineForOffset(end); if (line < layout.getLineCount() - 1) { - int move; - - if (layout.getParagraphDirection(line) == - layout.getParagraphDirection(line + 1)) { - float h = layout.getPrimaryHorizontal(end); - move = layout.getOffsetForHorizontal(line + 1, h); - } else { - move = layout.getLineStart(line + 1); - } - - setSelection(text, move); + setSelectionAndMemory( + text, layout, line, end, 1 /* direction */, false /* extend */); return true; } else if (end != text.length()) { setSelection(text, text.length()); @@ -263,17 +354,7 @@ public class Selection { int line = layout.getLineForOffset(end); if (line > 0) { - int move; - - if (layout.getParagraphDirection(line) == - layout.getParagraphDirection(line - 1)) { - float h = layout.getPrimaryHorizontal(end); - move = layout.getOffsetForHorizontal(line - 1, h); - } else { - move = layout.getLineStart(line - 1); - } - - extendSelection(text, move); + setSelectionAndMemory(text, layout, line, end, -1 /* direction */, true /* extend */); return true; } else if (end != 0) { extendSelection(text, 0); @@ -292,20 +373,10 @@ public class Selection { int line = layout.getLineForOffset(end); if (line < layout.getLineCount() - 1) { - int move; - - if (layout.getParagraphDirection(line) == - layout.getParagraphDirection(line + 1)) { - float h = layout.getPrimaryHorizontal(end); - move = layout.getOffsetForHorizontal(line + 1, h); - } else { - move = layout.getLineStart(line + 1); - } - - extendSelection(text, move); + setSelectionAndMemory(text, layout, line, end, 1 /* direction */, true /* extend */); return true; } else if (end != text.length()) { - extendSelection(text, text.length()); + extendSelection(text, text.length(), -1); return true; } @@ -466,6 +537,8 @@ public class Selection { private static final class START implements NoCopySpan { } private static final class END implements NoCopySpan { } + private static final class MEMORY implements NoCopySpan { } + private static final Object SELECTION_MEMORY = new MEMORY(); /* * Public constants diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index a03a4fbd5243..2e10fe8d4267 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,13 +16,15 @@ package android.text; +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Paint; -import android.os.LocaleList; +import android.text.AutoGrowArray.FloatArray; import android.text.style.LeadingMarginSpan; import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; import android.text.style.LineHeightSpan; -import android.text.style.MetricAffectingSpan; import android.text.style.TabStopSpan; import android.util.Log; import android.util.Pools.SynchronizedPool; @@ -30,9 +32,10 @@ import android.util.Pools.SynchronizedPool; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; -import java.nio.ByteBuffer; +import dalvik.annotation.optimization.CriticalNative; +import dalvik.annotation.optimization.FastNative; + import java.util.Arrays; -import java.util.Locale; /** * StaticLayout is a Layout for text that will not be edited after it @@ -45,25 +48,34 @@ import java.util.Locale; * Canvas.drawText()} directly.</p> */ public class StaticLayout extends Layout { + /* + * The break iteration is done in native code. The protocol for using the native code is as + * follows. + * + * First, call nInit to setup native line breaker object. Then, for each paragraph, do the + * following: + * + * - Create MeasuredText by MeasuredText.buildForStaticLayout which measures in native. + * - Run nComputeLineBreaks() to obtain line breaks for the paragraph. + * + * After all paragraphs, call finish() to release expensive buffers. + */ static final String TAG = "StaticLayout"; /** - * Builder for static layouts. The builder is a newer pattern for constructing - * StaticLayout objects and should be preferred over the constructors, - * particularly to access newer features. To build a static layout, first - * call {@link #obtain} with the required arguments (text, paint, and width), - * then call setters for optional parameters, and finally {@link #build} - * to build the StaticLayout object. Parameters not explicitly set will get + * Builder for static layouts. The builder is the preferred pattern for constructing + * StaticLayout objects and should be preferred over the constructors, particularly to access + * newer features. To build a static layout, first call {@link #obtain} with the required + * arguments (text, paint, and width), then call setters for optional parameters, and finally + * {@link #build} to build the StaticLayout object. Parameters not explicitly set will get * default values. */ public final static class Builder { - private Builder() { - mNativePtr = nNewBuilder(); - } + private Builder() {} /** - * Obtain a builder for constructing StaticLayout objects + * Obtain a builder for constructing StaticLayout objects. * * @param source The text to be laid out, optionally with spans * @param start The index of the start of the text @@ -72,8 +84,10 @@ public class StaticLayout extends Layout { * @param width The width in pixels * @return a builder object used for constructing the StaticLayout */ - public static Builder obtain(CharSequence source, int start, int end, TextPaint paint, - int width) { + @NonNull + public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start, + @IntRange(from = 0) int end, @NonNull TextPaint paint, + @IntRange(from = 0) int width) { Builder b = sPool.acquire(); if (b == null) { b = new Builder(); @@ -87,39 +101,41 @@ public class StaticLayout extends Layout { b.mWidth = width; b.mAlignment = Alignment.ALIGN_NORMAL; b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; - b.mSpacingMult = 1.0f; - b.mSpacingAdd = 0.0f; + b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER; + b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION; b.mIncludePad = true; + b.mFallbackLineSpacing = false; b.mEllipsizedWidth = width; b.mEllipsize = null; b.mMaxLines = Integer.MAX_VALUE; b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; - - b.mMeasuredText = MeasuredText.obtain(); return b; } - private static void recycle(Builder b) { + /** + * This method should be called after the layout is finished getting constructed and the + * builder needs to be cleaned up and returned to the pool. + */ + private static void recycle(@NonNull Builder b) { b.mPaint = null; b.mText = null; - MeasuredText.recycle(b.mMeasuredText); - b.mMeasuredText = null; b.mLeftIndents = null; b.mRightIndents = null; - nFinishBuilder(b.mNativePtr); + b.mLeftPaddings = null; + b.mRightPaddings = null; sPool.release(b); } // release any expensive state /* package */ void finish() { - nFinishBuilder(mNativePtr); mText = null; mPaint = null; mLeftIndents = null; mRightIndents = null; - mMeasuredText.finish(); + mLeftPaddings = null; + mRightPaddings = null; } public Builder setText(CharSequence source) { @@ -138,7 +154,8 @@ public class StaticLayout extends Layout { * * @hide */ - public Builder setText(CharSequence source, int start, int end) { + @NonNull + public Builder setText(@NonNull CharSequence source, int start, int end) { mText = source; mStart = start; mEnd = end; @@ -153,7 +170,8 @@ public class StaticLayout extends Layout { * * @hide */ - public Builder setPaint(TextPaint paint) { + @NonNull + public Builder setPaint(@NonNull TextPaint paint) { mPaint = paint; return this; } @@ -166,7 +184,8 @@ public class StaticLayout extends Layout { * * @hide */ - public Builder setWidth(int width) { + @NonNull + public Builder setWidth(@IntRange(from = 0) int width) { mWidth = width; if (mEllipsize == null) { mEllipsizedWidth = width; @@ -180,34 +199,38 @@ public class StaticLayout extends Layout { * @param alignment Alignment for the resulting {@link StaticLayout} * @return this builder, useful for chaining */ - public Builder setAlignment(Alignment alignment) { + @NonNull + public Builder setAlignment(@NonNull Alignment alignment) { mAlignment = alignment; return this; } /** * Set the text direction heuristic. The text direction heuristic is used to - * resolve text direction based per-paragraph based on the input text. The default is + * resolve text direction per-paragraph based on the input text. The default is * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. * - * @param textDir text direction heuristic for resolving BiDi behavior. + * @param textDir text direction heuristic for resolving bidi behavior. * @return this builder, useful for chaining */ - public Builder setTextDirection(TextDirectionHeuristic textDir) { + @NonNull + public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { mTextDir = textDir; return this; } /** - * Set line spacing parameters. The default is 0.0 for {@code spacingAdd} - * and 1.0 for {@code spacingMult}. + * Set line spacing parameters. Each line will have its line spacing multiplied by + * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for + * {@code spacingAdd} and 1.0 for {@code spacingMult}. * - * @param spacingAdd line spacing add - * @param spacingMult line spacing multiplier + * @param spacingAdd the amount of line spacing addition + * @param spacingMult the line spacing multiplier * @return this builder, useful for chaining * @see android.widget.TextView#setLineSpacing */ - public Builder setLineSpacing(float spacingAdd, float spacingMult) { + @NonNull + public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) { mSpacingAdd = spacingAdd; mSpacingMult = spacingMult; return this; @@ -222,12 +245,32 @@ public class StaticLayout extends Layout { * @return this builder, useful for chaining * @see android.widget.TextView#setIncludeFontPadding */ + @NonNull public Builder setIncludePad(boolean includePad) { mIncludePad = includePad; return this; } /** + * Set whether to respect the ascent and descent of the fallback fonts that are used in + * displaying the text (which is needed to avoid text from consecutive lines running into + * each other). If set, fallback fonts that end up getting used can increase the ascent + * and descent of the lines that they are used on. + * + * <p>For backward compatibility reasons, the default is {@code false}, but setting this to + * true is strongly recommended. It is required to be true if text could be in languages + * like Burmese or Tibetan where text is typically much taller or deeper than Latin text. + * + * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts + * @return this builder, useful for chaining + */ + @NonNull + public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) { + mFallbackLineSpacing = useLineSpacingFromFallbacks; + return this; + } + + /** * Set the width as used for ellipsizing purposes, if it differs from the * normal layout width. The default is the {@code width} * passed to {@link #obtain}. @@ -236,7 +279,8 @@ public class StaticLayout extends Layout { * @return this builder, useful for chaining * @see android.widget.TextView#setEllipsize */ - public Builder setEllipsizedWidth(int ellipsizedWidth) { + @NonNull + public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) { mEllipsizedWidth = ellipsizedWidth; return this; } @@ -246,13 +290,13 @@ public class StaticLayout extends Layout { * is wide, or exceeding the number of lines (see #setMaxLines) in the case * of {@link android.text.TextUtils.TruncateAt#END} or * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead - * of broken. The default is - * {@code null}, indicating no ellipsis is to be applied. + * of broken. The default is {@code null}, indicating no ellipsis is to be applied. * * @param ellipsize type of ellipsis behavior * @return this builder, useful for chaining * @see android.widget.TextView#setEllipsize */ + @NonNull public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { mEllipsize = ellipsize; return this; @@ -267,7 +311,8 @@ public class StaticLayout extends Layout { * @return this builder, useful for chaining * @see android.widget.TextView#setMaxLines */ - public Builder setMaxLines(int maxLines) { + @NonNull + public Builder setMaxLines(@IntRange(from = 0) int maxLines) { mMaxLines = maxLines; return this; } @@ -280,6 +325,7 @@ public class StaticLayout extends Layout { * @return this builder, useful for chaining * @see android.widget.TextView#setBreakStrategy */ + @NonNull public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { mBreakStrategy = breakStrategy; return this; @@ -287,12 +333,15 @@ public class StaticLayout extends Layout { /** * Set hyphenation frequency, to control the amount of automatic hyphenation used. The - * default is {@link Layout#HYPHENATION_FREQUENCY_NONE}. + * possible values are defined in {@link Layout}, by constants named with the pattern + * {@code HYPHENATION_FREQUENCY_*}. The default is + * {@link Layout#HYPHENATION_FREQUENCY_NONE}. * * @param hyphenationFrequency hyphenation frequency for the paragraph * @return this builder, useful for chaining * @see android.widget.TextView#setHyphenationFrequency */ + @NonNull public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { mHyphenationFrequency = hyphenationFrequency; return this; @@ -306,18 +355,32 @@ public class StaticLayout extends Layout { * @param rightIndents array of indent values for right margin, in pixels * @return this builder, useful for chaining */ - public Builder setIndents(int[] leftIndents, int[] rightIndents) { + @NonNull + public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) { mLeftIndents = leftIndents; mRightIndents = rightIndents; - int leftLen = leftIndents == null ? 0 : leftIndents.length; - int rightLen = rightIndents == null ? 0 : rightIndents.length; - int[] indents = new int[Math.max(leftLen, rightLen)]; - for (int i = 0; i < indents.length; i++) { - int leftMargin = i < leftLen ? leftIndents[i] : 0; - int rightMargin = i < rightLen ? rightIndents[i] : 0; - indents[i] = leftMargin + rightMargin; - } - nSetIndents(mNativePtr, indents); + return this; + } + + /** + * Set available paddings to draw overhanging text on. Arguments are arrays holding the + * amount of padding available, one per line, measured in pixels. For lines past the last + * element in the array, the last element repeats. + * + * The individual padding amounts should be non-negative. The result of passing negative + * paddings is undefined. + * + * @param leftPaddings array of amounts of available padding for left margin, in pixels + * @param rightPaddings array of amounts of available padding for right margin, in pixels + * @return this builder, useful for chaining + * + * @hide + */ + @NonNull + public Builder setAvailablePaddings(@Nullable int[] leftPaddings, + @Nullable int[] rightPaddings) { + mLeftPaddings = leftPaddings; + mRightPaddings = rightPaddings; return this; } @@ -329,61 +392,22 @@ public class StaticLayout extends Layout { * @param justificationMode justification mode for the paragraph. * @return this builder, useful for chaining. */ + @NonNull public Builder setJustificationMode(@JustificationMode int justificationMode) { mJustificationMode = justificationMode; return this; } - private long[] getHyphenators(LocaleList locales) { - final int length = locales.size(); - final long[] result = new long[length]; - for (int i = 0; i < length; i++) { - final Locale locale = locales.get(i); - result[i] = Hyphenator.get(locale).getNativePtr(); - } - return result; - } - /** - * Measurement and break iteration is done in native code. The protocol for using - * the native code is as follows. - * - * For each paragraph, do a nSetupParagraph, which sets paragraph text, line width, tab - * stops, break strategy, and hyphenation frequency (and possibly other parameters in the - * future). + * Sets whether the line spacing should be applied for the last line. Default value is + * {@code false}. * - * Then, for each run within the paragraph: - * - setLocales (this must be done at least for the first run, optional afterwards) - * - one of the following, depending on the type of run: - * + addStyleRun (a text run, to be measured in native code) - * + addMeasuredRun (a run already measured in Java, passed into native code) - * + addReplacementRun (a replacement run, width is given) - * - * After measurement, nGetWidths() is valid if the widths are needed (eg for ellipsis). - * Run nComputeLineBreaks() to obtain line breaks for the paragraph. - * - * After all paragraphs, call finish() to release expensive buffers. + * @hide */ - - private void setLocales(LocaleList locales) { - if (!locales.equals(mLocales)) { - nSetLocales(mNativePtr, locales.toLanguageTags(), getHyphenators(locales)); - mLocales = locales; - } - } - - /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) { - setLocales(paint.getTextLocales()); - return nAddStyleRun(mNativePtr, paint.getNativeInstance(), paint.mNativeTypeface, - start, end, isRtl); - } - - /* package */ void addMeasuredRun(int start, int end, float[] widths) { - nAddMeasuredRun(mNativePtr, start, end, widths); - } - - /* package */ void addReplacementRun(int start, int end, float width) { - nAddReplacementRun(mNativePtr, start, end, width); + @NonNull + /* package */ Builder setAddLastLineLineSpacing(boolean value) { + mAddLastLineLineSpacing = value; + return this; } /** @@ -395,50 +419,39 @@ public class StaticLayout extends Layout { * * @return the newly constructed {@link StaticLayout} object */ + @NonNull public StaticLayout build() { StaticLayout result = new StaticLayout(this); Builder.recycle(this); return result; } - @Override - protected void finalize() throws Throwable { - try { - nFreeBuilder(mNativePtr); - } finally { - super.finalize(); - } - } - - /* package */ long mNativePtr; - - CharSequence mText; - int mStart; - int mEnd; - TextPaint mPaint; - int mWidth; - Alignment mAlignment; - TextDirectionHeuristic mTextDir; - float mSpacingMult; - float mSpacingAdd; - boolean mIncludePad; - int mEllipsizedWidth; - TextUtils.TruncateAt mEllipsize; - int mMaxLines; - int mBreakStrategy; - int mHyphenationFrequency; - int[] mLeftIndents; - int[] mRightIndents; - int mJustificationMode; - - Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); - - // This will go away and be subsumed by native builder code - MeasuredText mMeasuredText; - - LocaleList mLocales; - - private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); + private CharSequence mText; + private int mStart; + private int mEnd; + private TextPaint mPaint; + private int mWidth; + private Alignment mAlignment; + private TextDirectionHeuristic mTextDir; + private float mSpacingMult; + private float mSpacingAdd; + private boolean mIncludePad; + private boolean mFallbackLineSpacing; + private int mEllipsizedWidth; + private TextUtils.TruncateAt mEllipsize; + private int mMaxLines; + private int mBreakStrategy; + private int mHyphenationFrequency; + @Nullable private int[] mLeftIndents; + @Nullable private int[] mRightIndents; + @Nullable private int[] mLeftPaddings; + @Nullable private int[] mRightPaddings; + private int mJustificationMode; + private boolean mAddLastLineLineSpacing; + + private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); + + private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3); } public StaticLayout(CharSequence source, TextPaint paint, @@ -517,12 +530,17 @@ public class StaticLayout extends Layout { .setEllipsize(ellipsize) .setMaxLines(maxLines); /* - * This is annoying, but we can't refer to the layout until - * superclass construction is finished, and the superclass - * constructor wants the reference to the display text. + * This is annoying, but we can't refer to the layout until superclass construction is + * finished, and the superclass constructor wants the reference to the display text. + * + * In other words, the two Ellipsizer classes in Layout.java need a (Dynamic|Static)Layout + * as a parameter to do their calculations, but the Ellipsizers also need to be the input + * to the superclass's constructor (Layout). In order to go around the circular + * dependency, we construct the Ellipsizer with only one of the parameters, the text. And + * we fill in the rest of the needed information (layout, width, and method) later, here. * - * This will break if the superclass constructor ever actually - * cares about the content instead of just holding the reference. + * This will break if the superclass constructor ever actually cares about the content + * instead of just holding the reference. */ if (ellipsize != null) { Ellipsizer e = (Ellipsizer) getText(); @@ -538,8 +556,8 @@ public class StaticLayout extends Layout { mEllipsizedWidth = outerwidth; } - mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); - mLines = new int[mLineDirections.length]; + mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2); + mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns); mMaximumVisibleLineCount = maxLines; generate(b, b.mIncludePad, b.mIncludePad); @@ -547,12 +565,12 @@ public class StaticLayout extends Layout { Builder.recycle(b); } - /* package */ StaticLayout(CharSequence text) { + /* package */ StaticLayout(@Nullable CharSequence text) { super(text, null, 0, null, 0, 0); mColumns = COLUMNS_ELLIPSIZE; - mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); - mLines = new int[mLineDirections.length]; + mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2); + mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns); } private StaticLayout(Builder b) { @@ -561,7 +579,7 @@ public class StaticLayout extends Layout { : (b.mText instanceof Spanned) ? new SpannedEllipsizer(b.mText) : new Ellipsizer(b.mText), - b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd); + b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd); if (b.mEllipsize != null) { Ellipsizer e = (Ellipsizer) getText(); @@ -577,35 +595,34 @@ public class StaticLayout extends Layout { mEllipsizedWidth = b.mWidth; } - mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); - mLines = new int[mLineDirections.length]; + mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2); + mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns); mMaximumVisibleLineCount = b.mMaxLines; mLeftIndents = b.mLeftIndents; mRightIndents = b.mRightIndents; + mLeftPaddings = b.mLeftPaddings; + mRightPaddings = b.mRightPaddings; setJustificationMode(b.mJustificationMode); generate(b, b.mIncludePad, b.mIncludePad); } /* package */ void generate(Builder b, boolean includepad, boolean trackpad) { - CharSequence source = b.mText; - int bufStart = b.mStart; - int bufEnd = b.mEnd; + final CharSequence source = b.mText; + final int bufStart = b.mStart; + final int bufEnd = b.mEnd; TextPaint paint = b.mPaint; int outerWidth = b.mWidth; TextDirectionHeuristic textDir = b.mTextDir; + final boolean fallbackLineSpacing = b.mFallbackLineSpacing; float spacingmult = b.mSpacingMult; float spacingadd = b.mSpacingAdd; float ellipsizedWidth = b.mEllipsizedWidth; TextUtils.TruncateAt ellipsize = b.mEllipsize; + final boolean addLastLineSpacing = b.mAddLastLineLineSpacing; LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs - // store span end locations - int[] spanEndCache = new int[4]; - // store fontMetrics per span range - // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range) - int[] fmCache = new int[4 * 4]; - b.setLocales(paint.getTextLocales()); + FloatArray widths = new FloatArray(); mLineCount = 0; mEllipsized = false; @@ -617,340 +634,336 @@ public class StaticLayout extends Layout { Paint.FontMetricsInt fm = b.mFontMetricsInt; int[] chooseHtv = null; - MeasuredText measured = b.mMeasuredText; - - Spanned spanned = null; - if (source instanceof Spanned) - spanned = (Spanned) source; - - int paraEnd; - for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) { - paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd); - if (paraEnd < 0) - paraEnd = bufEnd; - else - paraEnd++; - - int firstWidthLineCount = 1; - int firstWidth = outerWidth; - int restWidth = outerWidth; - - LineHeightSpan[] chooseHt = null; - - if (spanned != null) { - LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd, - LeadingMarginSpan.class); - for (int i = 0; i < sp.length; i++) { - LeadingMarginSpan lms = sp[i]; - firstWidth -= sp[i].getLeadingMargin(true); - restWidth -= sp[i].getLeadingMargin(false); - - // LeadingMarginSpan2 is odd. The count affects all - // leading margin spans, not just this particular one - if (lms instanceof LeadingMarginSpan2) { - LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; - firstWidthLineCount = Math.max(firstWidthLineCount, - lms2.getLeadingMarginLineCount()); - } - } - - chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class); + final int[] indents; + if (mLeftIndents != null || mRightIndents != null) { + final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length; + final int rightLen = mRightIndents == null ? 0 : mRightIndents.length; + final int indentsLen = Math.max(leftLen, rightLen); + indents = new int[indentsLen]; + for (int i = 0; i < leftLen; i++) { + indents[i] = mLeftIndents[i]; + } + for (int i = 0; i < rightLen; i++) { + indents[i] += mRightIndents[i]; + } + } else { + indents = null; + } - if (chooseHt.length == 0) { - chooseHt = null; // So that out() would not assume it has any contents - } else { - if (chooseHtv == null || - chooseHtv.length < chooseHt.length) { - chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length); - } + final long nativePtr = nInit( + b.mBreakStrategy, b.mHyphenationFrequency, + // TODO: Support more justification mode, e.g. letter spacing, stretching. + b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE, + indents, mLeftPaddings, mRightPaddings); - for (int i = 0; i < chooseHt.length; i++) { - int o = spanned.getSpanStart(chooseHt[i]); + PremeasuredText premeasured = null; + final Spanned spanned; + if (source instanceof PremeasuredText) { + premeasured = (PremeasuredText) source; - if (o < paraStart) { - // starts in this layout, before the - // current paragraph + final CharSequence original = premeasured.getText(); + spanned = (original instanceof Spanned) ? (Spanned) original : null; - chooseHtv[i] = getLineTop(getLineForOffset(o)); - } else { - // starts in this paragraph + if (bufStart != premeasured.getStart() || bufEnd != premeasured.getEnd()) { + // The buffer position has changed. Re-measure here. + premeasured = PremeasuredText.build(original, paint, textDir, bufStart, bufEnd); + } else { + // We can use premeasured information. - chooseHtv[i] = v; - } - } - } + // Overwrite with the one when premeasured. + // TODO: Give an option for developer not to overwrite and measure again here? + textDir = premeasured.getTextDir(); + paint = premeasured.getPaint(); } + } else { + premeasured = PremeasuredText.build(source, paint, textDir, bufStart, bufEnd); + spanned = (source instanceof Spanned) ? (Spanned) source : null; + } - measured.setPara(source, paraStart, paraEnd, textDir, b); - char[] chs = measured.mChars; - float[] widths = measured.mWidths; - byte[] chdirs = measured.mLevels; - int dir = measured.mDir; - boolean easy = measured.mEasy; - - // tab stop locations - int[] variableTabStops = null; - if (spanned != null) { - TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, - paraEnd, TabStopSpan.class); - if (spans.length > 0) { - int[] stops = new int[spans.length]; - for (int i = 0; i < spans.length; i++) { - stops[i] = spans[i].getTabStop(); + try { + for (int paraIndex = 0; paraIndex < premeasured.getParagraphCount(); paraIndex++) { + final int paraStart = premeasured.getParagraphStart(paraIndex); + final int paraEnd = premeasured.getParagraphEnd(paraIndex); + + int firstWidthLineCount = 1; + int firstWidth = outerWidth; + int restWidth = outerWidth; + + LineHeightSpan[] chooseHt = null; + + if (spanned != null) { + LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd, + LeadingMarginSpan.class); + for (int i = 0; i < sp.length; i++) { + LeadingMarginSpan lms = sp[i]; + firstWidth -= sp[i].getLeadingMargin(true); + restWidth -= sp[i].getLeadingMargin(false); + + // LeadingMarginSpan2 is odd. The count affects all + // leading margin spans, not just this particular one + if (lms instanceof LeadingMarginSpan2) { + LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; + firstWidthLineCount = Math.max(firstWidthLineCount, + lms2.getLeadingMarginLineCount()); + } } - Arrays.sort(stops, 0, stops.length); - variableTabStops = stops; - } - } - nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart, - firstWidth, firstWidthLineCount, restWidth, - variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency, - // TODO: Support more justification mode, e.g. letter spacing, stretching. - b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE); - if (mLeftIndents != null || mRightIndents != null) { - // TODO(raph) performance: it would be better to do this once per layout rather - // than once per paragraph, but that would require a change to the native - // interface. - int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length; - int rightLen = mRightIndents == null ? 0 : mRightIndents.length; - int indentsLen = Math.max(1, Math.max(leftLen, rightLen) - mLineCount); - int[] indents = new int[indentsLen]; - for (int i = 0; i < indentsLen; i++) { - int leftMargin = mLeftIndents == null ? 0 : - mLeftIndents[Math.min(i + mLineCount, leftLen - 1)]; - int rightMargin = mRightIndents == null ? 0 : - mRightIndents[Math.min(i + mLineCount, rightLen - 1)]; - indents[i] = leftMargin + rightMargin; - } - nSetIndents(b.mNativePtr, indents); - } - - // measurement has to be done before performing line breaking - // but we don't want to recompute fontmetrics or span ranges the - // second time, so we cache those and then use those stored values - int fmCacheCount = 0; - int spanEndCacheCount = 0; - for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { - if (fmCacheCount * 4 >= fmCache.length) { - int[] grow = new int[fmCacheCount * 4 * 2]; - System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4); - fmCache = grow; - } + chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class); - if (spanEndCacheCount >= spanEndCache.length) { - int[] grow = new int[spanEndCacheCount * 2]; - System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount); - spanEndCache = grow; - } + if (chooseHt.length == 0) { + chooseHt = null; // So that out() would not assume it has any contents + } else { + if (chooseHtv == null || chooseHtv.length < chooseHt.length) { + chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length); + } - if (spanned == null) { - spanEnd = paraEnd; - int spanLen = spanEnd - spanStart; - measured.addStyleRun(paint, spanLen, fm); - } else { - spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, - MetricAffectingSpan.class); - int spanLen = spanEnd - spanStart; - MetricAffectingSpan[] spans = - spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); - spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class); - measured.addStyleRun(paint, spans, spanLen, fm); - } + for (int i = 0; i < chooseHt.length; i++) { + int o = spanned.getSpanStart(chooseHt[i]); - // the order of storage here (top, bottom, ascent, descent) has to match the code below - // where these values are retrieved - fmCache[fmCacheCount * 4 + 0] = fm.top; - fmCache[fmCacheCount * 4 + 1] = fm.bottom; - fmCache[fmCacheCount * 4 + 2] = fm.ascent; - fmCache[fmCacheCount * 4 + 3] = fm.descent; - fmCacheCount++; + if (o < paraStart) { + // starts in this layout, before the + // current paragraph - spanEndCache[spanEndCacheCount] = spanEnd; - spanEndCacheCount++; - } + chooseHtv[i] = getLineTop(getLineForOffset(o)); + } else { + // starts in this paragraph - nGetWidths(b.mNativePtr, widths); - int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks, - lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length); - - int[] breaks = lineBreaks.breaks; - float[] lineWidths = lineBreaks.widths; - int[] flags = lineBreaks.flags; - - final int remainingLineCount = mMaximumVisibleLineCount - mLineCount; - final boolean ellipsisMayBeApplied = ellipsize != null - && (ellipsize == TextUtils.TruncateAt.END - || (mMaximumVisibleLineCount == 1 - && ellipsize != TextUtils.TruncateAt.MARQUEE)); - if (remainingLineCount > 0 && remainingLineCount < breakCount && - ellipsisMayBeApplied) { - // Calculate width and flag. - float width = 0; - int flag = 0; - for (int i = remainingLineCount - 1; i < breakCount; i++) { - if (i == breakCount - 1) { - width += lineWidths[i]; - } else { - for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) { - width += widths[j]; + chooseHtv[i] = v; + } } } - flag |= flags[i] & TAB_MASK; } - // Treat the last line and overflowed lines as a single line. - breaks[remainingLineCount - 1] = breaks[breakCount - 1]; - lineWidths[remainingLineCount - 1] = width; - flags[remainingLineCount - 1] = flag; - - breakCount = remainingLineCount; - } - // here is the offset of the starting character of the line we are currently measuring - int here = paraStart; - - int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0; - int fmCacheIndex = 0; - int spanEndCacheIndex = 0; - int breakIndex = 0; - for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { - // retrieve end of span - spanEnd = spanEndCache[spanEndCacheIndex++]; - - // retrieve cached metrics, order matches above - fm.top = fmCache[fmCacheIndex * 4 + 0]; - fm.bottom = fmCache[fmCacheIndex * 4 + 1]; - fm.ascent = fmCache[fmCacheIndex * 4 + 2]; - fm.descent = fmCache[fmCacheIndex * 4 + 3]; - fmCacheIndex++; - - if (fm.top < fmTop) { - fmTop = fm.top; - } - if (fm.ascent < fmAscent) { - fmAscent = fm.ascent; - } - if (fm.descent > fmDescent) { - fmDescent = fm.descent; - } - if (fm.bottom > fmBottom) { - fmBottom = fm.bottom; - } - - // skip breaks ending before current span range - while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) { - breakIndex++; + // tab stop locations + int[] variableTabStops = null; + if (spanned != null) { + TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, + paraEnd, TabStopSpan.class); + if (spans.length > 0) { + int[] stops = new int[spans.length]; + for (int i = 0; i < spans.length; i++) { + stops[i] = spans[i].getTabStop(); + } + Arrays.sort(stops, 0, stops.length); + variableTabStops = stops; + } } - while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) { - int endPos = paraStart + breaks[breakIndex]; - - boolean moreChars = (endPos < bufEnd); + final MeasuredText measured = premeasured.getMeasuredText(paraIndex); + final char[] chs = measured.getChars(); + final int[] spanEndCache = measured.getSpanEndCache().getRawArray(); + final int[] fmCache = measured.getFontMetrics().getRawArray(); + // TODO: Stop keeping duplicated width copy in native and Java. + widths.resize(chs.length); + + // measurement has to be done before performing line breaking + // but we don't want to recompute fontmetrics or span ranges the + // second time, so we cache those and then use those stored values + + int breakCount = nComputeLineBreaks( + nativePtr, + + // Inputs + chs, + measured.getNativePtr(), + paraEnd - paraStart, + firstWidth, + firstWidthLineCount, + restWidth, + variableTabStops, + TAB_INCREMENT, + mLineCount, + + // Outputs + lineBreaks, + lineBreaks.breaks.length, + lineBreaks.breaks, + lineBreaks.widths, + lineBreaks.ascents, + lineBreaks.descents, + lineBreaks.flags, + widths.getRawArray()); + + final int[] breaks = lineBreaks.breaks; + final float[] lineWidths = lineBreaks.widths; + final float[] ascents = lineBreaks.ascents; + final float[] descents = lineBreaks.descents; + final int[] flags = lineBreaks.flags; + + final int remainingLineCount = mMaximumVisibleLineCount - mLineCount; + final boolean ellipsisMayBeApplied = ellipsize != null + && (ellipsize == TextUtils.TruncateAt.END + || (mMaximumVisibleLineCount == 1 + && ellipsize != TextUtils.TruncateAt.MARQUEE)); + if (0 < remainingLineCount && remainingLineCount < breakCount + && ellipsisMayBeApplied) { + // Calculate width and flag. + float width = 0; + int flag = 0; // XXX May need to also have starting hyphen edit + for (int i = remainingLineCount - 1; i < breakCount; i++) { + if (i == breakCount - 1) { + width += lineWidths[i]; + } else { + for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) { + width += widths.get(j); + } + } + flag |= flags[i] & TAB_MASK; + } + // Treat the last line and overflowed lines as a single line. + breaks[remainingLineCount - 1] = breaks[breakCount - 1]; + lineWidths[remainingLineCount - 1] = width; + flags[remainingLineCount - 1] = flag; - v = out(source, here, endPos, - fmAscent, fmDescent, fmTop, fmBottom, - v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex], - needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, - chs, widths, paraStart, ellipsize, ellipsizedWidth, - lineWidths[breakIndex], paint, moreChars); + breakCount = remainingLineCount; + } - if (endPos < spanEnd) { - // preserve metrics for current span + // here is the offset of the starting character of the line we are currently + // measuring + int here = paraStart; + + int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0; + int fmCacheIndex = 0; + int spanEndCacheIndex = 0; + int breakIndex = 0; + for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { + // retrieve end of span + spanEnd = spanEndCache[spanEndCacheIndex++]; + + // retrieve cached metrics, order matches above + fm.top = fmCache[fmCacheIndex * 4 + 0]; + fm.bottom = fmCache[fmCacheIndex * 4 + 1]; + fm.ascent = fmCache[fmCacheIndex * 4 + 2]; + fm.descent = fmCache[fmCacheIndex * 4 + 3]; + fmCacheIndex++; + + if (fm.top < fmTop) { fmTop = fm.top; - fmBottom = fm.bottom; + } + if (fm.ascent < fmAscent) { fmAscent = fm.ascent; + } + if (fm.descent > fmDescent) { fmDescent = fm.descent; - } else { - fmTop = fmBottom = fmAscent = fmDescent = 0; + } + if (fm.bottom > fmBottom) { + fmBottom = fm.bottom; } - here = endPos; - breakIndex++; - - if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) { - return; + // skip breaks ending before current span range + while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) { + breakIndex++; } - } - } - if (paraEnd == bufEnd) - break; - } + while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) { + int endPos = paraStart + breaks[breakIndex]; + + boolean moreChars = (endPos < bufEnd); + + final int ascent = fallbackLineSpacing + ? Math.min(fmAscent, Math.round(ascents[breakIndex])) + : fmAscent; + final int descent = fallbackLineSpacing + ? Math.max(fmDescent, Math.round(descents[breakIndex])) + : fmDescent; + v = out(source, here, endPos, + ascent, descent, fmTop, fmBottom, + v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, + flags[breakIndex], needMultiply, measured, bufEnd, + includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(), + paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex], + paint, moreChars); + + if (endPos < spanEnd) { + // preserve metrics for current span + fmTop = fm.top; + fmBottom = fm.bottom; + fmAscent = fm.ascent; + fmDescent = fm.descent; + } else { + fmTop = fmBottom = fmAscent = fmDescent = 0; + } - if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) && - mLineCount < mMaximumVisibleLineCount) { - // Log.e("text", "output last " + bufEnd); + here = endPos; + breakIndex++; - measured.setPara(source, bufEnd, bufEnd, textDir, b); + if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) { + return; + } + } + } - paint.getFontMetricsInt(fm); + if (paraEnd == bufEnd) { + break; + } + } - v = out(source, - bufEnd, bufEnd, fm.ascent, fm.descent, - fm.top, fm.bottom, - v, - spacingmult, spacingadd, null, - null, fm, 0, - needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd, - includepad, trackpad, null, - null, bufStart, ellipsize, - ellipsizedWidth, 0, paint, false); + if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) + && mLineCount < mMaximumVisibleLineCount) { + final MeasuredText measured = + MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, null); + paint.getFontMetricsInt(fm); + v = out(source, + bufEnd, bufEnd, fm.ascent, fm.descent, + fm.top, fm.bottom, + v, + spacingmult, spacingadd, null, + null, fm, 0, + needMultiply, measured, bufEnd, + includepad, trackpad, addLastLineSpacing, null, + null, bufStart, ellipsize, + ellipsizedWidth, 0, paint, false); + } + } finally { + nFinish(nativePtr); } } - private int out(CharSequence text, int start, int end, - int above, int below, int top, int bottom, int v, - float spacingmult, float spacingadd, - LineHeightSpan[] chooseHt, int[] chooseHtv, - Paint.FontMetricsInt fm, int flags, - boolean needMultiply, byte[] chdirs, int dir, - boolean easy, int bufEnd, boolean includePad, - boolean trackPad, char[] chs, - float[] widths, int widthStart, TextUtils.TruncateAt ellipsize, - float ellipsisWidth, float textWidth, - TextPaint paint, boolean moreChars) { - int j = mLineCount; - int off = j * mColumns; - int want = off + mColumns + TOP; + // The parameters that are not changed in the method are marked as final to make the code + // easier to understand. + private int out(final CharSequence text, final int start, final int end, int above, int below, + int top, int bottom, int v, final float spacingmult, final float spacingadd, + final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm, + final int flags, final boolean needMultiply, @NonNull final MeasuredText measured, + final int bufEnd, final boolean includePad, final boolean trackPad, + final boolean addLastLineLineSpacing, final char[] chs, final float[] widths, + final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth, + final float textWidth, final TextPaint paint, final boolean moreChars) { + final int j = mLineCount; + final int off = j * mColumns; + final int want = off + mColumns + TOP; int[] lines = mLines; + final int dir = measured.getParagraphDir(); if (want >= lines.length) { - Directions[] grow2 = ArrayUtils.newUnpaddedArray( - Directions.class, GrowingArrayUtils.growSize(want)); - System.arraycopy(mLineDirections, 0, grow2, 0, - mLineDirections.length); - mLineDirections = grow2; - - int[] grow = new int[grow2.length]; + final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want)); System.arraycopy(lines, 0, grow, 0, lines.length); mLines = grow; lines = grow; } - if (chooseHt != null) { - fm.ascent = above; - fm.descent = below; - fm.top = top; - fm.bottom = bottom; + if (j >= mLineDirections.length) { + final Directions[] grow = ArrayUtils.newUnpaddedArray(Directions.class, + GrowingArrayUtils.growSize(j)); + System.arraycopy(mLineDirections, 0, grow, 0, mLineDirections.length); + mLineDirections = grow; + } - for (int i = 0; i < chooseHt.length; i++) { - if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { - ((LineHeightSpan.WithDensity) chooseHt[i]). - chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); + lines[off + START] = start; + lines[off + TOP] = v; - } else { - chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); - } - } + // Information about hyphenation, tabs, and directions are needed for determining + // ellipsization, so the values should be assigned before ellipsization. - above = fm.ascent; - below = fm.descent; - top = fm.top; - bottom = fm.bottom; - } + // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining + // one bit for start field + lines[off + TAB] |= flags & TAB_MASK; + lines[off + HYPHEN] = flags; + lines[off + DIR] |= dir << DIR_SHIFT; + mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); - boolean firstLine = (j == 0); - boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); + final boolean firstLine = (j == 0); + final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); if (ellipsize != null) { // If there is only one line, then do any type of ellipsis except when it is MARQUEE @@ -963,13 +976,48 @@ public class StaticLayout extends Layout { (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && ellipsize == TextUtils.TruncateAt.END); if (doEllipsis) { - calculateEllipsis(start, end, widths, widthStart, - ellipsisWidth, ellipsize, j, - textWidth, paint, forceEllipsis); + calculateEllipsis(text, start, end, widths, widthStart, + ellipsisWidth - getTotalInsets(j), ellipsize, j, + textWidth, paint, forceEllipsis, dir); } } - boolean lastLine = mEllipsized || (end == bufEnd); + final boolean lastLine; + if (mEllipsized) { + lastLine = true; + } else { + final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0 + && text.charAt(bufEnd - 1) == CHAR_NEW_LINE; + if (end == bufEnd && !lastCharIsNewLine) { + lastLine = true; + } else if (start == bufEnd && lastCharIsNewLine) { + lastLine = true; + } else { + lastLine = false; + } + } + + if (chooseHt != null) { + fm.ascent = above; + fm.descent = below; + fm.top = top; + fm.bottom = bottom; + + for (int i = 0; i < chooseHt.length; i++) { + if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { + ((LineHeightSpan.WithDensity) chooseHt[i]) + .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); + + } else { + chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); + } + } + + above = fm.ascent; + below = fm.descent; + top = fm.top; + bottom = fm.bottom; + } if (firstLine) { if (trackPad) { @@ -981,8 +1029,6 @@ public class StaticLayout extends Layout { } } - int extra; - if (lastLine) { if (trackPad) { mBottomPadding = bottom - below; @@ -993,8 +1039,9 @@ public class StaticLayout extends Layout { } } - if (needMultiply && !lastLine) { - double ex = (below - above) * (spacingmult - 1) + spacingadd; + final int extra; + if (needMultiply && (addLastLineLineSpacing || !lastLine)) { + final double ex = (below - above) * (spacingmult - 1) + spacingadd; if (ex >= 0) { extra = (int)(ex + EXTRA_ROUNDING); } else { @@ -1004,15 +1051,14 @@ public class StaticLayout extends Layout { extra = 0; } - lines[off + START] = start; - lines[off + TOP] = v; lines[off + DESCENT] = below + extra; + lines[off + EXTRA] = extra; // special case for non-ellipsized last visible line when maxLines is set // store the height as if it was ellipsized if (!mEllipsized && currentLineIsTheLastVisibleOne) { // below calculation as if it was the last line - int maxLineBelow = includePad ? bottom : below; + final int maxLineBelow = includePad ? bottom : below; // similar to the calculation of v below, without the extra. mMaxLineHeight = v + (maxLineBelow - above); } @@ -1021,33 +1067,13 @@ public class StaticLayout extends Layout { lines[off + mColumns + START] = end; lines[off + mColumns + TOP] = v; - // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining - // one bit for start field - lines[off + TAB] |= flags & TAB_MASK; - lines[off + HYPHEN] = flags; - - lines[off + DIR] |= dir << DIR_SHIFT; - Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; - // easy means all chars < the first RTL, so no emoji, no nothing - // XXX a run with no text or all spaces is easy but might be an empty - // RTL paragraph. Make sure easy is false if this is the case. - if (easy) { - mLineDirections[j] = linedirs; - } else { - mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, - start - widthStart, end - start); - } - mLineCount++; return v; } - private void calculateEllipsis(int lineStart, int lineEnd, - float[] widths, int widthStart, - float avail, TextUtils.TruncateAt where, - int line, float textWidth, TextPaint paint, - boolean forceEllipsis) { - avail -= getTotalInsets(line); + private void calculateEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths, + int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth, + TextPaint paint, boolean forceEllipsis, int dir) { if (textWidth <= avail && !forceEllipsis) { // Everything fits! mLines[mColumns * line + ELLIPSIS_START] = 0; @@ -1055,13 +1081,53 @@ public class StaticLayout extends Layout { return; } - float ellipsisWidth = paint.measureText( - (where == TextUtils.TruncateAt.END_SMALL) ? - TextUtils.ELLIPSIS_TWO_DOTS : TextUtils.ELLIPSIS_NORMAL, 0, 1); - int ellipsisStart = 0; - int ellipsisCount = 0; - int len = lineEnd - lineStart; + float tempAvail = avail; + int numberOfTries = 0; + boolean lineFits = false; + mWorkPaint.set(paint); + do { + final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths, + widthStart, tempAvail, where, line, mWorkPaint, forceEllipsis, dir); + if (ellipsizedWidth <= avail) { + lineFits = true; + } else { + numberOfTries++; + if (numberOfTries > 10) { + // If the text still doesn't fit after ten tries, assume it will never fit and + // ellipsize it all. + mLines[mColumns * line + ELLIPSIS_START] = 0; + mLines[mColumns * line + ELLIPSIS_COUNT] = lineEnd - lineStart; + lineFits = true; + } else { + // Some side effect of ellipsization has caused the text to go over the + // available width. Let's make the available width shorter by exactly that + // amount and retry. + tempAvail -= ellipsizedWidth - avail; + } + } + } while (!lineFits); + mEllipsized = true; + } + // Returns the width of the ellipsized line which in some rare cases can actually be larger + // than 'avail' (due to kerning or other context-based effect of replacement of text by + // ellipsis). If all the line needs to ellipsized away, or it's an invalud hyphenation mode, + // returns 0 so the caller can stop iterating. + // + // This method temporarily modifies the TextPaint passed to it, so the TextPaint passed to it + // should not be accessed while the method is running. + private float guessEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths, + int widthStart, float avail, TextUtils.TruncateAt where, int line, + TextPaint paint, boolean forceEllipsis, int dir) { + final int savedHyphenEdit = paint.getHyphenEdit(); + paint.setHyphenEdit(0); + final float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where)); + final int ellipsisStart; + final int ellipsisCount; + final int len = lineEnd - lineStart; + final int offset = lineStart - widthStart; + + int hyphen = getHyphen(line); // We only support start ellipsis on a single line if (where == TextUtils.TruncateAt.START) { if (mMaximumVisibleLineCount == 1) { @@ -1069,9 +1135,9 @@ public class StaticLayout extends Layout { int i; for (i = len; i > 0; i--) { - float w = widths[i - 1 + lineStart - widthStart]; + final float w = widths[i - 1 + offset]; if (w + sum + ellipsisWidth > avail) { - while (i < len && widths[i + lineStart - widthStart] == 0.0f) { + while (i < len && widths[i + offset] == 0.0f) { i++; } break; @@ -1082,9 +1148,13 @@ public class StaticLayout extends Layout { ellipsisStart = 0; ellipsisCount = i; + // Strip the potential hyphenation at beginning of line. + hyphen &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE; } else { + ellipsisStart = 0; + ellipsisCount = 0; if (Log.isLoggable(TAG, Log.WARN)) { - Log.w(TAG, "Start Ellipsis only supported with one line"); + Log.w(TAG, "Start ellipsis only supported with one line"); } } } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || @@ -1093,7 +1163,7 @@ public class StaticLayout extends Layout { int i; for (i = 0; i < len; i++) { - float w = widths[i + lineStart - widthStart]; + final float w = widths[i + offset]; if (w + sum + ellipsisWidth > avail) { break; @@ -1102,24 +1172,27 @@ public class StaticLayout extends Layout { sum += w; } - ellipsisStart = i; - ellipsisCount = len - i; - if (forceEllipsis && ellipsisCount == 0 && len > 0) { + if (forceEllipsis && i == len && len > 0) { ellipsisStart = len - 1; ellipsisCount = 1; + } else { + ellipsisStart = i; + ellipsisCount = len - i; } - } else { - // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line + // Strip the potential hyphenation at end of line. + hyphen &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE; + } else { // where = TextUtils.TruncateAt.MIDDLE + // We only support middle ellipsis on a single line. if (mMaximumVisibleLineCount == 1) { float lsum = 0, rsum = 0; int left = 0, right = len; - float ravail = (avail - ellipsisWidth) / 2; + final float ravail = (avail - ellipsisWidth) / 2; for (right = len; right > 0; right--) { - float w = widths[right - 1 + lineStart - widthStart]; + final float w = widths[right - 1 + offset]; if (w + rsum > ravail) { - while (right < len && widths[right + lineStart - widthStart] == 0.0f) { + while (right < len && widths[right + offset] == 0.0f) { right++; } break; @@ -1127,9 +1200,9 @@ public class StaticLayout extends Layout { rsum += w; } - float lavail = avail - ellipsisWidth - rsum; + final float lavail = avail - ellipsisWidth - rsum; for (left = 0; left < right; left++) { - float w = widths[left + lineStart - widthStart]; + final float w = widths[left + offset]; if (w + lsum > lavail) { break; @@ -1141,14 +1214,53 @@ public class StaticLayout extends Layout { ellipsisStart = left; ellipsisCount = right - left; } else { + ellipsisStart = 0; + ellipsisCount = 0; if (Log.isLoggable(TAG, Log.WARN)) { - Log.w(TAG, "Middle Ellipsis only supported with one line"); + Log.w(TAG, "Middle ellipsis only supported with one line"); } } } - mEllipsized = true; mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; + + if (ellipsisStart == 0 && (ellipsisCount == 0 || ellipsisCount == len)) { + // Unsupported ellipsization mode or all text is ellipsized away. Return 0. + return 0.0f; + } + + final boolean isSpanned = text instanceof Spanned; + final Ellipsizer ellipsizedText = isSpanned + ? new SpannedEllipsizer(text) + : new Ellipsizer(text); + ellipsizedText.mLayout = this; + ellipsizedText.mMethod = where; + + final boolean hasTabs = getLineContainsTab(line); + final TabStops tabStops; + if (hasTabs && isSpanned) { + final TabStopSpan[] tabs = getParagraphSpans((Spanned) ellipsizedText, lineStart, + lineEnd, TabStopSpan.class); + if (tabs.length == 0) { + tabStops = null; + } else { + tabStops = new TabStops(TAB_INCREMENT, tabs); + } + } else { + tabStops = null; + } + paint.setHyphenEdit(hyphen); + final TextLine textline = TextLine.obtain(); + textline.set(paint, ellipsizedText, lineStart, lineEnd, dir, getLineDirections(line), + hasTabs, tabStops); + // Since TextLine.metric() returns negative values for RTL text, multiplication by dir + // converts it to an actual width. Note that we don't want to use the absolute value, + // since we may actually have glyphs with negative advances, which by definition always + // fit. + final float ellipsizedWidth = textline.metrics(null) * dir; + TextLine.recycle(textline); + paint.setHyphenEdit(savedHyphenEdit); + return ellipsizedWidth; } private float getTotalInsets(int line) { @@ -1197,6 +1309,14 @@ public class StaticLayout extends Layout { return mLines[mColumns * line + TOP]; } + /** + * @hide + */ + @Override + public int getLineExtra(int line) { + return mLines[mColumns * line + EXTRA]; + } + @Override public int getLineDescent(int line) { return mLines[mColumns * line + DESCENT]; @@ -1219,6 +1339,9 @@ public class StaticLayout extends Layout { @Override public final Directions getLineDirections(int line) { + if (line > getLineCount()) { + throw new ArrayIndexOutOfBoundsException(); + } return mLineDirections[line]; } @@ -1314,41 +1437,48 @@ public class StaticLayout extends Layout { mMaxLineHeight : super.getHeight(); } - private static native long nNewBuilder(); - private static native void nFreeBuilder(long nativePtr); - private static native void nFinishBuilder(long nativePtr); - - /* package */ static native long nLoadHyphenator(ByteBuffer buf, int offset, - int minPrefix, int minSuffix); - - private static native void nSetLocales(long nativePtr, String locales, - long[] nativeHyphenators); + @FastNative + private static native long nInit( + @BreakStrategy int breakStrategy, + @HyphenationFrequency int hyphenationFrequency, + boolean isJustified, + @Nullable int[] indents, + @Nullable int[] leftPaddings, + @Nullable int[] rightPaddings); - private static native void nSetIndents(long nativePtr, int[] indents); - - // Set up paragraph text and settings; done as one big method to minimize jni crossings - private static native void nSetupParagraph(long nativePtr, char[] text, int length, - float firstWidth, int firstWidthLineCount, float restWidth, - int[] variableTabStops, int defaultTabStop, int breakStrategy, int hyphenationFrequency, - boolean isJustified); - - private static native float nAddStyleRun(long nativePtr, long nativePaint, - long nativeTypeface, int start, int end, boolean isRtl); - - private static native void nAddMeasuredRun(long nativePtr, - int start, int end, float[] widths); - - private static native void nAddReplacementRun(long nativePtr, int start, int end, float width); - - private static native void nGetWidths(long nativePtr, float[] widths); + @CriticalNative + private static native void nFinish(long nativePtr); // populates LineBreaks and returns the number of breaks found // // the arrays inside the LineBreaks objects are passed in as well // to reduce the number of JNI calls in the common case where the // arrays do not have to be resized - private static native int nComputeLineBreaks(long nativePtr, LineBreaks recycle, - int[] recycleBreaks, float[] recycleWidths, int[] recycleFlags, int recycleLength); + // The individual character widths will be returned in charWidths. The length of charWidths must + // be at least the length of the text. + private static native int nComputeLineBreaks( + /* non zero */ long nativePtr, + + // Inputs + @NonNull char[] text, + /* Non Zero */ long measuredTextPtr, + @IntRange(from = 0) int length, + @FloatRange(from = 0.0f) float firstWidth, + @IntRange(from = 0) int firstWidthLineCount, + @FloatRange(from = 0.0f) float restWidth, + @Nullable int[] variableTabStops, + int defaultTabStop, + @IntRange(from = 0) int indentsOffset, + + // Outputs + @NonNull LineBreaks recycle, + @IntRange(from = 0) int recycleLength, + @NonNull int[] recycleBreaks, + @NonNull float[] recycleWidths, + @NonNull float[] recycleAscents, + @NonNull float[] recycleDescents, + @NonNull int[] recycleFlags, + @NonNull float[] charWidths); private int mLineCount; private int mTopPadding, mBottomPadding; @@ -1370,16 +1500,19 @@ public class StaticLayout extends Layout { */ private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT; - private static final int COLUMNS_NORMAL = 4; - private static final int COLUMNS_ELLIPSIZE = 6; + private TextPaint mWorkPaint = new TextPaint(); + + private static final int COLUMNS_NORMAL = 5; + private static final int COLUMNS_ELLIPSIZE = 7; private static final int START = 0; private static final int DIR = START; private static final int TAB = START; private static final int TOP = 1; private static final int DESCENT = 2; - private static final int HYPHEN = 3; - private static final int ELLIPSIS_START = 4; - private static final int ELLIPSIS_COUNT = 5; + private static final int EXTRA = 3; + private static final int HYPHEN = 4; + private static final int ELLIPSIS_START = 5; + private static final int ELLIPSIS_COUNT = 6; private int[] mLines; private Directions[] mLineDirections; @@ -1404,10 +1537,14 @@ public class StaticLayout extends Layout { private static final int INITIAL_SIZE = 16; public int[] breaks = new int[INITIAL_SIZE]; public float[] widths = new float[INITIAL_SIZE]; + public float[] ascents = new float[INITIAL_SIZE]; + public float[] descents = new float[INITIAL_SIZE]; public int[] flags = new int[INITIAL_SIZE]; // hasTab // breaks, widths, and flags should all have the same length } - private int[] mLeftIndents; - private int[] mRightIndents; + @Nullable private int[] mLeftIndents; + @Nullable private int[] mRightIndents; + @Nullable private int[] mLeftPaddings; + @Nullable private int[] mRightPaddings; } diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 2dbff100375a..86cc0141b0a4 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -28,6 +28,7 @@ import android.text.style.MetricAffectingSpan; import android.text.style.ReplacementSpan; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import java.util.ArrayList; @@ -44,7 +45,8 @@ import java.util.ArrayList; * * @hide */ -class TextLine { +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public class TextLine { private static final boolean DEBUG = false; private TextPaint mPaint; @@ -73,7 +75,7 @@ class TextLine { new SpanSet<ReplacementSpan>(ReplacementSpan.class); private final DecorationInfo mDecorationInfo = new DecorationInfo(); - private final ArrayList<DecorationInfo> mDecorations = new ArrayList(); + private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>(); private static final TextLine[] sCached = new TextLine[3]; @@ -82,7 +84,8 @@ class TextLine { * * @return an uninitialized TextLine */ - static TextLine obtain() { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public static TextLine obtain() { TextLine tl; synchronized (sCached) { for (int i = sCached.length; --i >= 0;) { @@ -107,7 +110,8 @@ class TextLine { * @return null, as a convenience from clearing references to the provided * TextLine */ - static TextLine recycle(TextLine tl) { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public static TextLine recycle(TextLine tl) { tl.mText = null; tl.mPaint = null; tl.mDirections = null; @@ -142,7 +146,8 @@ class TextLine { * @param hasTabs true if the line might contain tabs * @param tabStops the tabStops. Can be null. */ - void set(TextPaint paint, CharSequence text, int start, int limit, int dir, + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void set(TextPaint paint, CharSequence text, int start, int limit, int dir, Directions directions, boolean hasTabs, TabStops tabStops) { mPaint = paint; mText = text; @@ -196,7 +201,8 @@ class TextLine { /** * Justify the line to the given width. */ - void justify(float justifyWidth) { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void justify(float justifyWidth) { int end = mLen; while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) { end--; @@ -277,7 +283,8 @@ class TextLine { * @param fmi receives font metrics information, can be null * @return the signed width of the line */ - float metrics(FontMetricsInt fmi) { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public float metrics(FontMetricsInt fmi) { return measure(mLen, false, fmi); } @@ -340,14 +347,14 @@ class TextLine { boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; if (inSegment && advance) { - return h += measureRun(segstart, offset, j, runIsRtl, fmi); + return h + measureRun(segstart, offset, j, runIsRtl, fmi); } float w = measureRun(segstart, j, j, runIsRtl, fmi); h += advance ? w : -w; if (inSegment) { - return h += measureRun(segstart, offset, j, runIsRtl, null); + return h + measureRun(segstart, offset, j, runIsRtl, null); } if (codept == '\t') { @@ -828,14 +835,14 @@ class TextLine { } if (info.isUnderlineText) { final float thickness = - Math.max(((Paint) wp).getUnderlineThickness(), 1.0f); + Math.max(wp.getUnderlineThickness(), 1.0f); drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness, decorationXLeft, decorationXRight, y); } if (info.isStrikeThruText) { final float thickness = - Math.max(((Paint) wp).getStrikeThruThickness(), 1.0f); + Math.max(wp.getStrikeThruThickness(), 1.0f); drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness, decorationXLeft, decorationXRight, y); } @@ -1165,23 +1172,18 @@ class TextLine { } private boolean isStretchableWhitespace(int ch) { - // TODO: Support other stretchable whitespace. (Bug: 34013491) - return ch == 0x0020 || ch == 0x00A0; - } - - private int nextStretchableSpace(int start, int end) { - for (int i = start; i < end; i++) { - final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart); - if (isStretchableWhitespace(c)) return i; - } - return end; + // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709). + return ch == 0x0020; } /* Return the number of spaces in the text line, for the purpose of justification */ private int countStretchableSpaces(int start, int end) { int count = 0; - for (int i = start; i < end; i = nextStretchableSpace(i + 1, end)) { - count++; + for (int i = start; i < end; i++) { + final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart); + if (isStretchableWhitespace(c)) { + count++; + } } return count; } diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 3e64af47c276..9c9fbf23832f 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -17,6 +17,7 @@ package android.text; import android.annotation.FloatRange; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.PluralsRes; @@ -41,7 +42,6 @@ import android.text.style.EasyEditSpan; import android.text.style.ForegroundColorSpan; import android.text.style.LeadingMarginSpan; import android.text.style.LocaleSpan; -import android.text.style.MetricAffectingSpan; import android.text.style.ParagraphStyle; import android.text.style.QuoteSpan; import android.text.style.RelativeSizeSpan; @@ -77,12 +77,21 @@ import java.util.regex.Pattern; public class TextUtils { private static final String TAG = "TextUtils"; - /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..." + // Zero-width character used to fill ellipsized strings when codepoint lenght must be preserved. + /* package */ static final char ELLIPSIS_FILLER = '\uFEFF'; // ZERO WIDTH NO-BREAK SPACE + + // TODO: Based on CLDR data, these need to be localized for Dzongkha (dz) and perhaps + // Hong Kong Traditional Chinese (zh-Hant-HK), but that may need to depend on the actual word + // being ellipsized and not the locale. + private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…) + private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥) + /** {@hide} */ - public static final String ELLIPSIS_STRING = new String(ELLIPSIS_NORMAL); + @NonNull + public static String getEllipsisString(@NonNull TruncateAt method) { + return (method == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL; + } - /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".." - private static final String ELLIPSIS_TWO_DOTS_STRING = new String(ELLIPSIS_TWO_DOTS); private TextUtils() { /* cannot be instantiated */ } @@ -297,37 +306,46 @@ public class TextUtils { /** * Returns a string containing the tokens joined by delimiters. - * @param tokens an array objects to be joined. Strings will be formed from - * the objects by calling object.toString(). + * + * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string + * "null" will be used as the delimiter. + * @param tokens an array objects to be joined. Strings will be formed from the objects by + * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If + * tokens is an empty array, an empty string will be returned. */ - public static String join(CharSequence delimiter, Object[] tokens) { - StringBuilder sb = new StringBuilder(); - boolean firstTime = true; - for (Object token: tokens) { - if (firstTime) { - firstTime = false; - } else { - sb.append(delimiter); - } - sb.append(token); + public static String join(@NonNull CharSequence delimiter, @NonNull Object[] tokens) { + final int length = tokens.length; + if (length == 0) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + sb.append(tokens[0]); + for (int i = 1; i < length; i++) { + sb.append(delimiter); + sb.append(tokens[i]); } return sb.toString(); } /** * Returns a string containing the tokens joined by delimiters. - * @param tokens an array objects to be joined. Strings will be formed from - * the objects by calling object.toString(). + * + * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string + * "null" will be used as the delimiter. + * @param tokens an array objects to be joined. Strings will be formed from the objects by + * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If + * tokens is empty, an empty string will be returned. */ - public static String join(CharSequence delimiter, Iterable tokens) { - StringBuilder sb = new StringBuilder(); - Iterator<?> it = tokens.iterator(); - if (it.hasNext()) { + public static String join(@NonNull CharSequence delimiter, @NonNull Iterable tokens) { + final Iterator<?> it = tokens.iterator(); + if (!it.hasNext()) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + sb.append(it.next()); + while (it.hasNext()) { + sb.append(delimiter); sb.append(it.next()); - while (it.hasNext()) { - sb.append(delimiter); - sb.append(it.next()); - } } return sb.toString(); } @@ -1176,9 +1194,11 @@ public class TextUtils { * or, if it does not fit, a truncated * copy with ellipsis character added at the specified edge or center. */ - public static CharSequence ellipsize(CharSequence text, - TextPaint p, - float avail, TruncateAt where) { + @NonNull + public static CharSequence ellipsize(@NonNull CharSequence text, + @NonNull TextPaint p, + @FloatRange(from = 0.0) float avail, + @NonNull TruncateAt where) { return ellipsize(text, p, avail, where, false, null); } @@ -1194,14 +1214,16 @@ public class TextUtils { * report the start and end of the ellipsized range. TextDirection * is determined by the first strong directional character. */ - public static CharSequence ellipsize(CharSequence text, - TextPaint paint, - float avail, TruncateAt where, + @NonNull + public static CharSequence ellipsize(@NonNull CharSequence text, + @NonNull TextPaint paint, + @FloatRange(from = 0.0) float avail, + @NonNull TruncateAt where, boolean preserveLength, - EllipsizeCallback callback) { + @Nullable EllipsizeCallback callback) { return ellipsize(text, paint, avail, where, preserveLength, callback, TextDirectionHeuristics.FIRSTSTRONG_LTR, - (where == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS_STRING : ELLIPSIS_STRING); + getEllipsisString(where)); } /** @@ -1217,89 +1239,131 @@ public class TextUtils { * * @hide */ - public static CharSequence ellipsize(CharSequence text, - TextPaint paint, - float avail, TruncateAt where, + @NonNull + public static CharSequence ellipsize(@NonNull CharSequence text, + @NonNull TextPaint paint, + @FloatRange(from = 0.0) float avail, + @NonNull TruncateAt where, boolean preserveLength, - EllipsizeCallback callback, - TextDirectionHeuristic textDir, String ellipsis) { - - int len = text.length(); + @Nullable EllipsizeCallback callback, + @NonNull TextDirectionHeuristic textDir, + @NonNull String ellipsis) { - MeasuredText mt = MeasuredText.obtain(); + final int len = text.length(); + MeasuredText mt = null; + MeasuredText resultMt = null; try { - float width = setPara(mt, paint, text, 0, text.length(), textDir); + mt = MeasuredText.buildForMeasurement(paint, text, 0, text.length(), textDir, mt); + float width = mt.getWholeWidth(); if (width <= avail) { if (callback != null) { callback.ellipsized(0, 0); } - return text; } - // XXX assumes ellipsis string does not require shaping and - // is unaffected by style - float ellipsiswid = paint.measureText(ellipsis); - avail -= ellipsiswid; - - int left = 0; - int right = len; - if (avail < 0) { - // it all goes - } else if (where == TruncateAt.START) { - right = len - mt.breakText(len, false, avail); - } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) { - left = mt.breakText(len, true, avail); - } else { - right = len - mt.breakText(len, false, avail / 2); - avail -= mt.measure(right, len); - left = mt.breakText(right, true, avail); - } - - if (callback != null) { - callback.ellipsized(left, right); - } - - char[] buf = mt.mChars; - Spanned sp = text instanceof Spanned ? (Spanned) text : null; - - int remaining = len - (right - left); - if (preserveLength) { - if (remaining > 0) { // else eliminate the ellipsis too - buf[left++] = ellipsis.charAt(0); + // First estimate of effective width of ellipsis. + float ellipsisWidth = paint.measureText(ellipsis); + int numberOfTries = 0; + boolean textFits = false; + int start, end; + CharSequence result; + do { + if (avail < ellipsisWidth) { + // Even the ellipsis can't fit. So it all goes. + start = 0; + end = len; + } else { + final float remainingWidth = avail - ellipsisWidth; + if (where == TruncateAt.START) { + start = 0; + end = len - mt.breakText(len, false /* backwards */, remainingWidth); + } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) { + start = mt.breakText(len, true /* forwards */, remainingWidth); + end = len; + } else { + end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2); + start = mt.breakText(end, true /* forwards */, + remainingWidth - mt.measure(end, len)); + } } - for (int i = left; i < right; i++) { - buf[i] = ZWNBS_CHAR; + + final char[] buf = mt.getChars(); + final Spanned sp = text instanceof Spanned ? (Spanned) text : null; + + final int removed = end - start; + final int remaining = len - removed; + if (preserveLength) { + int pos = start; + if (remaining > 0 && removed >= ellipsis.length()) { + ellipsis.getChars(0, ellipsis.length(), buf, start); + pos += ellipsis.length(); + } // else eliminate the ellipsis + while (pos < end) { + buf[pos++] = ELLIPSIS_FILLER; + } + final String s = new String(buf, 0, len); + if (sp == null) { + result = s; + } else { + final SpannableString ss = new SpannableString(s); + copySpansFrom(sp, 0, len, Object.class, ss, 0); + result = ss; + } + } else { + if (remaining == 0) { + result = ""; + } else if (sp == null) { + final StringBuilder sb = new StringBuilder(remaining + ellipsis.length()); + sb.append(buf, 0, start); + sb.append(ellipsis); + sb.append(buf, end, len - end); + result = sb.toString(); + } else { + final SpannableStringBuilder ssb = new SpannableStringBuilder(); + ssb.append(text, 0, start); + ssb.append(ellipsis); + ssb.append(text, end, len); + result = ssb; + } } - String s = new String(buf, 0, len); - if (sp == null) { - return s; + + if (remaining == 0) { // All text is gone. + textFits = true; + } else { + resultMt = MeasuredText.buildForMeasurement( + paint, result, 0, result.length(), textDir, resultMt); + width = resultMt.getWholeWidth(); + if (width <= avail) { + textFits = true; + } else { + numberOfTries++; + if (numberOfTries > 10) { + // If the text still doesn't fit after ten tries, assume it will never + // fit and ellipsize it all. We do this by setting the width of the + // ellipsis to be positive infinity, so we get to empty text in the next + // round. + ellipsisWidth = Float.POSITIVE_INFINITY; + } else { + // Adjust the width of the ellipsis by adding the amount 'width' is + // still over. + ellipsisWidth += width - avail; + } + } } - SpannableString ss = new SpannableString(s); - copySpansFrom(sp, 0, len, Object.class, ss, 0); - return ss; + } while (!textFits); + if (callback != null) { + callback.ellipsized(start, end); } - - if (remaining == 0) { - return ""; + return result; + } finally { + if (mt != null) { + mt.recycle(); } - - if (sp == null) { - StringBuilder sb = new StringBuilder(remaining + ellipsis.length()); - sb.append(buf, 0, left); - sb.append(ellipsis); - sb.append(buf, right, len - right); - return sb.toString(); + if (resultMt != null) { + resultMt.recycle(); } - - SpannableStringBuilder ssb = new SpannableStringBuilder(); - ssb.append(text, 0, left); - ssb.append(ellipsis); - ssb.append(text, right, len); - return ssb; - } finally { - MeasuredText.recycle(mt); } } @@ -1330,7 +1394,6 @@ public class TextUtils { * @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"}) * doesn't fit, it will return an empty string. */ - public static CharSequence listEllipsize(@Nullable Context context, @Nullable List<CharSequence> elements, @NonNull String separator, @NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail, @@ -1370,7 +1433,7 @@ public class TextUtils { final int remainingElements = totalLen - i - 1; if (remainingElements > 0) { CharSequence morePiece = (res == null) ? - ELLIPSIS_STRING : + ELLIPSIS_NORMAL : res.getQuantityString(moreId, remainingElements, remainingElements); morePiece = bidiFormatter.unicodeWrap(morePiece); output.append(morePiece); @@ -1416,15 +1479,17 @@ public class TextUtils { public static CharSequence commaEllipsize(CharSequence text, TextPaint p, float avail, String oneMore, String more, TextDirectionHeuristic textDir) { - MeasuredText mt = MeasuredText.obtain(); + MeasuredText mt = null; + MeasuredText tempMt = null; try { int len = text.length(); - float width = setPara(mt, p, text, 0, len, textDir); + mt = MeasuredText.buildForMeasurement(p, text, 0, len, textDir, mt); + final float width = mt.getWholeWidth(); if (width <= avail) { return text; } - char[] buf = mt.mChars; + char[] buf = mt.getChars(); int commaCount = 0; for (int i = 0; i < len; i++) { @@ -1440,9 +1505,8 @@ public class TextUtils { int w = 0; int count = 0; - float[] widths = mt.mWidths; + float[] widths = mt.getWidths().getRawArray(); - MeasuredText tempMt = MeasuredText.obtain(); for (int i = 0; i < len; i++) { w += widths[i]; @@ -1459,8 +1523,9 @@ public class TextUtils { } // XXX this is probably ok, but need to look at it more - tempMt.setPara(format, 0, format.length(), textDir, null); - float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null); + tempMt = MeasuredText.buildForMeasurement( + p, format, 0, format.length(), textDir, tempMt); + float moreWid = tempMt.getWholeWidth(); if (w + moreWid <= avail) { ok = i + 1; @@ -1468,40 +1533,18 @@ public class TextUtils { } } } - MeasuredText.recycle(tempMt); SpannableStringBuilder out = new SpannableStringBuilder(okFormat); out.insert(0, text, 0, ok); return out; } finally { - MeasuredText.recycle(mt); - } - } - - private static float setPara(MeasuredText mt, TextPaint paint, - CharSequence text, int start, int end, TextDirectionHeuristic textDir) { - - mt.setPara(text, start, end, textDir, null); - - float width; - Spanned sp = text instanceof Spanned ? (Spanned) text : null; - int len = end - start; - if (sp == null) { - width = mt.addStyleRun(paint, len, null); - } else { - width = 0; - int spanEnd; - for (int spanStart = 0; spanStart < len; spanStart = spanEnd) { - spanEnd = sp.nextSpanTransition(spanStart, len, - MetricAffectingSpan.class); - MetricAffectingSpan[] spans = sp.getSpans( - spanStart, spanEnd, MetricAffectingSpan.class); - spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class); - width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); + if (mt != null) { + mt.recycle(); + } + if (tempMt != null) { + tempMt.recycle(); } } - - return width; } // Returns true if the character's presence could affect RTL layout. @@ -2032,11 +2075,48 @@ public class TextUtils { builder.append(end); } + /** + * Intent size limitations prevent sending over a megabyte of data. Limit + * text length to 100K characters - 200KB. + */ + private static final int PARCEL_SAFE_TEXT_LENGTH = 100000; + + /** + * Trims the text to {@link #PARCEL_SAFE_TEXT_LENGTH} length. Returns the string as it is if + * the length() is smaller than {@link #PARCEL_SAFE_TEXT_LENGTH}. Used for text that is parceled + * into a {@link Parcelable}. + * + * @hide + */ + @Nullable + public static <T extends CharSequence> T trimToParcelableSize(@Nullable T text) { + return trimToSize(text, PARCEL_SAFE_TEXT_LENGTH); + } + + /** + * Trims the text to {@code size} length. Returns the string as it is if the length() is + * smaller than {@code size}. If chars at {@code size-1} and {@code size} is a surrogate + * pair, returns a CharSequence of length {@code size-1}. + * + * @param size length of the result, should be greater than 0 + * + * @hide + */ + @Nullable + public static <T extends CharSequence> T trimToSize(@Nullable T text, + @IntRange(from = 1) int size) { + Preconditions.checkArgument(size > 0); + if (TextUtils.isEmpty(text) || text.length() <= size) return text; + if (Character.isHighSurrogate(text.charAt(size - 1)) + && Character.isLowSurrogate(text.charAt(size))) { + size = size - 1; + } + return (T) text.subSequence(0, size); + } + private static Object sLock = new Object(); private static char[] sTemp = null; private static String[] EMPTY_STRING_ARRAY = new String[]{}; - - private static final char ZWNBS_CHAR = '\uFEFF'; } diff --git a/core/java/android/text/format/DateFormat.java b/core/java/android/text/format/DateFormat.java index b5a8acaf674f..de2dcce5bc5d 100755 --- a/core/java/android/text/format/DateFormat.java +++ b/core/java/android/text/format/DateFormat.java @@ -16,6 +16,7 @@ package android.text.format; +import android.annotation.NonNull; import android.content.Context; import android.os.UserHandle; import android.provider.Settings; @@ -177,43 +178,47 @@ public class DateFormat { * @hide */ public static boolean is24HourFormat(Context context, int userHandle) { - String value = Settings.System.getStringForUser(context.getContentResolver(), + final String value = Settings.System.getStringForUser(context.getContentResolver(), Settings.System.TIME_12_24, userHandle); + if (value != null) { + return value.equals("24"); + } - if (value == null) { - Locale locale = context.getResources().getConfiguration().locale; + return is24HourLocale(context.getResources().getConfiguration().locale); + } - synchronized (sLocaleLock) { - if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) { - return sIs24Hour; - } + /** + * Returns true if the specified locale uses a 24-hour time format by default, ignoring user + * settings. + * @param locale the locale to check + * @return true if the locale uses a 24 hour time format by default, false otherwise + * @hide + */ + public static boolean is24HourLocale(@NonNull Locale locale) { + synchronized (sLocaleLock) { + if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) { + return sIs24Hour; } + } - java.text.DateFormat natural = - java.text.DateFormat.getTimeInstance(java.text.DateFormat.LONG, locale); - - if (natural instanceof SimpleDateFormat) { - SimpleDateFormat sdf = (SimpleDateFormat) natural; - String pattern = sdf.toPattern(); + final java.text.DateFormat natural = + java.text.DateFormat.getTimeInstance(java.text.DateFormat.LONG, locale); - if (pattern.indexOf('H') >= 0) { - value = "24"; - } else { - value = "12"; - } - } else { - value = "12"; - } - - synchronized (sLocaleLock) { - sIs24HourLocale = locale; - sIs24Hour = value.equals("24"); - } + final boolean is24Hour; + if (natural instanceof SimpleDateFormat) { + final SimpleDateFormat sdf = (SimpleDateFormat) natural; + final String pattern = sdf.toPattern(); + is24Hour = hasDesignator(pattern, 'H'); + } else { + is24Hour = false; + } - return sIs24Hour; + synchronized (sLocaleLock) { + sIs24HourLocale = locale; + sIs24Hour = is24Hour; } - return value.equals("24"); + return is24Hour; } /** @@ -249,17 +254,18 @@ public class DateFormat { /** * Returns a {@link java.text.DateFormat} object that can format the time according - * to the current locale and the user's 12-/24-hour clock preference. + * to the context's locale and the user's 12-/24-hour clock preference. * @param context the application context * @return the {@link java.text.DateFormat} object that properly formats the time. */ public static java.text.DateFormat getTimeFormat(Context context) { - return new java.text.SimpleDateFormat(getTimeFormatString(context)); + final Locale locale = context.getResources().getConfiguration().locale; + return new java.text.SimpleDateFormat(getTimeFormatString(context), locale); } /** * Returns a String pattern that can be used to format the time according - * to the current locale and the user's 12-/24-hour clock preference. + * to the context's locale and the user's 12-/24-hour clock preference. * @param context the application context * @hide */ @@ -269,45 +275,48 @@ public class DateFormat { /** * Returns a String pattern that can be used to format the time according - * to the current locale and the user's 12-/24-hour clock preference. + * to the context's locale and the user's 12-/24-hour clock preference. * @param context the application context * @param userHandle the user handle of the user to query the format for * @hide */ public static String getTimeFormatString(Context context, int userHandle) { - LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale); + final LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale); return is24HourFormat(context, userHandle) ? d.timeFormat_Hm : d.timeFormat_hm; } /** * Returns a {@link java.text.DateFormat} object that can format the date - * in short form according to the current locale. + * in short form according to the context's locale. * * @param context the application context * @return the {@link java.text.DateFormat} object that properly formats the date. */ public static java.text.DateFormat getDateFormat(Context context) { - return java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT); + final Locale locale = context.getResources().getConfiguration().locale; + return java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT, locale); } /** * Returns a {@link java.text.DateFormat} object that can format the date - * in long form (such as {@code Monday, January 3, 2000}) for the current locale. + * in long form (such as {@code Monday, January 3, 2000}) for the context's locale. * @param context the application context * @return the {@link java.text.DateFormat} object that formats the date in long form. */ public static java.text.DateFormat getLongDateFormat(Context context) { - return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG); + final Locale locale = context.getResources().getConfiguration().locale; + return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG, locale); } /** * Returns a {@link java.text.DateFormat} object that can format the date - * in medium form (such as {@code Jan 3, 2000}) for the current locale. + * in medium form (such as {@code Jan 3, 2000}) for the context's locale. * @param context the application context * @return the {@link java.text.DateFormat} object that formats the date in long form. */ public static java.text.DateFormat getMediumDateFormat(Context context) { - return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM); + final Locale locale = context.getResources().getConfiguration().locale; + return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM, locale); } /** @@ -320,11 +329,13 @@ public class DateFormat { * order returned here. */ public static char[] getDateFormatOrder(Context context) { - return ICU.getDateFormatOrder(getDateFormatString()); + return ICU.getDateFormatOrder(getDateFormatString(context)); } - private static String getDateFormatString() { - java.text.DateFormat df = java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT); + private static String getDateFormatString(Context context) { + final Locale locale = context.getResources().getConfiguration().locale; + java.text.DateFormat df = java.text.DateFormat.getDateInstance( + java.text.DateFormat.SHORT, locale); if (df instanceof SimpleDateFormat) { return ((SimpleDateFormat) df).toPattern(); } @@ -375,6 +386,9 @@ public class DateFormat { * Test if a format string contains the given designator. Always returns * {@code false} if the input format is {@code null}. * + * Note that this is intended for searching for designators, not arbitrary + * characters. So searching for a literal single quote would not work correctly. + * * @hide */ public static boolean hasDesignator(CharSequence inFormat, char designator) { @@ -382,50 +396,19 @@ public class DateFormat { final int length = inFormat.length(); - int c; - int count; - - for (int i = 0; i < length; i += count) { - count = 1; - c = inFormat.charAt(i); - - if (c == QUOTE) { - count = skipQuotedText(inFormat, i, length); - } else if (c == designator) { - return true; - } - } - - return false; - } - - private static int skipQuotedText(CharSequence s, int i, int len) { - if (i + 1 < len && s.charAt(i + 1) == QUOTE) { - return 2; - } - - int count = 1; - // skip leading quote - i++; - - while (i < len) { - char c = s.charAt(i); - + boolean insideQuote = false; + for (int i = 0; i < length; i++) { + final char c = inFormat.charAt(i); if (c == QUOTE) { - count++; - // QUOTEQUOTE -> QUOTE - if (i + 1 < len && s.charAt(i + 1) == QUOTE) { - i++; - } else { - break; + insideQuote = !insideQuote; + } else if (!insideQuote) { + if (c == designator) { + return true; } - } else { - i++; - count++; } } - return count; + return false; } /** diff --git a/core/java/android/text/format/Formatter.java b/core/java/android/text/format/Formatter.java index 2c83fc4d9049..8c90156d159d 100644 --- a/core/java/android/text/format/Formatter.java +++ b/core/java/android/text/format/Formatter.java @@ -355,21 +355,21 @@ public final class Formatter { final Locale locale = localeFromContext(context); final MeasureFormat measureFormat = MeasureFormat.getInstance( locale, MeasureFormat.FormatWidth.SHORT); - if (days >= 2) { + if (days >= 2 || (days > 0 && hours == 0)) { days += (hours+12)/24; return measureFormat.format(new Measure(days, MeasureUnit.DAY)); } else if (days > 0) { return measureFormat.formatMeasures( new Measure(days, MeasureUnit.DAY), new Measure(hours, MeasureUnit.HOUR)); - } else if (hours >= 2) { + } else if (hours >= 2 || (hours > 0 && minutes == 0)) { hours += (minutes+30)/60; return measureFormat.format(new Measure(hours, MeasureUnit.HOUR)); } else if (hours > 0) { return measureFormat.formatMeasures( new Measure(hours, MeasureUnit.HOUR), new Measure(minutes, MeasureUnit.MINUTE)); - } else if (minutes >= 2) { + } else if (minutes >= 2 || (minutes > 0 && seconds == 0)) { minutes += (seconds+30)/60; return measureFormat.format(new Measure(minutes, MeasureUnit.MINUTE)); } else if (minutes > 0) { diff --git a/core/java/android/text/style/BulletSpan.java b/core/java/android/text/style/BulletSpan.java index 74084154ec60..43dd0ff52e49 100644 --- a/core/java/android/text/style/BulletSpan.java +++ b/core/java/android/text/style/BulletSpan.java @@ -31,7 +31,8 @@ public class BulletSpan implements LeadingMarginSpan, ParcelableSpan { private final boolean mWantColor; private final int mColor; - private static final int BULLET_RADIUS = 3; + // Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices. + private static final float BULLET_RADIUS = 3 * 1.2f; private static Path sBulletPath = null; public static final int STANDARD_GAP_WIDTH = 2; @@ -59,34 +60,41 @@ public class BulletSpan implements LeadingMarginSpan, ParcelableSpan { mColor = src.readInt(); } + @Override public int getSpanTypeId() { return getSpanTypeIdInternal(); } /** @hide */ + @Override public int getSpanTypeIdInternal() { return TextUtils.BULLET_SPAN; } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel dest, int flags) { writeToParcelInternal(dest, flags); } /** @hide */ + @Override public void writeToParcelInternal(Parcel dest, int flags) { dest.writeInt(mGapWidth); dest.writeInt(mWantColor ? 1 : 0); dest.writeInt(mColor); } + @Override public int getLeadingMargin(boolean first) { - return 2 * BULLET_RADIUS + mGapWidth; + return (int) (2 * BULLET_RADIUS + mGapWidth); } + @Override public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, @@ -102,19 +110,28 @@ public class BulletSpan implements LeadingMarginSpan, ParcelableSpan { p.setStyle(Paint.Style.FILL); + if (l != null) { + // "bottom" position might include extra space as a result of line spacing + // configuration. Subtract extra space in order to show bullet in the vertical + // center of characters. + final int line = l.getLineForOffset(start); + bottom = bottom - l.getLineExtra(line); + } + + final float y = (top + bottom) / 2f; + if (c.isHardwareAccelerated()) { if (sBulletPath == null) { sBulletPath = new Path(); - // Bullet is slightly better to avoid aliasing artifacts on mdpi devices. - sBulletPath.addCircle(0.0f, 0.0f, 1.2f * BULLET_RADIUS, Direction.CW); + sBulletPath.addCircle(0.0f, 0.0f, BULLET_RADIUS, Direction.CW); } c.save(); - c.translate(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f); + c.translate(x + dir * BULLET_RADIUS, y); c.drawPath(sBulletPath, p); c.restore(); } else { - c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f, BULLET_RADIUS, p); + c.drawCircle(x + dir * BULLET_RADIUS, y, BULLET_RADIUS, p); } if (mWantColor) { diff --git a/core/java/android/util/AndroidException.java b/core/java/android/util/AndroidException.java index dfe00c9bd49f..1345ddf189e1 100644 --- a/core/java/android/util/AndroidException.java +++ b/core/java/android/util/AndroidException.java @@ -34,5 +34,11 @@ public class AndroidException extends Exception { public AndroidException(Exception cause) { super(cause); } + + /** @hide */ + protected AndroidException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } }; diff --git a/core/java/android/util/ExceptionUtils.java b/core/java/android/util/ExceptionUtils.java index 44019c32560d..da7387fcae70 100644 --- a/core/java/android/util/ExceptionUtils.java +++ b/core/java/android/util/ExceptionUtils.java @@ -78,4 +78,12 @@ public class ExceptionUtils { propagateIfInstanceOf(t, RuntimeException.class); throw new RuntimeException(t); } + + /** + * Gets the root {@link Throwable#getCause() cause} of {@code t} + */ + public static @NonNull Throwable getRootCause(@NonNull Throwable t) { + while (t.getCause() != null) t = t.getCause(); + return t; + } } diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java new file mode 100644 index 000000000000..54b48b619101 --- /dev/null +++ b/core/java/android/util/FeatureFlagUtils.java @@ -0,0 +1,91 @@ +/* + * 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; + +import android.content.Context; +import android.os.SystemProperties; +import android.provider.Settings; +import android.text.TextUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Util class to get feature flag information. + * + * @hide + */ +public class FeatureFlagUtils { + + public static final String FFLAG_PREFIX = "sys.fflag."; + public static final String FFLAG_OVERRIDE_PREFIX = FFLAG_PREFIX + "override."; + + private static final Map<String, String> DEFAULT_FLAGS; + static { + DEFAULT_FLAGS = new HashMap<>(); + DEFAULT_FLAGS.put("device_info_v2", "true"); + DEFAULT_FLAGS.put("new_settings_suggestion", "true"); + DEFAULT_FLAGS.put("settings_search_v2", "false"); + DEFAULT_FLAGS.put("settings_app_info_v2", "false"); + DEFAULT_FLAGS.put("settings_connected_device_v2", "false"); + DEFAULT_FLAGS.put("settings_battery_v2", "false"); + DEFAULT_FLAGS.put("settings_battery_display_app_list", "false"); + } + + /** + * Whether or not a flag is enabled. + * + * @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(Context context, String feature) { + // Override precedence: + // Settings.Global -> sys.fflag.override.* -> static list + + // Step 1: check if feature flag is set in Settings.Global. + String value; + if (context != null) { + value = Settings.Global.getString(context.getContentResolver(), feature); + if (!TextUtils.isEmpty(value)) { + return Boolean.parseBoolean(value); + } + } + + // Step 2: check if feature flag has any override. Flag name: sys.fflag.override.<feature> + value = SystemProperties.get(FFLAG_OVERRIDE_PREFIX + feature); + if (!TextUtils.isEmpty(value)) { + return Boolean.parseBoolean(value); + } + // Step 3: check if feature flag has any default value. + value = getAllFeatureFlags().get(feature); + return Boolean.parseBoolean(value); + } + + /** + * Override feature flag to new state. + */ + public static void setEnabled(Context context, String feature, boolean enabled) { + SystemProperties.set(FFLAG_OVERRIDE_PREFIX + feature, enabled ? "true" : "false"); + } + + /** + * Returns all feature flags in their raw form. + */ + public static Map<String, String> getAllFeatureFlags() { + return DEFAULT_FLAGS; + } +} diff --git a/core/java/android/util/KeyValueListParser.java b/core/java/android/util/KeyValueListParser.java index be531ff35991..0a00794a1471 100644 --- a/core/java/android/util/KeyValueListParser.java +++ b/core/java/android/util/KeyValueListParser.java @@ -147,4 +147,46 @@ public class KeyValueListParser { } return def; } + + /** + * Get the value for key as an integer array. + * + * The value should be encoded as "0:1:2:3:4" + * + * @param key The key to lookup. + * @param def The value to return if the key was not found. + * @return the int[] value associated with the key. + */ + public int[] getIntArray(String key, int[] def) { + String value = mValues.get(key); + if (value != null) { + try { + String[] parts = value.split(":"); + if (parts.length > 0) { + int[] ret = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + ret[i] = Integer.parseInt(parts[i]); + } + return ret; + } + } catch (NumberFormatException e) { + // fallthrough + } + } + 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/core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl b/core/java/android/util/MutableInt.java index 7294124b4cdc..a3d8606d916a 100644 --- a/core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl +++ b/core/java/android/util/MutableInt.java @@ -14,12 +14,14 @@ * limitations under the License. */ -package com.android.internal.widget; +package android.util; -import android.os.IBinder; +/** + */ +public final class MutableInt { + public int value; -/** {@hide} */ -oneway interface IRemoteViewsAdapterConnection { - void onServiceConnected(IBinder service); - void onServiceDisconnected(); + public MutableInt(int value) { + this.value = value; + } } diff --git a/core/java/android/util/MutableLong.java b/core/java/android/util/MutableLong.java new file mode 100644 index 000000000000..575068ea9364 --- /dev/null +++ b/core/java/android/util/MutableLong.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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; + +/** + */ +public final class MutableLong { + public long value; + + public MutableLong(long value) { + this.value = value; + } +} diff --git a/core/java/android/util/Pools.java b/core/java/android/util/Pools.java index 70581be80dce..f0b7e01dae48 100644 --- a/core/java/android/util/Pools.java +++ b/core/java/android/util/Pools.java @@ -130,22 +130,29 @@ public final class Pools { } /** - * Synchronized) pool of objects. + * Synchronized pool of objects. * * @param <T> The pooled type. */ public static class SynchronizedPool<T> extends SimplePool<T> { - private final Object mLock = new Object(); + private final Object mLock; /** * Creates a new instance. * * @param maxPoolSize The max pool size. + * @param lock an optional custom object to synchronize on * * @throws IllegalArgumentException If the max pool size is less than zero. */ - public SynchronizedPool(int maxPoolSize) { + public SynchronizedPool(int maxPoolSize, Object lock) { super(maxPoolSize); + mLock = lock; + } + + /** @see #SynchronizedPool(int, Object) */ + public SynchronizedPool(int maxPoolSize) { + this(maxPoolSize, new Object()); } @Override diff --git a/core/java/android/util/StatsManager.java b/core/java/android/util/StatsManager.java new file mode 100644 index 000000000000..26a3c361e8c1 --- /dev/null +++ b/core/java/android/util/StatsManager.java @@ -0,0 +1,162 @@ +/* + * 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) { + Slog.d(TAG, "Failed to find statsd when adding configuration"); + return false; + } + return service.addConfiguration(configKey, config, pkg, cls); + } catch (RemoteException e) { + Slog.d(TAG, "Failed to connect to statsd when adding configuration"); + 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) { + Slog.d(TAG, "Failed to find statsd when removing configuration"); + return false; + } + return service.removeConfiguration(configKey); + } catch (RemoteException e) { + Slog.d(TAG, "Failed to connect to statsd when removing configuration"); + return false; + } + } + } + + /** + * Clients can request data with a binder call. This getter is destructive and also clears + * the retrieved metrics from statsd memory. + * + * @param configKey Configuration key to retrieve data from. + * @return Serialized ConfigMetricsReportList proto. Returns null on failure. + */ + @RequiresPermission(Manifest.permission.DUMP) + public byte[] getData(String configKey) { + synchronized (this) { + try { + IStatsManager service = getIStatsManagerLocked(); + if (service == null) { + Slog.d(TAG, "Failed to find statsd when getting data"); + return null; + } + return service.getData(configKey); + } catch (RemoteException e) { + Slog.d(TAG, "Failed to connecto statsd when getting data"); + return null; + } + } + } + + /** + * Clients can request metadata for statsd. Will contain stats across all configurations but not + * the actual metrics themselves (metrics must be collected via {@link #getData(String)}. + * This getter is not destructive and will not reset any metrics/counters. + * + * @return Serialized StatsdStatsReport proto. Returns null on failure. + */ + @RequiresPermission(Manifest.permission.DUMP) + public byte[] getMetadata() { + synchronized (this) { + try { + IStatsManager service = getIStatsManagerLocked(); + if (service == null) { + Slog.d(TAG, "Failed to find statsd when getting metadata"); + return null; + } + return service.getMetadata(); + } catch (RemoteException e) { + Slog.d(TAG, "Failed to connecto statsd when getting metadata"); + 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/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java index 2b03ed6c3ae1..cc4a0b60dd0a 100644 --- a/core/java/android/util/TimeUtils.java +++ b/core/java/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/core/java/android/util/TimingsTraceLog.java b/core/java/android/util/TimingsTraceLog.java index 36e9f77bb831..3e6f09bfa799 100644 --- a/core/java/android/util/TimingsTraceLog.java +++ b/core/java/android/util/TimingsTraceLog.java @@ -25,6 +25,7 @@ import java.util.Deque; /** * Helper class for reporting boot and shutdown timing metrics. + * <p>Note: This class is not thread-safe. Use a separate copy for other threads</p> * @hide */ public class TimingsTraceLog { @@ -34,10 +35,12 @@ public class TimingsTraceLog { DEBUG_BOOT_TIME ? new ArrayDeque<>() : null; private final String mTag; private long mTraceTag; + private long mThreadId; public TimingsTraceLog(String tag, long traceTag) { mTag = tag; mTraceTag = traceTag; + mThreadId = Thread.currentThread().getId(); } /** @@ -45,6 +48,7 @@ public class TimingsTraceLog { * @param name name to appear in trace */ public void traceBegin(String name) { + assertSameThread(); Trace.traceBegin(mTraceTag, name); if (DEBUG_BOOT_TIME) { mStartTimes.push(Pair.create(name, SystemClock.elapsedRealtime())); @@ -56,6 +60,7 @@ public class TimingsTraceLog { * Also {@link #logDuration logs} the duration. */ public void traceEnd() { + assertSameThread(); Trace.traceEnd(mTraceTag); if (!DEBUG_BOOT_TIME) { return; @@ -68,6 +73,15 @@ public class TimingsTraceLog { logDuration(event.first, (SystemClock.elapsedRealtime() - event.second)); } + private void assertSameThread() { + final Thread currentThread = Thread.currentThread(); + if (currentThread.getId() != mThreadId) { + throw new IllegalStateException("Instance of TimingsTraceLog can only be called from " + + "the thread it was created on (tid: " + mThreadId + "), but was from " + + currentThread.getName() + " (tid: " + currentThread.getId() + ")"); + } + } + /** * Log the duration so it can be parsed by external tools for performance reporting */ diff --git a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java index a9ccae114ba8..180812340ba8 100644 --- a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java +++ b/core/java/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/core/java/android/util/apk/ApkVerityBuilder.java b/core/java/android/util/apk/ApkVerityBuilder.java new file mode 100644 index 000000000000..7412ef411fb4 --- /dev/null +++ b/core/java/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/core/java/android/util/apk/ByteBufferDataSource.java b/core/java/android/util/apk/ByteBufferDataSource.java new file mode 100644 index 000000000000..3976568a429d --- /dev/null +++ b/core/java/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/core/java/android/util/apk/ByteBufferFactory.java b/core/java/android/util/apk/ByteBufferFactory.java new file mode 100644 index 000000000000..7a998822c870 --- /dev/null +++ b/core/java/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/core/java/android/util/apk/DataDigester.java b/core/java/android/util/apk/DataDigester.java new file mode 100644 index 000000000000..278be8037b0e --- /dev/null +++ b/core/java/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/core/java/android/util/apk/DataSource.java b/core/java/android/util/apk/DataSource.java new file mode 100644 index 000000000000..82f3800aa6d3 --- /dev/null +++ b/core/java/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/core/java/android/util/apk/MemoryMappedFileDataSource.java b/core/java/android/util/apk/MemoryMappedFileDataSource.java new file mode 100644 index 000000000000..8d2b1e328862 --- /dev/null +++ b/core/java/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/core/java/android/util/apk/SignatureInfo.java b/core/java/android/util/apk/SignatureInfo.java new file mode 100644 index 000000000000..8e1233af34a1 --- /dev/null +++ b/core/java/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/core/java/android/util/proto/ProtoOutputStream.java b/core/java/android/util/proto/ProtoOutputStream.java index 9afa56dd57e4..a94806a02fbb 100644 --- a/core/java/android/util/proto/ProtoOutputStream.java +++ b/core/java/android/util/proto/ProtoOutputStream.java @@ -29,8 +29,8 @@ import java.io.UnsupportedEncodingException; * Class to write to a protobuf stream. * * Each write method takes an ID code from the protoc generated classes - * and the value to write. To make a nested object, call startObject - * and then endObject when you are done. + * and the value to write. To make a nested object, call #start + * and then #end when you are done. * * The ID codes have type information embedded into them, so if you call * the incorrect function you will get an IllegalArgumentException. @@ -60,16 +60,16 @@ import java.io.UnsupportedEncodingException; * Message objects. We need to find another way. * * So what we do here is to let the calling code write the data into a - * byte[] (actually a collection of them wrapped in the EncodedBuffer) class, + * byte[] (actually a collection of them wrapped in the EncodedBuffer class), * but not do the varint encoding of the sub-message sizes. Then, we do a * recursive traversal of the buffer itself, calculating the sizes (which are * then knowable, although still not the actual sizes in the buffer because of * possible further nesting). Then we do a third pass, compacting the * buffer and varint encoding the sizes. * - * This gets us a relatively small number number of fixed-size allocations, + * This gets us a relatively small number of fixed-size allocations, * which is less likely to cause memory fragmentation or churn the GC, and - * the same number of data copies as would have gotten with setting it + * the same number of data copies as we would have gotten with setting it * field-by-field in generated code, and no code bloat from generated code. * The final data copy is also done with System.arraycopy, which will be * more efficient, in general, than doing the individual fields twice (as in @@ -77,26 +77,26 @@ import java.io.UnsupportedEncodingException; * * To accomplish the multiple passes, whenever we write a * WIRE_TYPE_LENGTH_DELIMITED field, we write the size occupied in our - * buffer as a fixed 32 bit int (called childRawSize), not variable length + * buffer as a fixed 32 bit int (called childRawSize), not a variable length * one. We reserve another 32 bit slot for the computed size (called * childEncodedSize). If we know the size up front, as we do for strings * and byte[], then we also put that into childEncodedSize, if we don't, we - * write the negative of childRawSize, as a sentiel that we need to + * write the negative of childRawSize, as a sentinel that we need to * compute it during the second pass and recursively compact it during the * third pass. * - * Unsgigned size varints can be up to five bytes long, but we reserve eight + * Unsigned size varints can be up to five bytes long, but we reserve eight * bytes for overhead, so we know that when we compact the buffer, there * will always be space for the encoded varint. * * When we can figure out the size ahead of time, we do, in order * to save overhead with recalculating it, and with the later arraycopy. * - * During the period between when the caller has called startObject, but - * not yet called endObject, we maintain a linked list of the tokens - * returned by startObject, stored in those 8 bytes of size storage space. + * During the period between when the caller has called #start, but + * not yet called #end, we maintain a linked list of the tokens + * returned by #start, stored in those 8 bytes of size storage space. * We use that linked list of tokens to ensure that the caller has - * correctly matched pairs of startObject and endObject calls, and issue + * correctly matched pairs of #start and #end calls, and issue * errors if they are not matched. */ @TestApi @@ -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"); @@ -2375,6 +2381,9 @@ public final class ProtoOutputStream { if (countString == null) { countString = "fieldCount=" + fieldCount; } + if (countString.length() > 0) { + countString += " "; + } final long fieldType = fieldId & FIELD_TYPE_MASK; String typeString = getFieldTypeString(fieldType); @@ -2382,7 +2391,7 @@ public final class ProtoOutputStream { typeString = "fieldType=" + fieldType; } - return fieldCount + " " + typeString + " tag=" + ((int)fieldId) + return countString + typeString + " tag=" + ((int) fieldId) + " fieldId=0x" + Long.toHexString(fieldId); } diff --git a/core/java/android/util/proto/ProtoUtils.java b/core/java/android/util/proto/ProtoUtils.java new file mode 100644 index 000000000000..85b7ec8265d1 --- /dev/null +++ b/core/java/android/util/proto/ProtoUtils.java @@ -0,0 +1,51 @@ +/* + * 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.proto; + +import android.util.AggStats; +import android.util.Duration; + +/** + * This class contains a list of helper functions to write common proto in + * //frameworks/base/core/proto/android/base directory + */ +public class ProtoUtils { + + /** + * Dump AggStats to ProtoOutputStream + * @hide + */ + public static void toAggStatsProto(ProtoOutputStream proto, long fieldId, + long min, long average, long max) { + final long aggStatsToken = proto.start(fieldId); + proto.write(AggStats.MIN, min); + proto.write(AggStats.AVERAGE, average); + proto.write(AggStats.MAX, max); + proto.end(aggStatsToken); + } + + /** + * Dump Duration to ProtoOutputStream + * @hide + */ + public static void toDuration(ProtoOutputStream proto, long fieldId, long startMs, long endMs) { + final long token = proto.start(fieldId); + proto.write(Duration.START_MS, startMs); + proto.write(Duration.END_MS, endMs); + proto.end(token); + } +} diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 597be6864494..ba6b6cf6e106 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -19,6 +19,7 @@ package android.view; import static android.view.DisplayEventReceiver.VSYNC_SOURCE_APP; import static android.view.DisplayEventReceiver.VSYNC_SOURCE_SURFACE_FLINGER; +import android.annotation.TestApi; import android.hardware.display.DisplayManagerGlobal; import android.os.Handler; import android.os.Looper; @@ -163,6 +164,7 @@ public final class Choreographer { private long mLastFrameTimeNanos; private long mFrameIntervalNanos; private boolean mDebugPrintNextFrameTimeDelta; + private int mFPSDivisor = 1; /** * Contains information about the current frame for jank-tracking, @@ -195,6 +197,7 @@ public final class Choreographer { * Callback type: Animation callback. Runs before traversals. * @hide */ + @TestApi public static final int CALLBACK_ANIMATION = 1; /** @@ -286,6 +289,7 @@ public final class Choreographer { * @return the requested time between frames, in milliseconds * @hide */ + @TestApi public static long getFrameDelay() { return sFrameDelay; } @@ -305,6 +309,7 @@ public final class Choreographer { * @param frameDelay the requested time between frames, in milliseconds * @hide */ + @TestApi public static void setFrameDelay(long frameDelay) { sFrameDelay = frameDelay; } @@ -597,6 +602,11 @@ public final class Choreographer { } } + void setFPSDivisor(int divisor) { + if (divisor <= 0) divisor = 1; + mFPSDivisor = divisor; + } + void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { @@ -639,6 +649,14 @@ public final class Choreographer { return; } + if (mFPSDivisor > 1) { + long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos; + if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) { + scheduleVsyncLocked(); + return; + } + } + mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos); mFrameScheduled = false; mLastFrameTimeNanos = frameTimeNanos; diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index e7c3f92da830..9673be01d7fb 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.CONFIGURE_DISPLAY_COLOR_MODE; import android.annotation.IntDef; import android.annotation.RequiresPermission; +import android.app.KeyguardManager; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -209,8 +210,8 @@ public final class Display { * </p> * * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD - * @see WindowManagerPolicy#isKeyguardSecure(int) - * @see WindowManagerPolicy#isKeyguardTrustedLw() + * @see KeyguardManager#isDeviceSecure() + * @see KeyguardManager#isDeviceLocked() * @see #getFlags * @hide */ @@ -294,11 +295,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 +313,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 +1006,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 +1125,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 +1134,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/core/java/android/service/autofill/Dataset.aidl b/core/java/android/view/DisplayCutout.aidl index 2342c5f5a45a..6d13b99b6eff 100644 --- a/core/java/android/service/autofill/Dataset.aidl +++ b/core/java/android/view/DisplayCutout.aidl @@ -1,5 +1,5 @@ /** - * Copyright (c) 2016, The Android Open Source Project + * 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. @@ -14,6 +14,6 @@ * limitations under the License. */ -package android.service.autofill; +package android.view; -parcelable Dataset; +parcelable DisplayCutout.ParcelableWrapper; diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java new file mode 100644 index 000000000000..19cd42e1fa18 --- /dev/null +++ b/core/java/android/view/DisplayCutout.java @@ -0,0 +1,408 @@ +/* + * 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; + +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import android.annotation.NonNull; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a part of the display that is not functional for displaying content. + * + * <p>{@code DisplayCutout} is immutable. + * + * @hide will become API + */ +public final class DisplayCutout { + + private static final Rect ZERO_RECT = new Rect(0, 0, 0, 0); + private static final ArrayList<Point> EMPTY_LIST = new ArrayList<>(); + + /** + * An instance where {@link #hasCutout()} returns {@code false}. + * + * @hide + */ + public static final DisplayCutout NO_CUTOUT = + new DisplayCutout(ZERO_RECT, ZERO_RECT, EMPTY_LIST); + + private final Rect mSafeInsets; + private final Rect mBoundingRect; + private final List<Point> mBoundingPolygon; + + /** + * Creates a DisplayCutout instance. + * + * NOTE: the Rects passed into this instance are not copied and MUST remain unchanged. + * + * @hide + */ + @VisibleForTesting + public DisplayCutout(Rect safeInsets, Rect boundingRect, List<Point> boundingPolygon) { + mSafeInsets = safeInsets != null ? safeInsets : ZERO_RECT; + mBoundingRect = boundingRect != null ? boundingRect : ZERO_RECT; + mBoundingPolygon = boundingPolygon != null ? boundingPolygon : EMPTY_LIST; + } + + /** + * Returns whether there is a cutout. + * + * If false, the safe insets will all return zero, and the bounding box or polygon will be + * empty or outside the content view. + * + * @return {@code true} if there is a cutout, {@code false} otherwise + */ + public boolean hasCutout() { + return !mSafeInsets.equals(ZERO_RECT); + } + + /** Returns the inset from the top which avoids the display cutout. */ + public int getSafeInsetTop() { + return mSafeInsets.top; + } + + /** Returns the inset from the bottom which avoids the display cutout. */ + public int getSafeInsetBottom() { + return mSafeInsets.bottom; + } + + /** Returns the inset from the left which avoids the display cutout. */ + public int getSafeInsetLeft() { + return mSafeInsets.left; + } + + /** Returns the inset from the right which avoids the display cutout. */ + public int getSafeInsetRight() { + return mSafeInsets.right; + } + + /** + * Obtains the safe insets in a rect. + * + * @param out a rect which is set to the safe insets. + * @hide + */ + public void getSafeInsets(@NonNull Rect out) { + out.set(mSafeInsets); + } + + /** + * Obtains the bounding rect of the cutout. + * + * @param outRect is filled with the bounding rect of the cutout. Coordinates are relative + * to the top-left corner of the content view. + */ + public void getBoundingRect(@NonNull Rect outRect) { + outRect.set(mBoundingRect); + } + + /** + * Obtains the bounding polygon of the cutout. + * + * @param outPolygon is filled with a list of points representing the corners of a convex + * polygon which covers the cutout. Coordinates are relative to the + * top-left corner of the content view. + */ + public void getBoundingPolygon(List<Point> outPolygon) { + outPolygon.clear(); + for (int i = 0; i < mBoundingPolygon.size(); i++) { + outPolygon.add(new Point(mBoundingPolygon.get(i))); + } + } + + @Override + public int hashCode() { + int result = mSafeInsets.hashCode(); + result = result * 31 + mBoundingRect.hashCode(); + result = result * 31 + mBoundingPolygon.hashCode(); + return result; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof DisplayCutout) { + DisplayCutout c = (DisplayCutout) o; + return mSafeInsets.equals(c.mSafeInsets) + && mBoundingRect.equals(c.mBoundingRect) + && mBoundingPolygon.equals(c.mBoundingPolygon); + } + return false; + } + + @Override + public String toString() { + return "DisplayCutout{insets=" + mSafeInsets + + " bounding=" + mBoundingRect + + "}"; + } + + /** + * Insets the reference frame of the cutout in the given directions. + * + * @return a copy of this instance which has been inset + * @hide + */ + public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) { + if (mBoundingRect.isEmpty() + || insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) { + return this; + } + + Rect safeInsets = new Rect(mSafeInsets); + Rect boundingRect = new Rect(mBoundingRect); + ArrayList<Point> boundingPolygon = new ArrayList<>(); + getBoundingPolygon(boundingPolygon); + + // Note: it's not really well defined what happens when the inset is negative, because we + // don't know if the safe inset needs to expand in general. + if (insetTop > 0 || safeInsets.top > 0) { + safeInsets.top = atLeastZero(safeInsets.top - insetTop); + } + if (insetBottom > 0 || safeInsets.bottom > 0) { + safeInsets.bottom = atLeastZero(safeInsets.bottom - insetBottom); + } + if (insetLeft > 0 || safeInsets.left > 0) { + safeInsets.left = atLeastZero(safeInsets.left - insetLeft); + } + if (insetRight > 0 || safeInsets.right > 0) { + safeInsets.right = atLeastZero(safeInsets.right - insetRight); + } + + boundingRect.offset(-insetLeft, -insetTop); + offset(boundingPolygon, -insetLeft, -insetTop); + + return new DisplayCutout(safeInsets, boundingRect, boundingPolygon); + } + + /** + * Calculates the safe insets relative to the given reference frame. + * + * @return a copy of this instance with the safe insets calculated + * @hide + */ + public DisplayCutout calculateRelativeTo(Rect frame) { + if (mBoundingRect.isEmpty() || !Rect.intersects(frame, mBoundingRect)) { + return NO_CUTOUT; + } + + Rect boundingRect = new Rect(mBoundingRect); + ArrayList<Point> boundingPolygon = new ArrayList<>(); + getBoundingPolygon(boundingPolygon); + + return DisplayCutout.calculateRelativeTo(frame, boundingRect, boundingPolygon); + } + + private static DisplayCutout calculateRelativeTo(Rect frame, Rect boundingRect, + ArrayList<Point> boundingPolygon) { + Rect safeRect = new Rect(); + int bestArea = 0; + int bestVariant = 0; + for (int variant = ROTATION_0; variant <= ROTATION_270; variant++) { + int area = calculateInsetVariantArea(frame, boundingRect, variant, safeRect); + if (bestArea < area) { + bestArea = area; + bestVariant = variant; + } + } + calculateInsetVariantArea(frame, boundingRect, bestVariant, safeRect); + if (safeRect.isEmpty()) { + // The entire frame overlaps with the cutout. + safeRect.set(0, frame.height(), 0, 0); + } else { + // Convert safeRect to insets relative to frame. We're reusing the rect here to avoid + // an allocation. + safeRect.set( + Math.max(0, safeRect.left - frame.left), + Math.max(0, safeRect.top - frame.top), + Math.max(0, frame.right - safeRect.right), + Math.max(0, frame.bottom - safeRect.bottom)); + } + + boundingRect.offset(-frame.left, -frame.top); + offset(boundingPolygon, -frame.left, -frame.top); + + return new DisplayCutout(safeRect, boundingRect, boundingPolygon); + } + + private static int calculateInsetVariantArea(Rect frame, Rect boundingRect, int variant, + Rect outSafeRect) { + switch (variant) { + case ROTATION_0: + outSafeRect.set(frame.left, frame.top, frame.right, boundingRect.top); + break; + case ROTATION_90: + outSafeRect.set(frame.left, frame.top, boundingRect.left, frame.bottom); + break; + case ROTATION_180: + outSafeRect.set(frame.left, boundingRect.bottom, frame.right, frame.bottom); + break; + case ROTATION_270: + outSafeRect.set(boundingRect.right, frame.top, frame.right, frame.bottom); + break; + } + + return outSafeRect.isEmpty() ? 0 : outSafeRect.width() * outSafeRect.height(); + } + + private static int atLeastZero(int value) { + return value < 0 ? 0 : value; + } + + private static void offset(ArrayList<Point> points, int dx, int dy) { + for (int i = 0; i < points.size(); i++) { + points.get(i).offset(dx, dy); + } + } + + /** + * Creates an instance from a bounding polygon. + * + * @hide + */ + public static DisplayCutout fromBoundingPolygon(List<Point> points) { + Rect boundingRect = new Rect(Integer.MAX_VALUE, Integer.MAX_VALUE, + Integer.MIN_VALUE, Integer.MIN_VALUE); + ArrayList<Point> boundingPolygon = new ArrayList<>(); + + for (int i = 0; i < points.size(); i++) { + Point point = points.get(i); + boundingRect.left = Math.min(boundingRect.left, point.x); + boundingRect.right = Math.max(boundingRect.right, point.x); + boundingRect.top = Math.min(boundingRect.top, point.y); + boundingRect.bottom = Math.max(boundingRect.bottom, point.y); + boundingPolygon.add(new Point(point)); + } + + return new DisplayCutout(ZERO_RECT, boundingRect, boundingPolygon); + } + + /** + * Helper class for passing {@link DisplayCutout} through binder. + * + * Needed, because {@code readFromParcel} cannot be used with immutable classes. + * + * @hide + */ + public static final class ParcelableWrapper implements Parcelable { + + private DisplayCutout mInner; + + public ParcelableWrapper() { + this(NO_CUTOUT); + } + + public ParcelableWrapper(DisplayCutout cutout) { + mInner = cutout; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + if (mInner == NO_CUTOUT) { + out.writeInt(0); + } else { + out.writeInt(1); + out.writeTypedObject(mInner.mSafeInsets, flags); + out.writeTypedObject(mInner.mBoundingRect, flags); + out.writeTypedList(mInner.mBoundingPolygon, flags); + } + } + + /** + * Similar to {@link Creator#createFromParcel(Parcel)}, but reads into an existing + * instance. + * + * Needed for AIDL out parameters. + */ + public void readFromParcel(Parcel in) { + mInner = readCutout(in); + } + + public static final Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() { + @Override + public ParcelableWrapper createFromParcel(Parcel in) { + return new ParcelableWrapper(readCutout(in)); + } + + @Override + public ParcelableWrapper[] newArray(int size) { + return new ParcelableWrapper[size]; + } + }; + + private static DisplayCutout readCutout(Parcel in) { + if (in.readInt() == 0) { + return NO_CUTOUT; + } + + ArrayList<Point> boundingPolygon = new ArrayList<>(); + + Rect safeInsets = in.readTypedObject(Rect.CREATOR); + Rect boundingRect = in.readTypedObject(Rect.CREATOR); + in.readTypedList(boundingPolygon, Point.CREATOR); + + return new DisplayCutout(safeInsets, boundingRect, boundingPolygon); + } + + public DisplayCutout get() { + return mInner; + } + + public void set(ParcelableWrapper cutout) { + mInner = cutout.get(); + } + + public void set(DisplayCutout cutout) { + mInner = cutout; + } + + @Override + public int hashCode() { + return mInner.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof ParcelableWrapper + && mInner.equals(((ParcelableWrapper) o).mInner); + } + + @Override + public String toString() { + return String.valueOf(mInner); + } + } +} diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 0cec496fa264..b813ddb63859 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -16,12 +16,19 @@ package android.view; +import static android.view.DisplayInfoProto.APP_HEIGHT; +import static android.view.DisplayInfoProto.APP_WIDTH; +import static android.view.DisplayInfoProto.LOGICAL_HEIGHT; +import static android.view.DisplayInfoProto.LOGICAL_WIDTH; + import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.util.ArraySet; import android.util.DisplayMetrics; +import android.util.proto.ProtoOutputStream; import libcore.util.Objects; @@ -562,10 +569,10 @@ public final class DisplayInfo implements Parcelable { outMetrics.xdpi = outMetrics.noncompatXdpi = physicalXDpi; outMetrics.ydpi = outMetrics.noncompatYdpi = physicalYDpi; - width = configuration != null && configuration.appBounds != null - ? configuration.appBounds.width() : width; - height = configuration != null && configuration.appBounds != null - ? configuration.appBounds.height() : height; + final Rect appBounds = configuration != null + ? configuration.windowConfiguration.getAppBounds() : null; + width = appBounds != null ? appBounds.width() : width; + height = appBounds != null ? appBounds.height() : height; outMetrics.noncompatWidthPixels = outMetrics.widthPixels = width; outMetrics.noncompatHeightPixels = outMetrics.heightPixels = height; @@ -654,6 +661,22 @@ public final class DisplayInfo implements Parcelable { return sb.toString(); } + /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.view.DisplayInfoProto} + * + * @param protoOutputStream Stream to write the Rect object to. + * @param fieldId Field Id of the DisplayInfoProto as defined in the parent message + */ + public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + final long token = protoOutputStream.start(fieldId); + protoOutputStream.write(LOGICAL_WIDTH, logicalWidth); + protoOutputStream.write(LOGICAL_HEIGHT, logicalHeight); + protoOutputStream.write(APP_WIDTH, appWidth); + protoOutputStream.write(APP_HEIGHT, appHeight); + protoOutputStream.end(token); + } + private static String flagsToString(int flags) { StringBuilder result = new StringBuilder(); if ((flags & Display.FLAG_SECURE) != 0) { diff --git a/core/java/android/view/DragEvent.java b/core/java/android/view/DragEvent.java index 16f2d7d0b6c7..2c9f87128bca 100644 --- a/core/java/android/view/DragEvent.java +++ b/core/java/android/view/DragEvent.java @@ -103,7 +103,7 @@ import com.android.internal.view.IDragAndDropPermissions; * <tr> * <td>ACTION_DRAG_ENDED</td> * <td style="text-align: center;"> </td> - * <td style="text-align: center;"> </td> + * <td style="text-align: center;">X</td> * <td style="text-align: center;"> </td> * <td style="text-align: center;"> </td> * <td style="text-align: center;"> </td> @@ -112,6 +112,7 @@ import com.android.internal.view.IDragAndDropPermissions; * </table> * <p> * The {@link android.view.DragEvent#getAction()}, + * {@link android.view.DragEvent#getLocalState()} * {@link android.view.DragEvent#describeContents()}, * {@link android.view.DragEvent#writeToParcel(Parcel,int)}, and * {@link android.view.DragEvent#toString()} methods always return valid data. @@ -397,7 +398,7 @@ public class DragEvent implements Parcelable { * operation. In all other activities this method will return null * </p> * <p> - * This method returns valid data for all event actions except for {@link #ACTION_DRAG_ENDED}. + * This method returns valid data for all event actions. * </p> * @return The local state object sent to the system by startDragAndDrop(). */ diff --git a/core/java/android/view/FrameInfo.java b/core/java/android/view/FrameInfo.java index c79547c8313e..6c5e048f05d2 100644 --- a/core/java/android/view/FrameInfo.java +++ b/core/java/android/view/FrameInfo.java @@ -16,7 +16,7 @@ package android.view; -import android.annotation.IntDef; +import android.annotation.LongDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -48,7 +48,7 @@ final class FrameInfo { // Is this the first-draw following a window layout? public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1; - @IntDef(flag = true, value = { + @LongDef(flag = true, value = { FLAG_WINDOW_LAYOUT_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface FrameInfoFlags {} diff --git a/core/java/android/view/Gravity.java b/core/java/android/view/Gravity.java index 324a1ae3e606..defa58e10e6e 100644 --- a/core/java/android/view/Gravity.java +++ b/core/java/android/view/Gravity.java @@ -440,4 +440,60 @@ public class Gravity } return result; } + + /** + * @hide + */ + public static String toString(int gravity) { + final StringBuilder result = new StringBuilder(); + if ((gravity & FILL) == FILL) { + result.append("FILL").append(' '); + } else { + if ((gravity & FILL_VERTICAL) == FILL_VERTICAL) { + result.append("FILL_VERTICAL").append(' '); + } else { + if ((gravity & TOP) == TOP) { + result.append("TOP").append(' '); + } + if ((gravity & BOTTOM) == BOTTOM) { + result.append("BOTTOM").append(' '); + } + } + if ((gravity & FILL_HORIZONTAL) == FILL_HORIZONTAL) { + result.append("FILL_HORIZONTAL").append(' '); + } else { + if ((gravity & START) == START) { + result.append("START").append(' '); + } else if ((gravity & LEFT) == LEFT) { + result.append("LEFT").append(' '); + } + if ((gravity & END) == END) { + result.append("END").append(' '); + } else if ((gravity & RIGHT) == RIGHT) { + result.append("RIGHT").append(' '); + } + } + } + if ((gravity & CENTER) == CENTER) { + result.append("CENTER").append(' '); + } else { + if ((gravity & CENTER_VERTICAL) == CENTER_VERTICAL) { + result.append("CENTER_VERTICAL").append(' '); + } + if ((gravity & CENTER_HORIZONTAL) == CENTER_HORIZONTAL) { + result.append("CENTER_HORIZONTAL").append(' '); + } + } + if (result.length() == 0) { + result.append("NO GRAVITY").append(' '); + } + if ((gravity & DISPLAY_CLIP_VERTICAL) == DISPLAY_CLIP_VERTICAL) { + result.append("DISPLAY_CLIP_VERTICAL").append(' '); + } + if ((gravity & DISPLAY_CLIP_HORIZONTAL) == DISPLAY_CLIP_HORIZONTAL) { + result.append("DISPLAY_CLIP_HORIZONTAL").append(' '); + } + result.deleteCharAt(result.length() - 1); + return result.toString(); + } } diff --git a/core/java/android/view/IApplicationToken.aidl b/core/java/android/view/IApplicationToken.aidl index b01c0ef55812..a063a70a3181 100644 --- a/core/java/android/view/IApplicationToken.aidl +++ b/core/java/android/view/IApplicationToken.aidl @@ -20,5 +20,6 @@ package android.view; /** {@hide} */ interface IApplicationToken { + String getName(); } diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index 611cc6337fb4..07a57fb73a0d 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -23,6 +23,7 @@ import android.os.ParcelFileDescriptor; import android.view.DragEvent; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.DisplayCutout; import com.android.internal.os.IResultReceiver; import android.util.MergedConfiguration; @@ -50,7 +51,8 @@ oneway interface IWindow { void resized(in Rect frame, in Rect overscanInsets, in Rect contentInsets, in Rect visibleInsets, in Rect stableInsets, in Rect outsets, boolean reportDraw, in MergedConfiguration newMergedConfiguration, in Rect backDropFrame, - boolean forceLayout, boolean alwaysConsumeNavBar, int displayId); + boolean forceLayout, boolean alwaysConsumeNavBar, int displayId, + in DisplayCutout.ParcelableWrapper displayCutout); void moved(int newX, int newY); void dispatchAppVisibility(boolean visible); void dispatchGetNewSurface(); diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index e576a0fbdb2b..9e103a38263c 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -16,13 +16,13 @@ package android.view; -import com.android.internal.app.IAssistScreenshotReceiver; import com.android.internal.os.IResultReceiver; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodClient; import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.policy.IShortcutService; +import android.app.IAssistDataReceiver; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.graphics.Bitmap; @@ -147,7 +147,6 @@ interface IWindowManager void exitKeyguardSecurely(IOnKeyguardExitResult callback); boolean isKeyguardLocked(); boolean isKeyguardSecure(); - boolean inKeyguardRestrictedInputMode(); void dismissKeyguard(IKeyguardDismissCallback callback); // Requires INTERACT_ACROSS_USERS_FULL permission @@ -272,7 +271,7 @@ interface IWindowManager /** * Used only for assist -- request a screenshot of the current application. */ - boolean requestAssistScreenshot(IAssistScreenshotReceiver receiver); + boolean requestAssistScreenshot(IAssistDataReceiver receiver); /** * Called by the status bar to notify Views of changes to System UI visiblity. @@ -358,10 +357,10 @@ interface IWindowManager * Updates the dim layer used while resizing. * * @param visible Whether the dim layer should be visible. - * @param targetStackId The id of the task stack the dim layer should be placed on. + * @param targetWindowingMode The windowing mode of the stack the dim layer should be placed on. * @param alpha The translucency of the dim layer, between 0 and 1. */ - void setResizeDimLayer(boolean visible, int targetStackId, float alpha); + void setResizeDimLayer(boolean visible, int targetWindowingMode, float alpha); /** * Requests Keyboard Shortcuts from the displayed window. @@ -385,7 +384,7 @@ interface IWindowManager /** * Create an input consumer by name. */ - void createInputConsumer(String name, out InputChannel inputChannel); + void createInputConsumer(IBinder token, String name, out InputChannel inputChannel); /** * Destroy an input consumer by name. This method will also dispose the input channels diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 51d65144f260..ed167c812be1 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -22,6 +22,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; import android.util.MergedConfiguration; +import android.view.DisplayCutout; import android.view.InputChannel; import android.view.IWindow; import android.view.IWindowId; @@ -40,7 +41,8 @@ interface IWindowSession { out InputChannel outInputChannel); int addToDisplay(IWindow window, int seq, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, out Rect outContentInsets, - out Rect outStableInsets, out Rect outOutsets, out InputChannel outInputChannel); + out Rect outStableInsets, out Rect outOutsets, + out DisplayCutout.ParcelableWrapper displayCutout, out InputChannel outInputChannel); int addWithoutInputChannel(IWindow window, int seq, in WindowManager.LayoutParams attrs, in int viewVisibility, out Rect outContentInsets, out Rect outStableInsets); int addToDisplayWithoutInputChannel(IWindow window, int seq, in WindowManager.LayoutParams attrs, @@ -96,6 +98,7 @@ interface IWindowSession { int flags, out Rect outFrame, out Rect outOverscanInsets, out Rect outContentInsets, out Rect outVisibleInsets, out Rect outStableInsets, out Rect outOutsets, out Rect outBackdropFrame, + out DisplayCutout.ParcelableWrapper displayCutout, out MergedConfiguration outMergedConfiguration, out Surface outSurface); /* diff --git a/core/java/android/view/InputFilter.java b/core/java/android/view/InputFilter.java index d0dab4000fff..0ab4dc026e69 100644 --- a/core/java/android/view/InputFilter.java +++ b/core/java/android/view/InputFilter.java @@ -72,21 +72,21 @@ import android.os.RemoteException; * </p><p> * The early policy interception decides whether an input event should be delivered * to applications or dropped. The policy indicates its decision by setting the - * {@link WindowManagerPolicy#FLAG_PASS_TO_USER} policy flag. The input filter may + * {@link WindowManagerPolicyConstants#FLAG_PASS_TO_USER} policy flag. The input filter may * sometimes receive events that do not have this flag set. It should take note of * the fact that the policy intends to drop the event, clean up its state, and * then send appropriate cancellation events to the dispatcher if needed. * </p><p> * For example, suppose the input filter is processing a gesture and one of the touch events - * it receives does not have the {@link WindowManagerPolicy#FLAG_PASS_TO_USER} flag set. + * it receives does not have the {@link WindowManagerPolicyConstants#FLAG_PASS_TO_USER} flag set. * The input filter should clear its internal state about the gesture and then send key or * motion events to the dispatcher to cancel any keys or pointers that are down. * </p><p> * Corollary: Events that get sent to the dispatcher should usually include the - * {@link WindowManagerPolicy#FLAG_PASS_TO_USER} flag. Otherwise, they will be dropped! + * {@link WindowManagerPolicyConstants#FLAG_PASS_TO_USER} flag. Otherwise, they will be dropped! * </p><p> * It may be prudent to disable automatic key repeating for synthetic key events - * by setting the {@link WindowManagerPolicy#FLAG_DISABLE_KEY_REPEAT} policy flag. + * by setting the {@link WindowManagerPolicyConstants#FLAG_DISABLE_KEY_REPEAT} policy flag. * </p> * * @hide diff --git a/core/java/android/view/Menu.java b/core/java/android/view/Menu.java index a8ea4dc1b8a0..6d1f740ad1da 100644 --- a/core/java/android/view/Menu.java +++ b/core/java/android/view/Menu.java @@ -451,5 +451,10 @@ public interface Menu { * will use numeric shortcuts. */ public void setQwertyMode(boolean isQwerty); -} + /** + * Enable or disable the group dividers. + */ + default void setGroupDividerEnabled(boolean groupDividerEnabled) { + } +}
\ No newline at end of file diff --git a/core/java/android/view/MenuItem.java b/core/java/android/view/MenuItem.java index 88b9c0d31659..ad160cbf41eb 100644 --- a/core/java/android/view/MenuItem.java +++ b/core/java/android/view/MenuItem.java @@ -72,11 +72,6 @@ public interface MenuItem { public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8; /** - * @hide - */ - int SHOW_AS_OVERFLOW_ALWAYS = 1 << 31; - - /** * Interface definition for a callback to be invoked when a menu item is * clicked. * @@ -806,12 +801,22 @@ public interface MenuItem { } /** - * Returns true if {@link #setShowAsAction(int)} was set to {@link #SHOW_AS_OVERFLOW_ALWAYS}. - * Default value if {@code false}. + * Returns true if {@link #setShowAsAction(int)} was set to {@link #SHOW_AS_ACTION_ALWAYS}. + * Default value is {@code false}. * * @hide */ - default boolean requiresOverflow() { + default boolean requiresActionButton() { return false; } + + /** + * Returns true if {@link #setShowAsAction(int)} was set to {@link #SHOW_AS_ACTION_NEVER}. + * Default value is {@code true}. + * + * @hide + */ + default boolean requiresOverflow() { + return true; + } } diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index 580456023d68..ab0b3eec8753 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/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/core/java/android/view/RecordingCanvas.java b/core/java/android/view/RecordingCanvas.java index e4b91b75e95a..5088cdc948ef 100644 --- a/core/java/android/view/RecordingCanvas.java +++ b/core/java/android/view/RecordingCanvas.java @@ -395,7 +395,7 @@ public class RecordingCanvas extends Canvas { throw new IndexOutOfBoundsException(); } nDrawText(mNativeCanvasWrapper, text, index, count, x, y, paint.mBidiFlags, - paint.getNativeInstance(), paint.mNativeTypeface); + paint.getNativeInstance()); } @Override @@ -407,7 +407,7 @@ public class RecordingCanvas extends Canvas { if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { nDrawText(mNativeCanvasWrapper, text.toString(), start, end, x, y, - paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); + paint.mBidiFlags, paint.getNativeInstance()); } else if (text instanceof GraphicsOperations) { ((GraphicsOperations) text).drawText(this, start, end, x, y, paint); @@ -415,7 +415,7 @@ public class RecordingCanvas extends Canvas { char[] buf = TemporaryBuffer.obtain(end - start); TextUtils.getChars(text, start, end, buf, 0); nDrawText(mNativeCanvasWrapper, buf, 0, end - start, x, y, - paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); + paint.mBidiFlags, paint.getNativeInstance()); TemporaryBuffer.recycle(buf); } } @@ -423,7 +423,7 @@ public class RecordingCanvas extends Canvas { @Override public final void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) { nDrawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags, - paint.getNativeInstance(), paint.mNativeTypeface); + paint.getNativeInstance()); } @Override @@ -433,7 +433,7 @@ public class RecordingCanvas extends Canvas { throw new IndexOutOfBoundsException(); } nDrawText(mNativeCanvasWrapper, text, start, end, x, y, paint.mBidiFlags, - paint.getNativeInstance(), paint.mNativeTypeface); + paint.getNativeInstance()); } @Override @@ -444,7 +444,7 @@ public class RecordingCanvas extends Canvas { } nDrawTextOnPath(mNativeCanvasWrapper, text, index, count, path.readOnlyNI(), hOffset, vOffset, - paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); + paint.mBidiFlags, paint.getNativeInstance()); } @Override @@ -452,7 +452,7 @@ public class RecordingCanvas extends Canvas { float vOffset, @NonNull Paint paint) { if (text.length() > 0) { nDrawTextOnPath(mNativeCanvasWrapper, text, path.readOnlyNI(), hOffset, vOffset, - paint.mBidiFlags, paint.getNativeInstance(), paint.mNativeTypeface); + paint.mBidiFlags, paint.getNativeInstance()); } } @@ -473,7 +473,7 @@ public class RecordingCanvas extends Canvas { } nDrawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount, - x, y, isRtl, paint.getNativeInstance(), paint.mNativeTypeface); + x, y, isRtl, paint.getNativeInstance()); } @Override @@ -494,7 +494,7 @@ public class RecordingCanvas extends Canvas { if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { nDrawTextRun(mNativeCanvasWrapper, text.toString(), start, end, contextStart, - contextEnd, x, y, isRtl, paint.getNativeInstance(), paint.mNativeTypeface); + contextEnd, x, y, isRtl, paint.getNativeInstance()); } else if (text instanceof GraphicsOperations) { ((GraphicsOperations) text).drawTextRun(this, start, end, contextStart, contextEnd, x, y, isRtl, paint); @@ -504,7 +504,7 @@ public class RecordingCanvas extends Canvas { char[] buf = TemporaryBuffer.obtain(contextLen); TextUtils.getChars(text, contextStart, contextEnd, buf, 0); nDrawTextRun(mNativeCanvasWrapper, buf, start - contextStart, len, - 0, contextLen, x, y, isRtl, paint.getNativeInstance(), paint.mNativeTypeface); + 0, contextLen, x, y, isRtl, paint.getNativeInstance()); TemporaryBuffer.recycle(buf); } } @@ -614,28 +614,25 @@ public class RecordingCanvas extends Canvas { @FastNative private static native void nDrawText(long nativeCanvas, char[] text, int index, int count, - float x, float y, int flags, long nativePaint, long nativeTypeface); + float x, float y, int flags, long nativePaint); @FastNative private static native void nDrawText(long nativeCanvas, String text, int start, int end, - float x, float y, int flags, long nativePaint, long nativeTypeface); + float x, float y, int flags, long nativePaint); @FastNative private static native void nDrawTextRun(long nativeCanvas, String text, int start, int end, - int contextStart, int contextEnd, float x, float y, boolean isRtl, long nativePaint, - long nativeTypeface); + int contextStart, int contextEnd, float x, float y, boolean isRtl, long nativePaint); @FastNative private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count, - int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint, - long nativeTypeface); + int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint); @FastNative private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count, - long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint, - long nativeTypeface); + long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint); @FastNative private static native void nDrawTextOnPath(long nativeCanvas, String text, long nativePath, - float hOffset, float vOffset, int flags, long nativePaint, long nativeTypeface); + float hOffset, float vOffset, int flags, long nativePaint); } diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java index ea6e63c3b9de..5070151815f5 100644 --- a/core/java/android/view/RenderNode.java +++ b/core/java/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/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java index 95150409514d..c4a716011765 100644 --- a/core/java/android/view/RenderNodeAnimator.java +++ b/core/java/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/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index 2c1f73468ca6..ddced6cdd83f 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -762,7 +762,7 @@ public class Surface implements Parcelable { return "ROTATION_270"; } default: { - throw new IllegalArgumentException("Invalid rotation: " + rotation); + return Integer.toString(rotation); } } } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 1a2968f6345f..357b8d9d039b 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -16,29 +16,46 @@ package android.view; -import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; - +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; +import static android.graphics.Matrix.MSCALE_X; +import static android.graphics.Matrix.MSCALE_Y; +import static android.graphics.Matrix.MSKEW_X; +import static android.graphics.Matrix.MSKEW_Y; +import static android.graphics.Matrix.MTRANS_X; +import static android.graphics.Matrix.MTRANS_Y; + +import android.annotation.Size; import android.graphics.Bitmap; import android.graphics.GraphicBuffer; +import android.graphics.PixelFormat; +import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.Region; -import android.os.Binder; import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.os.UserHandle; import android.util.Log; import android.view.Surface.OutOfResourcesException; - import dalvik.system.CloseGuard; +import libcore.util.NativeAllocationRegistry; + +import java.io.Closeable; /** * SurfaceControl * @hide */ -public class SurfaceControl { +public class SurfaceControl implements Parcelable { private static final String TAG = "SurfaceControl"; private static native long nativeCreate(SurfaceSession session, String name, int w, int h, int format, int flags, long parentObject, int windowType, int ownerUid) throws OutOfResourcesException; + private static native long nativeReadFromParcel(Parcel in); + private static native void nativeWriteToParcel(long nativeObject, Parcel out); private static native void nativeRelease(long nativeObject); private static native void nativeDestroy(long nativeObject); private static native void nativeDisconnect(long nativeObject); @@ -52,25 +69,39 @@ public class SurfaceControl { private static native void nativeScreenshot(IBinder displayToken, Surface consumer, Rect sourceCrop, int width, int height, int minLayer, int maxLayer, boolean allLayers, boolean useIdentityTransform); - - private static native void nativeOpenTransaction(); - private static native void nativeCloseTransaction(boolean sync); - private static native void nativeSetAnimationTransaction(); - - private static native void nativeSetLayer(long nativeObject, int zorder); - private static native void nativeSetRelativeLayer(long nativeObject, IBinder relativeTo, - int zorder); - private static native void nativeSetPosition(long nativeObject, float x, float y); - private static native void nativeSetGeometryAppliesWithResize(long nativeObject); - private static native void nativeSetSize(long nativeObject, int w, int h); - private static native void nativeSetTransparentRegionHint(long nativeObject, Region region); - private static native void nativeSetAlpha(long nativeObject, float alpha); - private static native void nativeSetMatrix(long nativeObject, float dsdx, float dtdx, + private static native GraphicBuffer nativeCaptureLayers(IBinder layerHandleToken, + Rect sourceCrop, float frameScale); + + private static native long nativeCreateTransaction(); + private static native long nativeGetNativeTransactionFinalizer(); + private static native void nativeApplyTransaction(long transactionObj, boolean sync); + private static native void nativeMergeTransaction(long transactionObj, + long otherTransactionObj); + private static native void nativeSetAnimationTransaction(long transactionObj); + + private static native void nativeSetLayer(long transactionObj, long nativeObject, int zorder); + private static native void nativeSetRelativeLayer(long transactionObj, long nativeObject, + IBinder relativeTo, int zorder); + private static native void nativeSetPosition(long transactionObj, long nativeObject, + float x, float y); + private static native void nativeSetGeometryAppliesWithResize(long transactionObj, + long nativeObject); + private static native void nativeSetSize(long transactionObj, long nativeObject, int w, int h); + private static native void nativeSetTransparentRegionHint(long transactionObj, + long nativeObject, Region region); + private static native void nativeSetAlpha(long transactionObj, long nativeObject, float alpha); + private static native void nativeSetMatrix(long transactionObj, long nativeObject, + float dsdx, float dtdx, float dtdy, float dsdy); - private static native void nativeSetFlags(long nativeObject, int flags, int mask); - private static native void nativeSetWindowCrop(long nativeObject, int l, int t, int r, int b); - private static native void nativeSetFinalCrop(long nativeObject, int l, int t, int r, int b); - private static native void nativeSetLayerStack(long nativeObject, int layerStack); + private static native void nativeSetColor(long transactionObj, long nativeObject, float[] color); + private static native void nativeSetFlags(long transactionObj, long nativeObject, + int flags, int mask); + private static native void nativeSetWindowCrop(long transactionObj, long nativeObject, + int l, int t, int r, int b); + private static native void nativeSetFinalCrop(long transactionObj, long nativeObject, + int l, int t, int r, int b); + private static native void nativeSetLayerStack(long transactionObj, long nativeObject, + int layerStack); private static native boolean nativeClearContentFrameStats(long nativeObject); private static native boolean nativeGetContentFrameStats(long nativeObject, WindowContentFrameStats outStats); @@ -80,15 +111,16 @@ public class SurfaceControl { private static native IBinder nativeGetBuiltInDisplay(int physicalDisplayId); private static native IBinder nativeCreateDisplay(String name, boolean secure); private static native void nativeDestroyDisplay(IBinder displayToken); - private static native void nativeSetDisplaySurface( + private static native void nativeSetDisplaySurface(long transactionObj, IBinder displayToken, long nativeSurfaceObject); - private static native void nativeSetDisplayLayerStack( + private static native void nativeSetDisplayLayerStack(long transactionObj, IBinder displayToken, int layerStack); - private static native void nativeSetDisplayProjection( + private static native void nativeSetDisplayProjection(long transactionObj, IBinder displayToken, int orientation, int l, int t, int r, int b, int L, int T, int R, int B); - private static native void nativeSetDisplaySize(IBinder displayToken, int width, int height); + private static native void nativeSetDisplaySize(long transactionObj, IBinder displayToken, + int width, int height); private static native SurfaceControl.PhysicalDisplayInfo[] nativeGetDisplayConfigs( IBinder displayToken); private static native int nativeGetActiveConfig(IBinder displayToken); @@ -99,14 +131,17 @@ public class SurfaceControl { int colorMode); private static native void nativeSetDisplayPowerMode( IBinder displayToken, int mode); - private static native void nativeDeferTransactionUntil(long nativeObject, + private static native void nativeDeferTransactionUntil(long transactionObj, long nativeObject, IBinder handle, long frame); - private static native void nativeDeferTransactionUntilSurface(long nativeObject, + private static native void nativeDeferTransactionUntilSurface(long transactionObj, + long nativeObject, long surfaceObject, long frame); - private static native void nativeReparentChildren(long nativeObject, + private static native void nativeReparentChildren(long transactionObj, long nativeObject, IBinder handle); - private static native void nativeSeverChildren(long nativeObject); - private static native void nativeSetOverrideScalingMode(long nativeObject, + private static native void nativeReparent(long transactionObj, long nativeObject, + IBinder parentHandle); + private static native void nativeSeverChildren(long transactionObj, long nativeObject); + private static native void nativeSetOverrideScalingMode(long transactionObj, long nativeObject, int scalingMode); private static native IBinder nativeGetHandle(long nativeObject); private static native boolean nativeGetTransformToDisplayInverse(long nativeObject); @@ -118,6 +153,9 @@ public class SurfaceControl { private final String mName; long mNativeObject; // package visibility only for Surface.java access + static Transaction sGlobalTransaction; + static long sTransactionNestCount = 0; + /* flags used in constructor (keep in sync with ISurfaceComposerClient.h) */ /** @@ -161,7 +199,7 @@ public class SurfaceControl { /** * Surface creation flag: Indicates that the surface must be considered opaque, - * even if its pixel format is set to translucent. This can be useful if an + * even if its pixel format contains an alpha channel. This can be useful if an * application needs full RGBA 8888 support for instance but will * still draw every pixel opaque. * <p> @@ -273,6 +311,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. @@ -282,6 +326,203 @@ public class SurfaceControl { public static final int WINDOW_TYPE_DONT_SCREENSHOT = 441731; /** + * Builder class for {@link SurfaceControl} objects. + */ + public static class Builder { + private SurfaceSession mSession; + private int mFlags = HIDDEN; + private int mWidth; + private int mHeight; + private int mFormat = PixelFormat.OPAQUE; + private String mName; + private SurfaceControl mParent; + private int mWindowType; + private int mOwnerUid; + + /** + * Begin building a SurfaceControl with a given {@link SurfaceSession}. + * + * @param session The {@link SurfaceSession} with which to eventually construct the surface. + */ + public Builder(SurfaceSession session) { + mSession = session; + } + + /** + * Construct a new {@link SurfaceControl} with the set parameters. + */ + public SurfaceControl build() { + if (mWidth <= 0 || mHeight <= 0) { + throw new IllegalArgumentException( + "width and height must be set"); + } + return new SurfaceControl(mSession, mName, mWidth, mHeight, mFormat, + mFlags, mParent, mWindowType, mOwnerUid); + } + + /** + * Set a debugging-name for the SurfaceControl. + * + * @param name A name to identify the Surface in debugging. + */ + public Builder setName(String name) { + mName = name; + return this; + } + + /** + * Set the initial size of the controlled surface's buffers in pixels. + * + * @param width The buffer width in pixels. + * @param height The buffer height in pixels. + */ + public Builder setSize(int width, int height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException( + "width and height must be positive"); + } + mWidth = width; + mHeight = height; + return this; + } + + /** + * Set the pixel format of the controlled surface's buffers, using constants from + * {@link android.graphics.PixelFormat}. + */ + public Builder setFormat(@PixelFormat.Format int format) { + mFormat = format; + return this; + } + + /** + * Specify if the app requires a hardware-protected path to + * an external display sync. If protected content is enabled, but + * such a path is not available, then the controlled Surface will + * not be displayed. + * + * @param protectedContent Whether to require a protected sink. + */ + public Builder setProtected(boolean protectedContent) { + if (protectedContent) { + mFlags |= PROTECTED_APP; + } else { + mFlags &= ~PROTECTED_APP; + } + return this; + } + + /** + * Specify whether the Surface contains secure content. If true, the system + * will prevent the surfaces content from being copied by another process. In + * particular screenshots and VNC servers will be disabled. This is however + * not a complete prevention of readback as {@link #setProtected}. + */ + public Builder setSecure(boolean secure) { + if (secure) { + mFlags |= SECURE; + } else { + mFlags &= ~SECURE; + } + return this; + } + + /** + * Indicates whether the surface must be considered opaque, + * even if its pixel format is set to translucent. This can be useful if an + * application needs full RGBA 8888 support for instance but will + * still draw every pixel opaque. + * <p> + * This flag only determines whether opacity will be sampled from the alpha channel. + * Plane-alpha from calls to setAlpha() can still result in blended composition + * regardless of the opaque setting. + * + * Combined effects are (assuming a buffer format with an alpha channel): + * <ul> + * <li>OPAQUE + alpha(1.0) == opaque composition + * <li>OPAQUE + alpha(0.x) == blended composition + * <li>OPAQUE + alpha(0.0) == no composition + * <li>!OPAQUE + alpha(1.0) == blended composition + * <li>!OPAQUE + alpha(0.x) == blended composition + * <li>!OPAQUE + alpha(0.0) == no composition + * </ul> + * If the underlying buffer lacks an alpha channel, it is as if setOpaque(true) + * were set automatically. + * @param opaque Whether the Surface is OPAQUE. + */ + public Builder setOpaque(boolean opaque) { + if (opaque) { + mFlags |= OPAQUE; + } else { + mFlags &= ~OPAQUE; + } + return this; + } + + /** + * Set a parent surface for our new SurfaceControl. + * + * Child surfaces are constrained to the onscreen region of their parent. + * Furthermore they stack relatively in Z order, and inherit the transformation + * of the parent. + * + * @param parent The parent control. + */ + public Builder setParent(SurfaceControl parent) { + mParent = parent; + return this; + } + + /** + * Set surface metadata. + * + * Currently these are window-types as per {@link WindowManager.LayoutParams} and + * owner UIDs. Child surfaces inherit their parents + * metadata so only the WindowManager needs to set this on root Surfaces. + * + * @param windowType A window-type + * @param ownerUid UID of the window owner. + */ + public Builder setMetadata(int windowType, int ownerUid) { + if (UserHandle.getAppId(Process.myUid()) != Process.SYSTEM_UID) { + throw new UnsupportedOperationException( + "It only makes sense to set Surface metadata from the WindowManager"); + } + mWindowType = windowType; + mOwnerUid = ownerUid; + return this; + } + + /** + * Indicate whether a 'ColorLayer' is to be constructed. + * + * Color layers will not have an associated BufferQueue and will instead always render a + * solid color (that is, solid before plane alpha). Currently that color is black. + * + * @param isColorLayer Whether to create a color layer. + */ + public Builder setColorLayer(boolean isColorLayer) { + if (isColorLayer) { + mFlags |= FX_SURFACE_DIM; + } else { + mFlags &= ~FX_SURFACE_DIM; + } + return this; + } + + /** + * Set 'Surface creation flags' such as {@link HIDDEN}, {@link SECURE}. + * + * TODO: Finish conversion to individual builder methods? + * @param flags The combined flags + */ + public Builder setFlags(int flags) { + mFlags = flags; + return this; + } + } + + /** * Create a surface with a name. * <p> * The surface creation flags specify what kind of surface to create and @@ -306,19 +547,7 @@ public class SurfaceControl { * * @throws throws OutOfResourcesException If the SurfaceControl cannot be created. */ - public SurfaceControl(SurfaceSession session, - String name, int w, int h, int format, int flags, int windowType, int ownerUid) - throws OutOfResourcesException { - this(session, name, w, h, format, flags, null, windowType, ownerUid); - } - - public SurfaceControl(SurfaceSession session, - String name, int w, int h, int format, int flags) - throws OutOfResourcesException { - this(session, name, w, h, format, flags, null, INVALID_WINDOW_TYPE, Binder.getCallingUid()); - } - - public SurfaceControl(SurfaceSession session, String name, int w, int h, int format, int flags, + private SurfaceControl(SurfaceSession session, String name, int w, int h, int format, int flags, SurfaceControl parent, int windowType, int ownerUid) throws OutOfResourcesException { if (session == null) { @@ -359,6 +588,37 @@ public class SurfaceControl { mCloseGuard.open("release"); } + private SurfaceControl(Parcel in) { + mName = in.readString(); + mNativeObject = nativeReadFromParcel(in); + if (mNativeObject == 0) { + throw new IllegalArgumentException("Couldn't read SurfaceControl from parcel=" + in); + } + mCloseGuard.open("release"); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + nativeWriteToParcel(mNativeObject, dest); + } + + public static final Creator<SurfaceControl> CREATOR + = new Creator<SurfaceControl>() { + public SurfaceControl createFromParcel(Parcel in) { + return new SurfaceControl(in); + } + + public SurfaceControl[] newArray(int size) { + return new SurfaceControl[size]; + } + }; + @Override protected void finalize() throws Throwable { try { @@ -373,11 +633,6 @@ public class SurfaceControl { } } - @Override - public String toString() { - return "Surface(name=" + mName + ")"; - } - /** * Release the local reference to the server-side surface. * Always call release() when you're done with a Surface. @@ -425,97 +680,154 @@ public class SurfaceControl { /** start a transaction */ public static void openTransaction() { - nativeOpenTransaction(); + synchronized (SurfaceControl.class) { + if (sGlobalTransaction == null) { + sGlobalTransaction = new Transaction(); + } + synchronized(SurfaceControl.class) { + sTransactionNestCount++; + } + } + } + + private static void closeTransaction(boolean sync) { + synchronized(SurfaceControl.class) { + if (sTransactionNestCount == 0) { + Log.e(TAG, "Call to SurfaceControl.closeTransaction without matching openTransaction"); + } else if (--sTransactionNestCount > 0) { + return; + } + sGlobalTransaction.apply(sync); + } + } + + /** + * Merge the supplied transaction in to the deprecated "global" transaction. + * This clears the supplied transaction in an identical fashion to {@link Transaction#merge}. + * <p> + * This is a utility for interop with legacy-code and will go away with the Global Transaction. + */ + @Deprecated + public static void mergeToGlobalTransaction(Transaction t) { + synchronized(SurfaceControl.class) { + sGlobalTransaction.merge(t); + } } /** end a transaction */ public static void closeTransaction() { - nativeCloseTransaction(false); + closeTransaction(false); } public static void closeTransactionSync() { - nativeCloseTransaction(true); + closeTransaction(true); } public void deferTransactionUntil(IBinder handle, long frame) { if (frame > 0) { - nativeDeferTransactionUntil(mNativeObject, handle, frame); + synchronized(SurfaceControl.class) { + sGlobalTransaction.deferTransactionUntil(this, handle, frame); + } } } public void deferTransactionUntil(Surface barrier, long frame) { if (frame > 0) { - nativeDeferTransactionUntilSurface(mNativeObject, barrier.mNativeObject, frame); + synchronized(SurfaceControl.class) { + sGlobalTransaction.deferTransactionUntilSurface(this, barrier, frame); + } } } public void reparentChildren(IBinder newParentHandle) { - nativeReparentChildren(mNativeObject, newParentHandle); + synchronized(SurfaceControl.class) { + sGlobalTransaction.reparentChildren(this, newParentHandle); + } + } + + public void reparent(IBinder newParentHandle) { + synchronized(SurfaceControl.class) { + sGlobalTransaction.reparent(this, newParentHandle); + } } public void detachChildren() { - nativeSeverChildren(mNativeObject); + synchronized(SurfaceControl.class) { + sGlobalTransaction.detachChildren(this); + } } public void setOverrideScalingMode(int scalingMode) { checkNotReleased(); - nativeSetOverrideScalingMode(mNativeObject, scalingMode); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setOverrideScalingMode(this, scalingMode); + } } public IBinder getHandle() { return nativeGetHandle(mNativeObject); } - /** flag the transaction as an animation */ public static void setAnimationTransaction() { - nativeSetAnimationTransaction(); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setAnimationTransaction(); + } } public void setLayer(int zorder) { checkNotReleased(); - nativeSetLayer(mNativeObject, zorder); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setLayer(this, zorder); + } } - public void setRelativeLayer(IBinder relativeTo, int zorder) { + public void setRelativeLayer(SurfaceControl relativeTo, int zorder) { checkNotReleased(); - nativeSetRelativeLayer(mNativeObject, relativeTo, zorder); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setRelativeLayer(this, relativeTo, zorder); + } } public void setPosition(float x, float y) { checkNotReleased(); - nativeSetPosition(mNativeObject, x, y); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setPosition(this, x, y); + } } - /** - * If the buffer size changes in this transaction, position and crop updates specified - * in this transaction will not complete until a buffer of the new size - * arrives. As transform matrix and size are already frozen in this fashion, - * this enables totally freezing the surface until the resize has completed - * (at which point the geometry influencing aspects of this transaction will then occur) - */ public void setGeometryAppliesWithResize() { checkNotReleased(); - nativeSetGeometryAppliesWithResize(mNativeObject); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setGeometryAppliesWithResize(this); + } } public void setSize(int w, int h) { checkNotReleased(); - nativeSetSize(mNativeObject, w, h); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setSize(this, w, h); + } } public void hide() { checkNotReleased(); - nativeSetFlags(mNativeObject, SURFACE_HIDDEN, SURFACE_HIDDEN); + synchronized(SurfaceControl.class) { + sGlobalTransaction.hide(this); + } } public void show() { checkNotReleased(); - nativeSetFlags(mNativeObject, 0, SURFACE_HIDDEN); + synchronized(SurfaceControl.class) { + sGlobalTransaction.show(this); + } } public void setTransparentRegionHint(Region region) { checkNotReleased(); - nativeSetTransparentRegionHint(mNativeObject, region); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setTransparentRegionHint(this, region); + } } public boolean clearContentFrameStats() { @@ -536,71 +848,86 @@ public class SurfaceControl { return nativeGetAnimationFrameStats(outStats); } - /** - * Sets an alpha value for the entire Surface. This value is combined with the - * per-pixel alpha. It may be used with opaque Surfaces. - */ public void setAlpha(float alpha) { checkNotReleased(); - nativeSetAlpha(mNativeObject, alpha); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setAlpha(this, alpha); + } + } + + public void setColor(@Size(3) float[] color) { + checkNotReleased(); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setColor(this, color); + } } public void setMatrix(float dsdx, float dtdx, float dtdy, float dsdy) { checkNotReleased(); - nativeSetMatrix(mNativeObject, dsdx, dtdx, dtdy, dsdy); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setMatrix(this, dsdx, dtdx, dtdy, dsdy); + } + } + + /** + * Sets the transform and position of a {@link SurfaceControl} from a 3x3 transformation matrix. + * + * @param matrix The matrix to apply. + * @param float9 An array of 9 floats to be used to extract the values from the matrix. + */ + public void setMatrix(Matrix matrix, float[] float9) { + checkNotReleased(); + matrix.getValues(float9); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setMatrix(this, float9[MSCALE_X], float9[MSKEW_Y], + float9[MSKEW_X], float9[MSCALE_Y]); + sGlobalTransaction.setPosition(this, float9[MTRANS_X], float9[MTRANS_Y]); + } } public void setWindowCrop(Rect crop) { checkNotReleased(); - if (crop != null) { - nativeSetWindowCrop(mNativeObject, - crop.left, crop.top, crop.right, crop.bottom); - } else { - nativeSetWindowCrop(mNativeObject, 0, 0, 0, 0); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setWindowCrop(this, crop); } } public void setFinalCrop(Rect crop) { checkNotReleased(); - if (crop != null) { - nativeSetFinalCrop(mNativeObject, - crop.left, crop.top, crop.right, crop.bottom); - } else { - nativeSetFinalCrop(mNativeObject, 0, 0, 0, 0); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setFinalCrop(this, crop); } } public void setLayerStack(int layerStack) { checkNotReleased(); - nativeSetLayerStack(mNativeObject, layerStack); + synchronized(SurfaceControl.class) { + sGlobalTransaction.setLayerStack(this, layerStack); + } } - /** - * Sets the opacity of the surface. Setting the flag is equivalent to creating the - * Surface with the {@link #OPAQUE} flag. - */ public void setOpaque(boolean isOpaque) { checkNotReleased(); - if (isOpaque) { - nativeSetFlags(mNativeObject, SURFACE_OPAQUE, SURFACE_OPAQUE); - } else { - nativeSetFlags(mNativeObject, 0, SURFACE_OPAQUE); + + synchronized (SurfaceControl.class) { + sGlobalTransaction.setOpaque(this, isOpaque); } } - /** - * Sets the security of the surface. Setting the flag is equivalent to creating the - * Surface with the {@link #SECURE} flag. - */ public void setSecure(boolean isSecure) { checkNotReleased(); - if (isSecure) { - nativeSetFlags(mNativeObject, SECURE, SECURE); - } else { - nativeSetFlags(mNativeObject, 0, SECURE); + + synchronized (SurfaceControl.class) { + sGlobalTransaction.setSecure(this, isSecure); } } + @Override + public String toString() { + return "Surface(name=" + mName + ")/@0x" + + Integer.toHexString(System.identityHashCode(this)); + } + /* * set display parameters. * needs to be inside open/closeTransaction block @@ -723,50 +1050,28 @@ public class SurfaceControl { public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setDisplayProjection(displayToken, orientation, + layerStackRect, displayRect); } - if (layerStackRect == null) { - throw new IllegalArgumentException("layerStackRect must not be null"); - } - if (displayRect == null) { - throw new IllegalArgumentException("displayRect must not be null"); - } - nativeSetDisplayProjection(displayToken, orientation, - layerStackRect.left, layerStackRect.top, layerStackRect.right, layerStackRect.bottom, - displayRect.left, displayRect.top, displayRect.right, displayRect.bottom); } public static void setDisplayLayerStack(IBinder displayToken, int layerStack) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setDisplayLayerStack(displayToken, layerStack); } - nativeSetDisplayLayerStack(displayToken, layerStack); } public static void setDisplaySurface(IBinder displayToken, Surface surface) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); - } - - if (surface != null) { - synchronized (surface.mLock) { - nativeSetDisplaySurface(displayToken, surface.mNativeObject); - } - } else { - nativeSetDisplaySurface(displayToken, 0); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setDisplaySurface(displayToken, surface); } } public static void setDisplaySize(IBinder displayToken, int width, int height) { - if (displayToken == null) { - throw new IllegalArgumentException("displayToken must not be null"); - } - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException("width and height must be positive"); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setDisplaySize(displayToken, width, height); } - - nativeSetDisplaySize(displayToken, width, height); } public static Display.HdrCapabilities getHdrCapabilities(IBinder displayToken) { @@ -844,7 +1149,9 @@ public class SurfaceControl { } /** - * Copy the current screen contents into a bitmap and return it. + * Copy the current screen contents into a hardware bitmap and return it. + * Note: If you want to modify the Bitmap in software, you will need to copy the Bitmap into + * a software Bitmap using {@link Bitmap#copy(Bitmap.Config, boolean)} * * CAVEAT: Versions of screenshot that return a {@link Bitmap} can * be extremely slow; avoid use unless absolutely necessary; prefer @@ -869,7 +1176,7 @@ public class SurfaceControl { * screenshots in its native portrait orientation by default, so this is * useful for returning screenshots that are independent of device * orientation. - * @return Returns a Bitmap containing the screen contents, or null + * @return Returns a hardware Bitmap containing the screen contents, or null * if an error occurs. Make sure to call Bitmap.recycle() as soon as * possible, once its content is not needed anymore. */ @@ -897,23 +1204,36 @@ public class SurfaceControl { } /** - * Like {@link SurfaceControl#screenshot(int, int, int, int, boolean)} but - * includes all Surfaces in the screenshot. + * Like {@link SurfaceControl#screenshot(Rect, int, int, int, int, boolean, int)} but + * includes all Surfaces in the screenshot. This will also update the orientation so it + * sends the correct coordinates to SF based on the rotation value. * + * @param sourceCrop The portion of the screen to capture into the Bitmap; + * caller may pass in 'new Rect()' if no cropping is desired. * @param width The desired width of the returned bitmap; the raw * screen will be scaled down to this size. * @param height The desired height of the returned bitmap; the raw * screen will be scaled down to this size. + * @param rotation Apply a custom clockwise rotation to the screenshot, i.e. + * Surface.ROTATION_0,90,180,270. Surfaceflinger will always take + * screenshots in its native portrait orientation by default, so this is + * useful for returning screenshots that are independent of device + * orientation. * @return Returns a Bitmap containing the screen contents, or null * if an error occurs. Make sure to call Bitmap.recycle() as soon as * possible, once its content is not needed anymore. */ - public static Bitmap screenshot(int width, int height) { + public static Bitmap screenshot(Rect sourceCrop, int width, int height, int rotation) { // TODO: should take the display as a parameter IBinder displayToken = SurfaceControl.getBuiltInDisplay( SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN); - return nativeScreenshot(displayToken, new Rect(), width, height, 0, 0, true, - false, Surface.ROTATION_0); + if (rotation == ROTATION_90 || rotation == ROTATION_270) { + rotation = (rotation == ROTATION_90) ? ROTATION_270 : ROTATION_90; + } + + SurfaceControl.rotateCropForSF(sourceCrop, rotation); + return nativeScreenshot(displayToken, sourceCrop, width, height, 0, 0, true, + false, rotation); } private static void screenshot(IBinder display, Surface consumer, Rect sourceCrop, @@ -928,4 +1248,327 @@ public class SurfaceControl { nativeScreenshot(display, consumer, sourceCrop, width, height, minLayer, maxLayer, allLayers, useIdentityTransform); } + + private static void rotateCropForSF(Rect crop, int rot) { + if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + int tmp = crop.top; + crop.top = crop.left; + crop.left = tmp; + tmp = crop.right; + crop.right = crop.bottom; + crop.bottom = tmp; + } + } + + /** + * Captures a layer and its children and returns a {@link GraphicBuffer} with the content. + * + * @param layerHandleToken The root layer to capture. + * @param sourceCrop The portion of the root surface to capture; caller may pass in 'new + * Rect()' or null if no cropping is desired. + * @param frameScale The desired scale of the returned buffer; the raw + * screen will be scaled up/down. + * + * @return Returns a GraphicBuffer that contains the layer capture. + */ + public static GraphicBuffer captureLayers(IBinder layerHandleToken, Rect sourceCrop, + float frameScale) { + return nativeCaptureLayers(layerHandleToken, sourceCrop, frameScale); + } + + public static class Transaction implements Closeable { + public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry( + Transaction.class.getClassLoader(), + nativeGetNativeTransactionFinalizer(), 512); + private long mNativeObject; + + Runnable mFreeNativeResources; + + public Transaction() { + mNativeObject = nativeCreateTransaction(); + mFreeNativeResources + = sRegistry.registerNativeAllocation(this, mNativeObject); + } + + /** + * Apply the transaction, clearing it's state, and making it usable + * as a new transaction. + */ + public void apply() { + apply(false); + } + + /** + * Close the transaction, if the transaction was not already applied this will cancel the + * transaction. + */ + @Override + public void close() { + mFreeNativeResources.run(); + mNativeObject = 0; + } + + /** + * Jankier version of apply. Avoid use (b/28068298). + */ + public void apply(boolean sync) { + nativeApplyTransaction(mNativeObject, sync); + } + + 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) { + 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 setMatrix(SurfaceControl sc, Matrix matrix, float[] float9) { + matrix.getValues(float9); + setMatrix(sc, float9[MSCALE_X], float9[MSKEW_Y], + float9[MSKEW_X], float9[MSCALE_Y]); + setPosition(sc, float9[MTRANS_X], float9[MTRANS_Y]); + 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); + } else { + nativeSetWindowCrop(mNativeObject, sc.mNativeObject, 0, 0, 0, 0); + } + + return this; + } + + public Transaction setFinalCrop(SurfaceControl sc, Rect crop) { + sc.checkNotReleased(); + if (crop != null) { + nativeSetFinalCrop(mNativeObject, sc.mNativeObject, + crop.left, crop.top, crop.right, crop.bottom); + } else { + nativeSetFinalCrop(mNativeObject, sc.mNativeObject, 0, 0, 0, 0); + } + + return this; + } + + 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) { + 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; + } + + /** + * Sets a color for the Surface. + * @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; + } + + /** + * If the buffer size changes in this transaction, position and crop updates specified + * in this transaction will not complete until a buffer of the new size + * arrives. As transform matrix and size are already frozen in this fashion, + * this enables totally freezing the surface until the resize has completed + * (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; + } + + /** + * Sets the security of the surface. Setting the flag is equivalent to creating the + * Surface with the {@link #SECURE} flag. + */ + public Transaction setSecure(SurfaceControl sc, boolean isSecure) { + sc.checkNotReleased(); + if (isSecure) { + nativeSetFlags(mNativeObject, sc.mNativeObject, SECURE, SECURE); + } else { + nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SECURE); + } + return this; + } + + /** + * Sets the opacity of the surface. Setting the flag is equivalent to creating the + * 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 { + nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SURFACE_OPAQUE); + } + return this; + } + + public Transaction setDisplaySurface(IBinder displayToken, Surface surface) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + + if (surface != null) { + synchronized (surface.mLock) { + nativeSetDisplaySurface(mNativeObject, displayToken, surface.mNativeObject); + } + } else { + nativeSetDisplaySurface(mNativeObject, displayToken, 0); + } + return this; + } + + public Transaction setDisplayLayerStack(IBinder displayToken, int layerStack) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + nativeSetDisplayLayerStack(mNativeObject, displayToken, layerStack); + return this; + } + + public Transaction setDisplayProjection(IBinder displayToken, + int orientation, Rect layerStackRect, Rect displayRect) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + if (layerStackRect == null) { + throw new IllegalArgumentException("layerStackRect must not be null"); + } + if (displayRect == null) { + throw new IllegalArgumentException("displayRect must not be null"); + } + nativeSetDisplayProjection(mNativeObject, displayToken, orientation, + layerStackRect.left, layerStackRect.top, layerStackRect.right, layerStackRect.bottom, + displayRect.left, displayRect.top, displayRect.right, displayRect.bottom); + return this; + } + + public Transaction setDisplaySize(IBinder displayToken, int width, int height) { + if (displayToken == null) { + throw new IllegalArgumentException("displayToken must not be null"); + } + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("width and height must be positive"); + } + + nativeSetDisplaySize(mNativeObject, displayToken, width, height); + return this; + } + + /** flag the transaction as an animation */ + public Transaction setAnimationTransaction() { + nativeSetAnimationTransaction(mNativeObject); + return this; + } + + /** + * Merge the other transaction into this transaction, clearing the + * other transaction as if it had been applied. + */ + public Transaction merge(Transaction other) { + nativeMergeTransaction(mNativeObject, other.mNativeObject); + return this; + } + } } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 462dad3fad7a..578679b12b9a 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -17,9 +17,9 @@ package android.view; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; -import static android.view.WindowManagerPolicy.APPLICATION_MEDIA_OVERLAY_SUBLAYER; -import static android.view.WindowManagerPolicy.APPLICATION_MEDIA_SUBLAYER; -import static android.view.WindowManagerPolicy.APPLICATION_PANEL_SUBLAYER; +import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER; +import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER; +import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER; import android.content.Context; import android.content.res.CompatibilityInfo.Translator; @@ -532,10 +532,15 @@ public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallb mDeferredDestroySurfaceControl = mSurfaceControl; updateOpaqueFlag(); - mSurfaceControl = new SurfaceControlWithBackground(mSurfaceSession, - "SurfaceView - " + viewRoot.getTitle().toString(), - mSurfaceWidth, mSurfaceHeight, mFormat, - mSurfaceFlags); + final String name = "SurfaceView - " + viewRoot.getTitle().toString(); + + mSurfaceControl = new SurfaceControlWithBackground( + name, + (mSurfaceFlags & SurfaceControl.OPAQUE) != 0, + new SurfaceControl.Builder(mSurfaceSession) + .setSize(mSurfaceWidth, mSurfaceHeight) + .setFormat(mFormat) + .setFlags(mSurfaceFlags)); } else if (mSurfaceControl == null) { return; } @@ -1098,13 +1103,15 @@ public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallb private boolean mOpaque = true; public boolean mVisible = false; - public SurfaceControlWithBackground(SurfaceSession s, - String name, int w, int h, int format, int flags) + public SurfaceControlWithBackground(String name, boolean opaque, SurfaceControl.Builder b) throws Exception { - super(s, name, w, h, format, flags); - mBackgroundControl = new SurfaceControl(s, "Background for - " + name, w, h, - PixelFormat.OPAQUE, flags | SurfaceControl.FX_SURFACE_DIM); - mOpaque = (flags & SurfaceControl.OPAQUE) != 0; + super(b.setName(name).build()); + + mBackgroundControl = b.setName("Background for -" + name) + .setFormat(OPAQUE) + .setColorLayer(true) + .build(); + mOpaque = opaque; } @Override diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index 489de565acb6..0a69772c3606 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/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. @@ -189,6 +190,17 @@ public final class ThreadedRenderer { public static final String DEBUG_SHOW_NON_RECTANGULAR_CLIP_PROPERTY = "debug.hwui.show_non_rect_clip"; + /** + * Sets the FPS devisor to lower the FPS. + * + * Sets a positive integer as a divisor. 1 (the default value) menas the full FPS, and 2 + * means half the full FPS. + * + * + * @hide + */ + public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor"; + static { // Try to check OpenGL support early if possible. isAvailable(); @@ -272,7 +284,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()); } /** @@ -914,6 +928,14 @@ public final class ThreadedRenderer { return nCreateHardwareBitmap(node.getNativeDisplayList(), width, height); } + /** + * Sets whether or not high contrast text rendering is enabled. The setting is global + * but only affects content rendered after the change is made. + */ + public static void setHighContrastText(boolean highContrastText) { + nSetHighContrastText(highContrastText); + } + @Override protected void finalize() throws Throwable { try { @@ -944,6 +966,9 @@ public final class ThreadedRenderer { if (mInitialized) return; mInitialized = true; mAppContext = context.getApplicationContext(); + + // b/68769804: For low FPS experiments. + setFPSDivisor(SystemProperties.getInt(DEBUG_FPS_DIVISOR, 1)); initSched(renderProxy); initGraphicsStats(); } @@ -996,10 +1021,17 @@ public final class ThreadedRenderer { observer.mNative = null; } + /** b/68769804: For low FPS experiments. */ + public static void setFPSDivisor(int divisor) { + if (divisor <= 0) divisor = 1; + Choreographer.getInstance().setFPSDivisor(divisor); + nHackySetRTAnimationsEnabled(divisor == 1); + } + /** 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); @@ -1063,4 +1095,7 @@ public final class ThreadedRenderer { int srcLeft, int srcTop, int srcRight, int srcBottom, Bitmap bitmap); private static native Bitmap nCreateHardwareBitmap(long renderNode, int width, int height); + private static native void nSetHighContrastText(boolean enabled); + // For temporary experimentation b/66945974 + private static native void nHackySetRTAnimationsEnabled(boolean enabled); } diff --git a/core/java/android/view/TouchDelegate.java b/core/java/android/view/TouchDelegate.java index cf36f4360c3b..dc50fa1d6036 100644 --- a/core/java/android/view/TouchDelegate.java +++ b/core/java/android/view/TouchDelegate.java @@ -44,7 +44,7 @@ public class TouchDelegate { /** * mBounds inflated to include some slop. This rect is to track whether the motion events - * should be considered to be be within the delegate view. + * should be considered to be within the delegate view. */ private Rect mSlopBounds; @@ -64,14 +64,12 @@ public class TouchDelegate { public static final int BELOW = 2; /** - * The touchable region of the View extends to the left of its - * actual extent. + * The touchable region of the View extends to the left of its actual extent. */ public static final int TO_LEFT = 4; /** - * The touchable region of the View extends to the right of its - * actual extent. + * The touchable region of the View extends to the right of its actual extent. */ public static final int TO_RIGHT = 8; @@ -108,28 +106,24 @@ public class TouchDelegate { boolean handled = false; switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - Rect bounds = mBounds; - - if (bounds.contains(x, y)) { - mDelegateTargeted = true; - sendToDelegate = true; - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_MOVE: - sendToDelegate = mDelegateTargeted; - if (sendToDelegate) { - Rect slopBounds = mSlopBounds; - if (!slopBounds.contains(x, y)) { - hit = false; + case MotionEvent.ACTION_DOWN: + mDelegateTargeted = mBounds.contains(x, y); + sendToDelegate = mDelegateTargeted; + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_MOVE: + sendToDelegate = mDelegateTargeted; + if (sendToDelegate) { + Rect slopBounds = mSlopBounds; + if (!slopBounds.contains(x, y)) { + hit = false; + } } - } - break; - case MotionEvent.ACTION_CANCEL: - sendToDelegate = mDelegateTargeted; - mDelegateTargeted = false; - break; + break; + case MotionEvent.ACTION_CANCEL: + sendToDelegate = mDelegateTargeted; + mDelegateTargeted = false; + break; } if (sendToDelegate) { final View delegateView = mDelegateView; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 166d6b7a5b1c..623759e178d8 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -893,6 +893,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static boolean sAutoFocusableOffUIThreadWontNotifyParents; + /** + * Prior to P things like setScaleX() allowed passing float values that were bogus such as + * Float.NaN. If the app is targetting P or later then passing these values will result in an + * exception being thrown. If the app is targetting an earlier SDK version, then we will + * silently clamp these values to avoid crashes elsewhere when the rendering code hits + * these bogus values. + */ + private static boolean sThrowOnInvalidFloatProperties; + /** @hide */ @IntDef({NOT_FOCUSABLE, FOCUSABLE, FOCUSABLE_AUTO}) @Retention(RetentionPolicy.SOURCE) @@ -1448,17 +1457,59 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * <p>Enables low quality mode for the drawing cache.</p> + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public static final int DRAWING_CACHE_QUALITY_LOW = 0x00080000; /** * <p>Enables high quality mode for the drawing cache.</p> + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public static final int DRAWING_CACHE_QUALITY_HIGH = 0x00100000; /** * <p>Enables automatic quality mode for the drawing cache.</p> + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public static final int DRAWING_CACHE_QUALITY_AUTO = 0x00000000; private static final int[] DRAWING_CACHE_QUALITY_FLAGS = { @@ -2300,9 +2351,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final int PFLAG_HOVERED = 0x10000000; /** - * no longer needed, should be reused + * Flag set by {@link AutofillManager} if it needs to be notified when this view is clicked. */ - private static final int PFLAG_DOES_NOTHING_REUSE_PLEASE = 0x20000000; + private static final int PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK = 0x20000000; /** {@hide} */ static final int PFLAG_ACTIVATED = 0x40000000; @@ -2887,6 +2938,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 1 PFLAG3_TEMPORARY_DETACH * 1 PFLAG3_NO_REVEAL_ON_FOCUS * 1 PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT + * 1 PFLAG3_SCREEN_READER_FOCUSABLE * |-------|-------|-------|-------| */ @@ -3167,6 +3219,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT = 0x8000000; + /** + * Works like focusable for screen readers, but without the side effects on input focus. + * @see #setScreenReaderFocusable(boolean) + */ + private static final int PFLAG3_SCREEN_READER_FOCUSABLE = 0x10000000; + /* End of masks for mPrivateFlags3 */ /** @@ -3394,6 +3452,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} but not * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION * FLAG_TRANSLUCENT_NAVIGATION}. + * + * @see android.R.attr#windowLightNavigationBar */ public static final int SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR = 0x00000010; @@ -3740,15 +3800,90 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ @ViewDebug.ExportedProperty(flagMapping = { - @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LOW_PROFILE, - equals = SYSTEM_UI_FLAG_LOW_PROFILE, - name = "SYSTEM_UI_FLAG_LOW_PROFILE", outputIf = true), - @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_HIDE_NAVIGATION, - equals = SYSTEM_UI_FLAG_HIDE_NAVIGATION, - name = "SYSTEM_UI_FLAG_HIDE_NAVIGATION", outputIf = true), - @ViewDebug.FlagToString(mask = PUBLIC_STATUS_BAR_VISIBILITY_MASK, - equals = SYSTEM_UI_FLAG_VISIBLE, - name = "SYSTEM_UI_FLAG_VISIBLE", outputIf = true) + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LOW_PROFILE, + equals = SYSTEM_UI_FLAG_LOW_PROFILE, + name = "LOW_PROFILE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_HIDE_NAVIGATION, + equals = SYSTEM_UI_FLAG_HIDE_NAVIGATION, + name = "HIDE_NAVIGATION"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_FULLSCREEN, + equals = SYSTEM_UI_FLAG_FULLSCREEN, + name = "FULLSCREEN"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_STABLE, + equals = SYSTEM_UI_FLAG_LAYOUT_STABLE, + name = "LAYOUT_STABLE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION, + equals = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION, + name = "LAYOUT_HIDE_NAVIGATION"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, + equals = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, + name = "LAYOUT_FULLSCREEN"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_IMMERSIVE, + equals = SYSTEM_UI_FLAG_IMMERSIVE, + name = "IMMERSIVE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_IMMERSIVE_STICKY, + equals = SYSTEM_UI_FLAG_IMMERSIVE_STICKY, + name = "IMMERSIVE_STICKY"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LIGHT_STATUS_BAR, + equals = SYSTEM_UI_FLAG_LIGHT_STATUS_BAR, + name = "LIGHT_STATUS_BAR"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, + equals = SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, + name = "LIGHT_NAVIGATION_BAR"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_EXPAND, + equals = STATUS_BAR_DISABLE_EXPAND, + name = "STATUS_BAR_DISABLE_EXPAND"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_ICONS, + equals = STATUS_BAR_DISABLE_NOTIFICATION_ICONS, + name = "STATUS_BAR_DISABLE_NOTIFICATION_ICONS"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_ALERTS, + equals = STATUS_BAR_DISABLE_NOTIFICATION_ALERTS, + name = "STATUS_BAR_DISABLE_NOTIFICATION_ALERTS"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_TICKER, + equals = STATUS_BAR_DISABLE_NOTIFICATION_TICKER, + name = "STATUS_BAR_DISABLE_NOTIFICATION_TICKER"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_SYSTEM_INFO, + equals = STATUS_BAR_DISABLE_SYSTEM_INFO, + name = "STATUS_BAR_DISABLE_SYSTEM_INFO"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_HOME, + equals = STATUS_BAR_DISABLE_HOME, + name = "STATUS_BAR_DISABLE_HOME"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_BACK, + equals = STATUS_BAR_DISABLE_BACK, + name = "STATUS_BAR_DISABLE_BACK"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_CLOCK, + equals = STATUS_BAR_DISABLE_CLOCK, + name = "STATUS_BAR_DISABLE_CLOCK"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_RECENT, + equals = STATUS_BAR_DISABLE_RECENT, + name = "STATUS_BAR_DISABLE_RECENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_SEARCH, + equals = STATUS_BAR_DISABLE_SEARCH, + name = "STATUS_BAR_DISABLE_SEARCH"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSIENT, + equals = STATUS_BAR_TRANSIENT, + name = "STATUS_BAR_TRANSIENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSIENT, + equals = NAVIGATION_BAR_TRANSIENT, + name = "NAVIGATION_BAR_TRANSIENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_UNHIDE, + equals = STATUS_BAR_UNHIDE, + name = "STATUS_BAR_UNHIDE"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_UNHIDE, + equals = NAVIGATION_BAR_UNHIDE, + name = "NAVIGATION_BAR_UNHIDE"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSLUCENT, + equals = STATUS_BAR_TRANSLUCENT, + name = "STATUS_BAR_TRANSLUCENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSLUCENT, + equals = NAVIGATION_BAR_TRANSLUCENT, + name = "NAVIGATION_BAR_TRANSLUCENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSPARENT, + equals = NAVIGATION_BAR_TRANSPARENT, + name = "NAVIGATION_BAR_TRANSPARENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSPARENT, + equals = STATUS_BAR_TRANSPARENT, + name = "STATUS_BAR_TRANSPARENT") }, formatToHexString = true) int mSystemUiVisibility; @@ -4126,6 +4261,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, OnApplyWindowInsetsListener mOnApplyWindowInsetsListener; OnCapturedPointerListener mOnCapturedPointerListener; + + private ArrayList<OnKeyFallbackListener> mKeyFallbackListeners; } ListenerInfo mListenerInfo; @@ -4624,6 +4761,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, sUseDefaultFocusHighlight = context.getResources().getBoolean( com.android.internal.R.bool.config_useDefaultFocusHighlight); + sThrowOnInvalidFloatProperties = targetSdkVersion >= Build.VERSION_CODES.P; + sCompatibilityDone = true; } } @@ -5223,6 +5362,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, setDefaultFocusHighlightEnabled(a.getBoolean(attr, true)); } break; + case R.styleable.View_screenReaderFocusable: + if (a.peekValue(attr) != null) { + setScreenReaderFocusable(a.getBoolean(attr, false)); + } + break; } } @@ -6278,6 +6422,42 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return null; } + /** @hide */ + public void setNotifyAutofillManagerOnClick(boolean notify) { + if (notify) { + mPrivateFlags |= PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK; + } else { + mPrivateFlags &= ~PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK; + } + } + + private void notifyAutofillManagerOnClick() { + if ((mPrivateFlags & PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK) != 0) { + try { + getAutofillManager().notifyViewClicked(this); + } finally { + // Set it to already called so it's not called twice when called by + // performClickInternal() + mPrivateFlags |= ~PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK; + } + } + } + + /** + * Entry point for {@link #performClick()} - other methods on View should call it instead of + * {@code performClick()} directly to make sure the autofill manager is notified when + * necessary (as subclasses could extend {@code performClick()} without calling the parent's + * method). + */ + private boolean performClickInternal() { + // Must notify autofill manager before performing the click actions to avoid scenarios where + // the app has a click listener that changes the state of views the autofill service might + // be interested on. + notifyAutofillManagerOnClick(); + + return performClick(); + } + /** * Call this view's OnClickListener, if it is defined. Performs all normal * actions associated with clicking: reporting accessibility event, playing @@ -6286,7 +6466,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ + // NOTE: other methods on View should not call this method directly, but performClickInternal() + // instead, to guarantee that the autofill manager is notified when necessary (as subclasses + // could extend this method without calling super.performClick()). public boolean performClick() { + // We still need to call this method to handle the cases where performClick() was called + // externally, instead of through performClickInternal() + notifyAutofillManagerOnClick(); + final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { @@ -6896,8 +7083,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } - // Invisible and gone views are never focusable. - if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) { + // Invisible, gone, or disabled views are never focusable. + if ((mViewFlags & VISIBILITY_MASK) != VISIBLE + || (mViewFlags & ENABLED_MASK) != ENABLED) { return false; } @@ -7658,8 +7846,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <li>Call * {@link android.view.autofill.AutofillManager#notifyValueChanged(View, int, AutofillValue)} * when the value of a virtual child changed. - * <li>Call - * {@link + * <li>Call {@link * android.view.autofill.AutofillManager#notifyViewVisibilityChanged(View, int, boolean)} * when the visibility of a virtual child changed. * <li>Call {@link AutofillManager#commit()} when the autofill context of the view structure @@ -8230,6 +8417,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, info.setEnabled(isEnabled()); info.setClickable(isClickable()); info.setFocusable(isFocusable()); + info.setScreenReaderFocusable(isScreenReaderFocusable()); info.setFocused(isFocused()); info.setAccessibilityFocused(isAccessibilityFocused()); info.setSelected(isSelected()); @@ -8830,7 +9018,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #isDrawingCacheEnabled() * * @attr ref android.R.styleable#View_drawingCacheQuality + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated @DrawingCacheQuality public int getDrawingCacheQuality() { return mViewFlags & DRAWING_CACHE_QUALITY_MASK; @@ -8848,7 +9050,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #isDrawingCacheEnabled() * * @attr ref android.R.styleable#View_drawingCacheQuality + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void setDrawingCacheQuality(@DrawingCacheQuality int quality) { setFlags(quality, DRAWING_CACHE_QUALITY_MASK); } @@ -10158,6 +10374,45 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Returns whether the view should be treated as a focusable unit by screen reader + * accessibility tools. + * @see #setScreenReaderFocusable(boolean) + * + * @return Whether the view should be treated as a focusable unit by screen reader. + */ + public boolean isScreenReaderFocusable() { + return (mPrivateFlags3 & PFLAG3_SCREEN_READER_FOCUSABLE) != 0; + } + + /** + * When screen readers (one type of accessibility tool) decide what should be read to the + * user, they typically look for input focusable ({@link #isFocusable()}) parents of + * non-focusable text items, and read those focusable parents and their non-focusable children + * as a unit. In some situations, this behavior is desirable for views that should not take + * input focus. Setting an item to be screen reader focusable requests that the view be + * treated as a unit by screen readers without any effect on input focusability. The default + * value of {@code false} lets screen readers use other signals, like focusable, to determine + * how to group items. + * + * @param screenReaderFocusable Whether the view should be treated as a unit by screen reader + * accessibility tools. + */ + public void setScreenReaderFocusable(boolean screenReaderFocusable) { + int pflags3 = mPrivateFlags3; + if (screenReaderFocusable) { + pflags3 |= PFLAG3_SCREEN_READER_FOCUSABLE; + } else { + pflags3 &= ~PFLAG3_SCREEN_READER_FOCUSABLE; + } + + if (pflags3 != mPrivateFlags3) { + mPrivateFlags3 = pflags3; + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + } + } + + /** * Find the nearest view in the specified direction that can take focus. * This does not actually give focus to that view. * @@ -10527,7 +10782,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (views == null) { return; } - if (!isFocusable()) { + if (!isFocusable() || !isEnabled()) { return; } if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE @@ -10850,7 +11105,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // need to be focusable if ((mViewFlags & FOCUSABLE) != FOCUSABLE - || (mViewFlags & VISIBILITY_MASK) != VISIBLE) { + || (mViewFlags & VISIBILITY_MASK) != VISIBLE + || (mViewFlags & ENABLED_MASK) != ENABLED) { return false; } @@ -11355,7 +11611,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: { if (isClickable()) { - performClick(); + performClickInternal(); return true; } } break; @@ -12467,7 +12723,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // This is a tap, so remove the longpress check removeLongPressCallback(); if (!event.isCanceled()) { - return performClick(); + return performClickInternal(); } } } @@ -13039,7 +13295,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { - performClick(); + performClickInternal(); } } } @@ -13203,17 +13459,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Remove the pending callback for sending a - * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event. - */ - private void removeSendViewScrolledAccessibilityEventCallback() { - if (mSendViewScrolledAccessibilityEvent != null) { - removeCallbacks(mSendViewScrolledAccessibilityEvent); - mSendViewScrolledAccessibilityEvent.mIsPending = false; - } - } - - /** * Sets the TouchDelegate for this View. */ public void setTouchDelegate(TouchDelegate delegate) { @@ -13331,12 +13576,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // about in case nothing has focus. even if this specific view // isn't focusable, it may contain something that is, so let // the root view try to give this focus if nothing else does. - if ((mParent != null) && (mBottom > mTop) && (mRight > mLeft)) { + if ((mParent != null) && ((mViewFlags & ENABLED_MASK) == ENABLED) + && (mBottom > mTop) && (mRight > mLeft)) { mParent.focusableViewAvailable(this); } } } + if ((changed & ENABLED_MASK) != 0) { + if ((mViewFlags & ENABLED_MASK) == ENABLED) { + // a view becoming enabled should notify the parent as long as the view is also + // visible and the parent wasn't already notified by becoming visible during this + // setFlags invocation. + if ((mViewFlags & VISIBILITY_MASK) == VISIBLE + && ((changed & VISIBILITY_MASK) == 0)) { + if ((mParent != null) && (mViewFlags & ENABLED_MASK) == ENABLED) { + mParent.focusableViewAvailable(this); + } + } + } else { + if (hasFocus()) clearFocus(); + } + } + /* Check if the GONE bit has changed */ if ((changed & GONE) != 0) { needGlobalAttributesUpdate(false); @@ -13506,7 +13768,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, notifySubtreeAccessibilityStateChangedIfNeeded(); if (AccessibilityManager.getInstance(mContext).isEnabled()) { - postSendViewScrolledAccessibilityEventCallback(); + postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt); } mBackgroundSizeChanged = true; @@ -14021,6 +14283,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setScaleX(float scaleX) { if (scaleX != getScaleX()) { + scaleX = sanitizeFloatPropertyValue(scaleX, "scaleX"); invalidateViewProperty(true, false); mRenderNode.setScaleX(scaleX); invalidateViewProperty(false, true); @@ -14057,6 +14320,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setScaleY(float scaleY) { if (scaleY != getScaleY()) { + scaleY = sanitizeFloatPropertyValue(scaleY, "scaleY"); invalidateViewProperty(true, false); mRenderNode.setScaleY(scaleY); invalidateViewProperty(false, true); @@ -14606,6 +14870,43 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + private static float sanitizeFloatPropertyValue(float value, String propertyName) { + return sanitizeFloatPropertyValue(value, propertyName, -Float.MAX_VALUE, Float.MAX_VALUE); + } + + private static float sanitizeFloatPropertyValue(float value, String propertyName, + float min, float max) { + // The expected "nothing bad happened" path + if (value >= min && value <= max) return value; + + if (value < min || value == Float.NEGATIVE_INFINITY) { + if (sThrowOnInvalidFloatProperties) { + throw new IllegalArgumentException("Cannot set '" + propertyName + "' to " + + value + ", the value must be >= " + min); + } + return min; + } + + if (value > max || value == Float.POSITIVE_INFINITY) { + if (sThrowOnInvalidFloatProperties) { + throw new IllegalArgumentException("Cannot set '" + propertyName + "' to " + + value + ", the value must be <= " + max); + } + return max; + } + + if (Float.isNaN(value)) { + if (sThrowOnInvalidFloatProperties) { + throw new IllegalArgumentException( + "Cannot set '" + propertyName + "' to Float.NaN"); + } + return 0; // Unclear which direction this NaN went so... 0? + } + + // Shouldn't be possible to reach this. + throw new IllegalStateException("How do you get here?? " + value); + } + /** * The visual x position of this view, in pixels. This is equivalent to the * {@link #setTranslationX(float) translationX} property plus the current @@ -14692,6 +14993,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setElevation(float elevation) { if (elevation != getElevation()) { + elevation = sanitizeFloatPropertyValue(elevation, "elevation"); invalidateViewProperty(true, false); mRenderNode.setElevation(elevation); invalidateViewProperty(false, true); @@ -14784,6 +15086,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void setTranslationZ(float translationZ) { if (translationZ != getTranslationZ()) { + translationZ = sanitizeFloatPropertyValue(translationZ, "translationZ"); invalidateViewProperty(true, false); mRenderNode.setTranslationZ(translationZ); invalidateViewProperty(false, true); @@ -14972,6 +15275,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) { @@ -15445,7 +15757,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * {@code dirty}. * * @param dirty the rectangle representing the bounds of the dirty region + * + * @deprecated The switch to hardware accelerated rendering in API 14 reduced + * the importance of the dirty rectangle. In API 21 the given rectangle is + * ignored entirely in favor of an internally-calculated area instead. + * Because of this, clients are encouraged to just call {@link #invalidate()}. */ + @Deprecated public void invalidate(Rect dirty) { final int scrollX = mScrollX; final int scrollY = mScrollY; @@ -15466,7 +15784,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param t the top position of the dirty region * @param r the right position of the dirty region * @param b the bottom position of the dirty region + * + * @deprecated The switch to hardware accelerated rendering in API 14 reduced + * the importance of the dirty rectangle. In API 21 the given rectangle is + * ignored entirely in favor of an internally-calculated area instead. + * Because of this, clients are encouraged to just call {@link #invalidate()}. */ + @Deprecated public void invalidate(int l, int t, int r, int b) { final int scrollX = mScrollX; final int scrollY = mScrollY; @@ -16036,15 +16360,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * This event is sent at most once every * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}. */ - private void postSendViewScrolledAccessibilityEventCallback() { + private void postSendViewScrolledAccessibilityEventCallback(int dx, int dy) { if (mSendViewScrolledAccessibilityEvent == null) { mSendViewScrolledAccessibilityEvent = new SendViewScrolledAccessibilityEvent(); } - if (!mSendViewScrolledAccessibilityEvent.mIsPending) { - mSendViewScrolledAccessibilityEvent.mIsPending = true; - postDelayed(mSendViewScrolledAccessibilityEvent, - ViewConfiguration.getSendRecurringAccessibilityEventsInterval()); - } + mSendViewScrolledAccessibilityEvent.post(dx, dy); } /** @@ -17302,7 +17622,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, removeUnsetPressCallback(); removeLongPressCallback(); removePerformClickCallback(); - removeSendViewScrolledAccessibilityEventCallback(); + cancel(mSendViewScrolledAccessibilityEvent); stopNestedScroll(); // Anything that started animating right before detach should already @@ -18011,7 +18331,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #getDrawingCache() * @see #buildDrawingCache() * @see #setLayerType(int, android.graphics.Paint) + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void setDrawingCacheEnabled(boolean enabled) { mCachingFailed = false; setFlags(enabled ? DRAWING_CACHE_ENABLED : 0, DRAWING_CACHE_ENABLED); @@ -18024,7 +18358,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #setDrawingCacheEnabled(boolean) * @see #getDrawingCache() + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated @ViewDebug.ExportedProperty(category = "drawing") public boolean isDrawingCacheEnabled() { return (mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED; @@ -18038,10 +18386,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @SuppressWarnings({"UnusedDeclaration"}) public void outputDirtyFlags(String indent, boolean clear, int clearMask) { - Log.d("View", indent + this + " DIRTY(" + (mPrivateFlags & View.PFLAG_DIRTY_MASK) + - ") DRAWN(" + (mPrivateFlags & PFLAG_DRAWN) + ")" + " CACHE_VALID(" + - (mPrivateFlags & View.PFLAG_DRAWING_CACHE_VALID) + - ") INVALIDATED(" + (mPrivateFlags & PFLAG_INVALIDATED) + ")"); + Log.d(VIEW_LOG_TAG, indent + this + " DIRTY(" + + (mPrivateFlags & View.PFLAG_DIRTY_MASK) + + ") DRAWN(" + (mPrivateFlags & PFLAG_DRAWN) + ")" + " CACHE_VALID(" + + (mPrivateFlags & View.PFLAG_DRAWING_CACHE_VALID) + + ") INVALIDATED(" + (mPrivateFlags & PFLAG_INVALIDATED) + ")"); if (clear) { mPrivateFlags &= clearMask; } @@ -18112,7 +18461,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int layerType = getLayerType(); final DisplayListCanvas canvas = renderNode.start(width, height); - canvas.setHighContrastText(mAttachInfo.mHighContrastText); try { if (layerType == LAYER_TYPE_SOFTWARE) { @@ -18166,7 +18514,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return A non-scaled bitmap representing this view or null if cache is disabled. * * @see #getDrawingCache(boolean) + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public Bitmap getDrawingCache() { return getDrawingCache(false); } @@ -18197,7 +18559,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #isDrawingCacheEnabled() * @see #buildDrawingCache(boolean) * @see #destroyDrawingCache() + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public Bitmap getDrawingCache(boolean autoScale) { if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) { return null; @@ -18217,7 +18593,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #setDrawingCacheEnabled(boolean) * @see #buildDrawingCache() * @see #getDrawingCache() + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void destroyDrawingCache() { if (mDrawingCache != null) { mDrawingCache.recycle(); @@ -18239,7 +18629,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #setDrawingCacheEnabled(boolean) * @see #buildDrawingCache() * @see #getDrawingCache() + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void setDrawingCacheBackgroundColor(@ColorInt int color) { if (color != mDrawingCacheBackgroundColor) { mDrawingCacheBackgroundColor = color; @@ -18251,7 +18655,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #setDrawingCacheBackgroundColor(int) * * @return The background color to used for the drawing cache's bitmap + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated @ColorInt public int getDrawingCacheBackgroundColor() { return mDrawingCacheBackgroundColor; @@ -18261,7 +18679,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <p>Calling this method is equivalent to calling <code>buildDrawingCache(false)</code>.</p> * * @see #buildDrawingCache(boolean) + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void buildDrawingCache() { buildDrawingCache(false); } @@ -18288,7 +18720,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #getDrawingCache() * @see #destroyDrawingCache() + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void buildDrawingCache(boolean autoScale) { if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ? mDrawingCache == null : mUnscaledDrawingCache == null)) { @@ -19721,7 +20167,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, boolean changed = false; if (DBG) { - Log.d("View", this + " View.setFrame(" + left + "," + top + "," + Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")"); } @@ -24767,7 +25213,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private final class PerformClick implements Runnable { @Override public void run() { - performClick(); + performClickInternal(); } } @@ -24841,6 +25287,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Interface definition for a callback to be invoked when a hardware key event is + * dispatched to this view during the fallback phase. This means no view in the hierarchy + * has handled this event. + */ + public interface OnKeyFallbackListener { + /** + * Called when a hardware key is dispatched to a view in the fallback phase. This allows + * listeners to respond to events after the view hierarchy has had a chance to respond. + * <p>Key presses in software keyboards will generally NOT trigger this method, + * although some may elect to do so in some situations. Do not assume a + * software input method has to be key-based; even if it is, it may use key presses + * in a different way than you expect, so there is no way to reliably catch soft + * input key presses. + * + * @param v The view the key has been dispatched to. + * @param event The KeyEvent object containing full information about + * the event. + * @return True if the listener has consumed the event, false otherwise. + */ + boolean onKeyFallback(View v, KeyEvent event); + } + + /** * Interface definition for a callback to be invoked when a touch event is * dispatched to this view. The callback will be invoked before the touch * event is given to the view. @@ -25288,6 +25757,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ final Rect mStableInsets = new Rect(); + final DisplayCutout.ParcelableWrapper mDisplayCutout = + new DisplayCutout.ParcelableWrapper(DisplayCutout.NO_CUTOUT); + /** * For windows that include areas that are not covered by real surface these are the outsets * for real surface. @@ -25422,11 +25894,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, boolean mViewScrollChanged; /** - * Set to true if high contrast mode enabled - */ - boolean mHighContrastText; - - /** * Set to true if a pointer event is currently being handled. */ boolean mHandlingPointerEvent; @@ -25743,14 +26210,48 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private class SendViewScrolledAccessibilityEvent implements Runnable { public volatile boolean mIsPending; + public int mDeltaX; + public int mDeltaY; + public void post(int dx, int dy) { + mDeltaX += dx; + mDeltaY += dy; + if (!mIsPending) { + mIsPending = true; + postDelayed(this, ViewConfiguration.getSendRecurringAccessibilityEventsInterval()); + } + } + + @Override public void run() { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_VIEW_SCROLLED); + event.setScrollDeltaX(mDeltaX); + event.setScrollDeltaY(mDeltaY); + sendAccessibilityEventUnchecked(event); + } + reset(); + } + + private void reset() { mIsPending = false; + mDeltaX = 0; + mDeltaY = 0; } } /** + * Remove the pending callback for sending a + * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event. + */ + private void cancel(@Nullable SendViewScrolledAccessibilityEvent callback) { + if (callback == null || !callback.mIsPending) return; + removeCallbacks(callback); + callback.reset(); + } + + /** * <p> * This class represents a delegate that can be registered in a {@link View} * to enhance accessibility support via composition rather via inheritance. @@ -26484,4 +26985,56 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } return mTooltipInfo.mTooltipPopup.getContentView(); } + + /** + * Allows this view to handle {@link KeyEvent}s which weren't handled by normal dispatch. This + * occurs after the normal view hierarchy dispatch, but before the window callback. By default, + * this will dispatch into all the listeners registered via + * {@link #addKeyFallbackListener(OnKeyFallbackListener)} in last-in-first-out order (most + * recently added will receive events first). + * + * @param event A not-previously-handled event. + * @return {@code true} if the event was handled, {@code false} otherwise. + * @see #addKeyFallbackListener + */ + public boolean onKeyFallback(@NonNull KeyEvent event) { + if (mListenerInfo != null && mListenerInfo.mKeyFallbackListeners != null) { + for (int i = mListenerInfo.mKeyFallbackListeners.size() - 1; i >= 0; --i) { + if (mListenerInfo.mKeyFallbackListeners.get(i).onKeyFallback(this, event)) { + return true; + } + } + } + return false; + } + + /** + * Adds a listener which will receive unhandled {@link KeyEvent}s. + * @param listener the receiver of fallback {@link KeyEvent}s. + * @see #onKeyFallback(KeyEvent) + */ + public void addKeyFallbackListener(OnKeyFallbackListener listener) { + ArrayList<OnKeyFallbackListener> fallbacks = getListenerInfo().mKeyFallbackListeners; + if (fallbacks == null) { + fallbacks = new ArrayList<>(); + getListenerInfo().mKeyFallbackListeners = fallbacks; + } + fallbacks.add(listener); + } + + /** + * Removes a listener which will receive unhandled {@link KeyEvent}s. + * @param listener the receiver of fallback {@link KeyEvent}s. + * @see #onKeyFallback(KeyEvent) + */ + public void removeKeyFallbackListener(OnKeyFallbackListener listener) { + if (mListenerInfo != null) { + if (mListenerInfo.mKeyFallbackListeners != null) { + mListenerInfo.mKeyFallbackListeners.remove(listener); + if (mListenerInfo.mKeyFallbackListeners.isEmpty()) { + mListenerInfo.mKeyFallbackListeners = null; + } + } + } + } } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 574137b30f1e..c44c8dda83a9 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -84,12 +84,17 @@ public class ViewConfiguration { /** * Defines the duration in milliseconds a user needs to hold down the - * appropriate button to bring up the accessibility shortcut (first time) or enable it - * (once shortcut is configured). + * appropriate button to bring up the accessibility shortcut for the first time */ private static final int A11Y_SHORTCUT_KEY_TIMEOUT = 3000; /** + * 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 = 1000; + + /** * Defines the duration in milliseconds we will wait to see if a touch event * is a tap or a scroll. If the user does not move within this interval, it is * considered to be a tap. @@ -851,6 +856,15 @@ public class ViewConfiguration { } /** + * @return The amount of time a user needs to press the relevant keys to activate the + * accessibility shortcut after it's confirmed that accessibility shortcut is used. + * @hide + */ + public long getAccessibilityShortcutKeyTimeoutAfterConfirmation() { + return A11Y_SHORTCUT_KEY_TIMEOUT_AFTER_CONFIRMATION; + } + + /** * The amount of friction applied to scrolls and flings. * * @return A scalar dimensionless value representing the coefficient of diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 66c05785d6a9..afa941316be7 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -528,84 +528,23 @@ public class ViewDebug { /** @hide */ public static void profileViewAndChildren(final View view, BufferedWriter out) throws IOException { - profileViewAndChildren(view, out, true); + RenderNode node = RenderNode.create("ViewDebug", null); + profileViewAndChildren(view, node, out, true); + node.destroy(); } - private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root) - throws IOException { - + private static void profileViewAndChildren(View view, RenderNode node, BufferedWriter out, + boolean root) throws IOException { long durationMeasure = (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0) - ? profileViewOperation(view, new ViewOperation<Void>() { - public Void[] pre() { - forceLayout(view); - return null; - } - - private void forceLayout(View view) { - view.forceLayout(); - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - final int count = group.getChildCount(); - for (int i = 0; i < count; i++) { - forceLayout(group.getChildAt(i)); - } - } - } - - public void run(Void... data) { - view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec); - } - - public void post(Void... data) { - } - }) - : 0; + ? profileViewMeasure(view) : 0; long durationLayout = (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0) - ? profileViewOperation(view, new ViewOperation<Void>() { - public Void[] pre() { - return null; - } - - public void run(Void... data) { - view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom); - } - - public void post(Void... data) { - } - }) : 0; + ? profileViewLayout(view) : 0; long durationDraw = (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0) - ? profileViewOperation(view, new ViewOperation<Object>() { - public Object[] pre() { - final DisplayMetrics metrics = - (view != null && view.getResources() != null) ? - view.getResources().getDisplayMetrics() : null; - final Bitmap bitmap = metrics != null ? - Bitmap.createBitmap(metrics, metrics.widthPixels, - metrics.heightPixels, Bitmap.Config.RGB_565) : null; - final Canvas canvas = bitmap != null ? new Canvas(bitmap) : null; - return new Object[] { - bitmap, canvas - }; - } - - public void run(Object... data) { - if (data[1] != null) { - view.draw((Canvas) data[1]); - } - } + ? profileViewDraw(view, node) : 0; - public void post(Object... data) { - if (data[1] != null) { - ((Canvas) data[1]).setBitmap(null); - } - if (data[0] != null) { - ((Bitmap) data[0]).recycle(); - } - } - }) : 0; out.write(String.valueOf(durationMeasure)); out.write(' '); out.write(String.valueOf(durationLayout)); @@ -616,34 +555,86 @@ public class ViewDebug { ViewGroup group = (ViewGroup) view; final int count = group.getChildCount(); for (int i = 0; i < count; i++) { - profileViewAndChildren(group.getChildAt(i), out, false); + profileViewAndChildren(group.getChildAt(i), node, out, false); } } } - interface ViewOperation<T> { - T[] pre(); - void run(T... data); - void post(T... data); + private static long profileViewMeasure(final View view) { + return profileViewOperation(view, new ViewOperation() { + @Override + public void pre() { + forceLayout(view); + } + + private void forceLayout(View view) { + view.forceLayout(); + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + final int count = group.getChildCount(); + for (int i = 0; i < count; i++) { + forceLayout(group.getChildAt(i)); + } + } + } + + @Override + public void run() { + view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec); + } + }); + } + + private static long profileViewLayout(View view) { + return profileViewOperation(view, + () -> view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom)); } - private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) { + private static long profileViewDraw(View view, RenderNode node) { + DisplayMetrics dm = view.getResources().getDisplayMetrics(); + if (dm == null) { + return 0; + } + + if (view.isHardwareAccelerated()) { + DisplayListCanvas canvas = node.start(dm.widthPixels, dm.heightPixels); + try { + return profileViewOperation(view, () -> view.draw(canvas)); + } finally { + node.end(canvas); + } + } else { + Bitmap bitmap = Bitmap.createBitmap( + dm, dm.widthPixels, dm.heightPixels, Bitmap.Config.RGB_565); + Canvas canvas = new Canvas(bitmap); + try { + return profileViewOperation(view, () -> view.draw(canvas)); + } finally { + canvas.setBitmap(null); + bitmap.recycle(); + } + } + } + + interface ViewOperation { + default void pre() {} + + void run(); + } + + private static long profileViewOperation(View view, final ViewOperation operation) { final CountDownLatch latch = new CountDownLatch(1); final long[] duration = new long[1]; - view.post(new Runnable() { - public void run() { - try { - T[] data = operation.pre(); - long start = Debug.threadCpuTimeNanos(); - //noinspection unchecked - operation.run(data); - duration[0] = Debug.threadCpuTimeNanos() - start; - //noinspection unchecked - operation.post(data); - } finally { - latch.countDown(); - } + view.post(() -> { + try { + operation.pre(); + long start = Debug.threadCpuTimeNanos(); + //noinspection unchecked + operation.run(); + duration[0] = Debug.threadCpuTimeNanos() - start; + } finally { + latch.countDown(); } }); @@ -1375,6 +1366,81 @@ public class ViewDebug { } } + /** + * Converts an integer from a field that is mapped with {@link IntToString} to its string + * representation. + * + * @param clazz The class the field is defined on. + * @param field The field on which the {@link ExportedProperty} is defined on. + * @param integer The value to convert. + * @return The value converted into its string representation. + * @hide + */ + public static String intToString(Class<?> clazz, String field, int integer) { + final IntToString[] mapping = getMapping(clazz, field); + if (mapping == null) { + return Integer.toString(integer); + } + final int count = mapping.length; + for (int j = 0; j < count; j++) { + final IntToString map = mapping[j]; + if (map.from() == integer) { + return map.to(); + } + } + return Integer.toString(integer); + } + + /** + * Converts a set of flags from a field that is mapped with {@link FlagToString} to its string + * representation. + * + * @param clazz The class the field is defined on. + * @param field The field on which the {@link ExportedProperty} is defined on. + * @param flags The flags to convert. + * @return The flags converted into their string representations. + * @hide + */ + public static String flagsToString(Class<?> clazz, String field, int flags) { + final FlagToString[] mapping = getFlagMapping(clazz, field); + if (mapping == null) { + return Integer.toHexString(flags); + } + final StringBuilder result = new StringBuilder(); + final int count = mapping.length; + for (int j = 0; j < count; j++) { + final FlagToString flagMapping = mapping[j]; + final boolean ifTrue = flagMapping.outputIf(); + final int maskResult = flags & flagMapping.mask(); + final boolean test = maskResult == flagMapping.equals(); + if (test && ifTrue) { + final String name = flagMapping.name(); + result.append(name).append(' '); + } + } + if (result.length() > 0) { + result.deleteCharAt(result.length() - 1); + } + return result.toString(); + } + + private static FlagToString[] getFlagMapping(Class<?> clazz, String field) { + try { + return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class) + .flagMapping(); + } catch (NoSuchFieldException e) { + return null; + } + } + + private static IntToString[] getMapping(Class<?> clazz, String field) { + try { + return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class).mapping(); + } catch (NoSuchFieldException e) { + return null; + } + } + private static void exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix) throws IOException { diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index bf324070f5bf..122df934111d 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -421,22 +421,78 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Used to indicate that no drawing cache should be kept in memory. + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public static final int PERSISTENT_NO_CACHE = 0x0; /** * Used to indicate that the animation drawing cache should be kept in memory. + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public static final int PERSISTENT_ANIMATION_CACHE = 0x1; /** * Used to indicate that the scrolling drawing cache should be kept in memory. + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public static final int PERSISTENT_SCROLLING_CACHE = 0x2; /** * Used to indicate that all drawing caches should be kept in memory. + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public static final int PERSISTENT_ALL_CACHES = 0x3; // Layout Modes @@ -2535,7 +2591,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { - // If the event is targeting accessiiblity focus we give it to the + // If the event is targeting accessibility focus we give it to the // view that has accessibility focus and if it does not handle it // we clear the flag and dispatch the event to all children as usual. // We are looking up the accessibility focused host to avoid keeping @@ -3769,7 +3825,21 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * Enables or disables the drawing cache for each child of this view group. * * @param enabled true to enable the cache, false to dispose of it + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated protected void setChildrenDrawingCacheEnabled(boolean enabled) { if (enabled || (mPersistentDrawingCache & PERSISTENT_ALL_CACHES) != PERSISTENT_ALL_CACHES) { final View[] children = mChildren; @@ -3886,7 +3956,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * @hide + * Layout debugging code which draws rectangles around layout params. + * + * <p>This function is called automatically when the developer setting is enabled.<p/> + * + * <p>It is strongly advised to only call this function from debug builds as there is + * a risk of leaking unwanted layout information.<p/> + * + * @param canvas the canvas on which to draw + * @param paint the paint used to draw through */ protected void onDebugDrawMargins(Canvas canvas, Paint paint) { for (int i = 0; i < getChildCount(); i++) { @@ -3896,7 +3974,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * @hide + * Layout debugging code which draws rectangles around: + * <ul> + * <li>optical bounds<li/> + * <li>margins<li/> + * <li>clip bounds<li/> + * <ul/> + * + * <p>This function is called automatically when the developer setting is enabled.<p/> + * + * <p>It is strongly advised to only call this function from debug builds as there is + * a risk of leaking unwanted layout information.<p/> + * + * @param canvas the canvas on which to draw */ protected void onDebugDraw(Canvas canvas) { Paint paint = getDebugPaint(); @@ -6311,7 +6401,21 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @return one or a combination of {@link #PERSISTENT_NO_CACHE}, * {@link #PERSISTENT_ANIMATION_CACHE}, {@link #PERSISTENT_SCROLLING_CACHE} * and {@link #PERSISTENT_ALL_CACHES} + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated @ViewDebug.ExportedProperty(category = "drawing", mapping = { @ViewDebug.IntToString(from = PERSISTENT_NO_CACHE, to = "NONE"), @ViewDebug.IntToString(from = PERSISTENT_ANIMATION_CACHE, to = "ANIMATION"), @@ -6332,7 +6436,21 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @param drawingCacheToKeep one or a combination of {@link #PERSISTENT_NO_CACHE}, * {@link #PERSISTENT_ANIMATION_CACHE}, {@link #PERSISTENT_SCROLLING_CACHE} * and {@link #PERSISTENT_ALL_CACHES} + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void setPersistentDrawingCache(int drawingCacheToKeep) { mPersistentDrawingCache = drawingCacheToKeep & PERSISTENT_ALL_CACHES; } @@ -7585,10 +7703,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Use {@code canvas} to draw suitable debugging annotations for these LayoutParameters. * + * <p>This function is called automatically when the developer setting is enabled.<p/> + * + * <p>It is strongly advised to only call this function from debug builds as there is + * a risk of leaking unwanted layout information.<p/> + * * @param view the view that contains these layout parameters * @param canvas the canvas on which to draw - * - * @hide + * @param paint the paint used to draw through */ public void onDebugDraw(View view, Canvas canvas, Paint paint) { } @@ -8092,9 +8214,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return ((mMarginFlags & LAYOUT_DIRECTION_MASK) == View.LAYOUT_DIRECTION_RTL); } - /** - * @hide - */ @Override public void onDebugDraw(View view, Canvas canvas, Paint paint) { Insets oi = isLayoutModeOptical(view.mParent) ? view.getOpticalInsets() : Insets.NONE; diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 8f250a9e9f15..c6c42faad6b0 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -20,6 +20,7 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER; import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM; +import static android.view.WindowManager.LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL; @@ -72,6 +73,8 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.MergedConfiguration; import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.TimeUtils; import android.util.TypedValue; import android.view.Surface.OutOfResourcesException; @@ -140,10 +143,11 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV; /** - * Set to false if we do not want to use the multi threaded renderer. Note that by disabling + * Set to false if we do not want to use the multi threaded renderer even though + * threaded renderer (aka hardware renderering) is used. Note that by disabling * this, WindowCallbacks will not fire. */ - private static final boolean USE_MT_RENDERER = true; + private static final boolean MT_RENDERER_AVAILABLE = true; /** * Set this system property to true to force the view hierarchy to render @@ -300,6 +304,7 @@ public final class ViewRootImpl implements ViewParent, Rect mDirty; public boolean mIsAnimating; + private boolean mUseMTRenderer; private boolean mDragResizing; private boolean mInvalidateRootRequested; private int mResizeMode; @@ -361,12 +366,14 @@ public final class ViewRootImpl implements ViewParent, InputStage mFirstPostImeInputStage; InputStage mSyntheticInputStage; + private final KeyFallbackManager mKeyFallbackManager = new KeyFallbackManager(); + boolean mWindowAttributesChanged = false; int mWindowAttributesChangesFlag = 0; // These can be accessed by any thread, must be protected with a lock. // Surface can never be reassigned or cleared (use Surface.clear()). - final Surface mSurface = new Surface(); + public final Surface mSurface = new Surface(); boolean mAdded; boolean mAddedTouchMode; @@ -380,12 +387,15 @@ public final class ViewRootImpl implements ViewParent, final Rect mPendingContentInsets = new Rect(); final Rect mPendingOutsets = new Rect(); final Rect mPendingBackDropFrame = new Rect(); + final DisplayCutout.ParcelableWrapper mPendingDisplayCutout = + new DisplayCutout.ParcelableWrapper(DisplayCutout.NO_CUTOUT); boolean mPendingAlwaysConsumeNavBar; final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets = new ViewTreeObserver.InternalInsetsInfo(); final Rect mDispatchContentInsets = new Rect(); final Rect mDispatchStableInsets = new Rect(); + DisplayCutout mDispatchDisplayCutout = DisplayCutout.NO_CUTOUT; private WindowInsets mLastWindowInsets; @@ -541,18 +551,14 @@ public final class ViewRootImpl implements ViewParent, } public void addWindowCallbacks(WindowCallbacks callback) { - if (USE_MT_RENDERER) { - synchronized (mWindowCallbacks) { - mWindowCallbacks.add(callback); - } + synchronized (mWindowCallbacks) { + mWindowCallbacks.add(callback); } } public void removeWindowCallbacks(WindowCallbacks callback) { - if (USE_MT_RENDERER) { - synchronized (mWindowCallbacks) { - mWindowCallbacks.remove(callback); - } + synchronized (mWindowCallbacks) { + mWindowCallbacks.remove(callback); } } @@ -678,7 +684,17 @@ public final class ViewRootImpl implements ViewParent, // If the application owns the surface, don't enable hardware acceleration if (mSurfaceHolder == null) { + // While this is supposed to enable only, it can effectively disable + // the acceleration too. enableHardwareAcceleration(attrs); + final boolean useMTRenderer = MT_RENDERER_AVAILABLE + && mAttachInfo.mThreadedRenderer != null; + if (mUseMTRenderer != useMTRenderer) { + // Shouldn't be resizing, as it's done only in window setup, + // but end just in case. + endDragResizing(); + mUseMTRenderer = useMTRenderer; + } } boolean restore = false; @@ -726,7 +742,7 @@ public final class ViewRootImpl implements ViewParent, res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, - mAttachInfo.mOutsets, mInputChannel); + mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; @@ -748,6 +764,7 @@ public final class ViewRootImpl implements ViewParent, mPendingOverscanInsets.set(0, 0, 0, 0); mPendingContentInsets.set(mAttachInfo.mContentInsets); mPendingStableInsets.set(mAttachInfo.mStableInsets); + mPendingDisplayCutout.set(mAttachInfo.mDisplayCutout); mPendingVisibleInsets.set(0, 0, 0, 0); mAttachInfo.mAlwaysConsumeNavBar = (res & WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_NAV_BAR) != 0; @@ -1540,15 +1557,20 @@ public final class ViewRootImpl implements ViewParent, if (mLastWindowInsets == null || forceConstruct) { mDispatchContentInsets.set(mAttachInfo.mContentInsets); mDispatchStableInsets.set(mAttachInfo.mStableInsets); + mDispatchDisplayCutout = mAttachInfo.mDisplayCutout.get(); + Rect contentInsets = mDispatchContentInsets; Rect stableInsets = mDispatchStableInsets; + DisplayCutout displayCutout = mDispatchDisplayCutout; // For dispatch we preserve old logic, but for direct requests from Views we allow to // immediately use pending insets. if (!forceConstruct && (!mPendingContentInsets.equals(contentInsets) || - !mPendingStableInsets.equals(stableInsets))) { + !mPendingStableInsets.equals(stableInsets) || + !mPendingDisplayCutout.get().equals(displayCutout))) { contentInsets = mPendingContentInsets; stableInsets = mPendingStableInsets; + displayCutout = mPendingDisplayCutout.get(); } Rect outsets = mAttachInfo.mOutsets; if (outsets.left > 0 || outsets.top > 0 || outsets.right > 0 || outsets.bottom > 0) { @@ -1559,13 +1581,21 @@ public final class ViewRootImpl implements ViewParent, mLastWindowInsets = new WindowInsets(contentInsets, null /* windowDecorInsets */, stableInsets, mContext.getResources().getConfiguration().isScreenRound(), - mAttachInfo.mAlwaysConsumeNavBar); + mAttachInfo.mAlwaysConsumeNavBar, displayCutout); } return mLastWindowInsets; } void dispatchApplyInsets(View host) { - host.dispatchApplyWindowInsets(getWindowInsets(true /* forceConstruct */)); + WindowInsets insets = getWindowInsets(true /* forceConstruct */); + final boolean layoutInCutout = + (mWindowAttributes.flags2 & FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA) != 0; + if (!layoutInCutout) { + // Window is either not laid out in cutout or the status bar inset takes care of + // clearing the cutout, so we don't need to dispatch the cutout to the hierarchy. + insets = insets.consumeCutout(); + } + host.dispatchApplyWindowInsets(insets); } private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) { @@ -1668,8 +1698,6 @@ public final class ViewRootImpl implements ViewParent, host.dispatchAttachedToWindow(mAttachInfo, 0); mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true); dispatchApplyInsets(host); - //Log.i(mTag, "Screen on initialized: " + attachInfo.mKeepScreenOn); - } else { desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); @@ -1728,6 +1756,9 @@ public final class ViewRootImpl implements ViewParent, if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) { insetsChanged = true; } + if (!mPendingDisplayCutout.equals(mAttachInfo.mDisplayCutout)) { + insetsChanged = true; + } if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) { mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets); if (DEBUG_LAYOUT) Log.v(mTag, "Visible insets changing to: " @@ -1904,7 +1935,8 @@ public final class ViewRootImpl implements ViewParent, + " overscan=" + mPendingOverscanInsets.toShortString() + " content=" + mPendingContentInsets.toShortString() + " visible=" + mPendingVisibleInsets.toShortString() - + " visible=" + mPendingStableInsets.toShortString() + + " stable=" + mPendingStableInsets.toShortString() + + " cutout=" + mPendingDisplayCutout.get().toString() + " outsets=" + mPendingOutsets.toShortString() + " surface=" + mSurface); @@ -1929,6 +1961,8 @@ public final class ViewRootImpl implements ViewParent, mAttachInfo.mVisibleInsets); final boolean stableInsetsChanged = !mPendingStableInsets.equals( mAttachInfo.mStableInsets); + final boolean cutoutChanged = !mPendingDisplayCutout.equals( + mAttachInfo.mDisplayCutout); final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets); final boolean surfaceSizeChanged = (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED) != 0; @@ -1953,6 +1987,14 @@ public final class ViewRootImpl implements ViewParent, // Need to relayout with content insets. contentInsetsChanged = true; } + if (cutoutChanged) { + mAttachInfo.mDisplayCutout.set(mPendingDisplayCutout); + if (DEBUG_LAYOUT) { + Log.v(mTag, "DisplayCutout changing to: " + mAttachInfo.mDisplayCutout); + } + // Need to relayout with content insets. + contentInsetsChanged = true; + } if (alwaysConsumeNavBarChanged) { mAttachInfo.mAlwaysConsumeNavBar = mPendingAlwaysConsumeNavBar; contentInsetsChanged = true; @@ -2054,6 +2096,7 @@ public final class ViewRootImpl implements ViewParent, mResizeMode = freeformResizing ? RESIZE_MODE_FREEFORM : RESIZE_MODE_DOCKED_DIVIDER; + // TODO: Need cutout? startDragResizing(mPendingBackDropFrame, mWinFrame.equals(mPendingBackDropFrame), mPendingVisibleInsets, mPendingStableInsets, mResizeMode); @@ -2062,7 +2105,7 @@ public final class ViewRootImpl implements ViewParent, endDragResizing(); } } - if (!USE_MT_RENDERER) { + if (!mUseMTRenderer) { if (dragResizing) { mCanvasOffsetX = mWinFrame.left; mCanvasOffsetY = mWinFrame.top; @@ -2285,18 +2328,36 @@ public final class ViewRootImpl implements ViewParent, } } - if (mFirst && sAlwaysAssignFocus) { - // handle first focus request - if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: mView.hasFocus()=" - + mView.hasFocus()); - if (mView != null) { - if (!mView.hasFocus()) { - mView.restoreDefaultFocus(); - if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: requested focused view=" - + mView.findFocus()); - } else { - if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: existing focused view=" - + mView.findFocus()); + if (mFirst) { + if (sAlwaysAssignFocus) { + // handle first focus request + if (DEBUG_INPUT_RESIZE) { + Log.v(mTag, "First: mView.hasFocus()=" + mView.hasFocus()); + } + if (mView != null) { + if (!mView.hasFocus()) { + mView.restoreDefaultFocus(); + if (DEBUG_INPUT_RESIZE) { + Log.v(mTag, "First: requested focused view=" + mView.findFocus()); + } + } else { + if (DEBUG_INPUT_RESIZE) { + Log.v(mTag, "First: existing focused view=" + mView.findFocus()); + } + } + } + } else { + // Some views (like ScrollView) won't hand focus to descendants that aren't within + // their viewport. Before layout, there's a good change these views are size 0 + // which means no children can get focus. After layout, this view now has size, but + // is not guaranteed to hand-off focus to a focusable child (specifically, the edge- + // case where the child has a size prior to layout and thus won't trigger + // focusableViewAvailable). + View focused = mView.findFocus(); + if (focused instanceof ViewGroup + && ((ViewGroup) focused).getDescendantFocusability() + == ViewGroup.FOCUS_AFTER_DESCENDANTS) { + focused.restoreDefaultFocus(); } } } @@ -2682,8 +2743,10 @@ public final class ViewRootImpl implements ViewParent, @Override public void onPostDraw(DisplayListCanvas canvas) { drawAccessibilityFocusedDrawableIfNeeded(canvas); - for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { - mWindowCallbacks.get(i).onPostDraw(canvas); + if (mUseMTRenderer) { + for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { + mWindowCallbacks.get(i).onPostDraw(canvas); + } } } @@ -2827,7 +2890,7 @@ public final class ViewRootImpl implements ViewParent, try { mWindowDrawCountDown.await(); } catch (InterruptedException e) { - Log.e(mTag, "Window redraw count down interruped!"); + Log.e(mTag, "Window redraw count down interrupted!"); } mWindowDrawCountDown = null; } @@ -2897,8 +2960,6 @@ public final class ViewRootImpl implements ViewParent, final float appScale = mAttachInfo.mApplicationScale; final boolean scalingRequired = mAttachInfo.mScalingRequired; - int resizeAlpha = 0; - final Rect dirty = mDirty; if (mSurfaceHolder != null) { // The app owns the surface, we won't draw. @@ -3016,7 +3077,8 @@ public final class ViewRootImpl implements ViewParent, return; } - if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { + if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, + scalingRequired, dirty, surfaceInsets)) { return; } } @@ -3032,11 +3094,22 @@ public final class ViewRootImpl implements ViewParent, * @return true if drawing was successful, false if an error occurred */ private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, - boolean scalingRequired, Rect dirty) { + boolean scalingRequired, Rect dirty, Rect surfaceInsets) { // Draw with software renderer. final Canvas canvas; + + // We already have the offset of surfaceInsets in xoff, yoff and dirty region, + // therefore we need to add it back when moving the dirty region. + int dirtyXOffset = xoff; + int dirtyYOffset = yoff; + if (surfaceInsets != null) { + dirtyXOffset += surfaceInsets.left; + dirtyYOffset += surfaceInsets.top; + } + try { + dirty.offset(-dirtyXOffset, -dirtyYOffset); final int left = dirty.left; final int top = dirty.top; final int right = dirty.right; @@ -3063,6 +3136,8 @@ public final class ViewRootImpl implements ViewParent, // kill stuff (or ourself) for no reason. mLayoutRequested = true; // ask wm for a new surface next time. return false; + } finally { + dirty.offset(dirtyXOffset, dirtyYOffset); // Reset to the original value. } try { @@ -3469,6 +3544,7 @@ public final class ViewRootImpl implements ViewParent, } void dispatchDetachedFromWindow() { + mFirstInputStage.onDetachedFromWindow(); if (mView != null && mView.mAttachInfo != null) { mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false); mView.dispatchDetachedFromWindow(); @@ -3731,266 +3807,276 @@ public final class ViewRootImpl implements ViewParent, @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_INVALIDATE: - ((View) msg.obj).invalidate(); - break; - case MSG_INVALIDATE_RECT: - final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj; - info.target.invalidate(info.left, info.top, info.right, info.bottom); - info.recycle(); - break; - case MSG_PROCESS_INPUT_EVENTS: - mProcessInputEventsScheduled = false; - doProcessInputEvents(); - break; - case MSG_DISPATCH_APP_VISIBILITY: - handleAppVisibility(msg.arg1 != 0); - break; - case MSG_DISPATCH_GET_NEW_SURFACE: - handleGetNewSurface(); - break; - case MSG_RESIZED: { - // Recycled in the fall through... - SomeArgs args = (SomeArgs) msg.obj; - if (mWinFrame.equals(args.arg1) - && mPendingOverscanInsets.equals(args.arg5) - && mPendingContentInsets.equals(args.arg2) - && mPendingStableInsets.equals(args.arg6) - && mPendingVisibleInsets.equals(args.arg3) - && mPendingOutsets.equals(args.arg7) - && mPendingBackDropFrame.equals(args.arg8) - && args.arg4 == null - && args.argi1 == 0 - && mDisplay.getDisplayId() == args.argi3) { + case MSG_INVALIDATE: + ((View) msg.obj).invalidate(); break; - } - } // fall through... - case MSG_RESIZED_REPORT: - if (mAdded) { + case MSG_INVALIDATE_RECT: + final View.AttachInfo.InvalidateInfo info = + (View.AttachInfo.InvalidateInfo) msg.obj; + info.target.invalidate(info.left, info.top, info.right, info.bottom); + info.recycle(); + break; + case MSG_PROCESS_INPUT_EVENTS: + mProcessInputEventsScheduled = false; + doProcessInputEvents(); + break; + case MSG_DISPATCH_APP_VISIBILITY: + handleAppVisibility(msg.arg1 != 0); + break; + case MSG_DISPATCH_GET_NEW_SURFACE: + handleGetNewSurface(); + break; + case MSG_RESIZED: { + // Recycled in the fall through... SomeArgs args = (SomeArgs) msg.obj; - - final int displayId = args.argi3; - MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg4; - final boolean displayChanged = mDisplay.getDisplayId() != displayId; - - if (!mLastReportedMergedConfiguration.equals(mergedConfiguration)) { - // If configuration changed - notify about that and, maybe, about move to - // display. - performConfigurationChange(mergedConfiguration, false /* force */, - displayChanged ? displayId : INVALID_DISPLAY /* same display */); - } else if (displayChanged) { - // Moved to display without config change - report last applied one. - onMovedToDisplay(displayId, mLastConfigurationFromResources); + if (mWinFrame.equals(args.arg1) + && mPendingOverscanInsets.equals(args.arg5) + && mPendingContentInsets.equals(args.arg2) + && mPendingStableInsets.equals(args.arg6) + && mPendingDisplayCutout.get().equals(args.arg9) + && mPendingVisibleInsets.equals(args.arg3) + && mPendingOutsets.equals(args.arg7) + && mPendingBackDropFrame.equals(args.arg8) + && args.arg4 == null + && args.argi1 == 0 + && mDisplay.getDisplayId() == args.argi3) { + break; } + } // fall through... + case MSG_RESIZED_REPORT: + if (mAdded) { + SomeArgs args = (SomeArgs) msg.obj; + + final int displayId = args.argi3; + MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg4; + final boolean displayChanged = mDisplay.getDisplayId() != displayId; + + if (!mLastReportedMergedConfiguration.equals(mergedConfiguration)) { + // If configuration changed - notify about that and, maybe, + // about move to display. + performConfigurationChange(mergedConfiguration, false /* force */, + displayChanged + ? displayId : INVALID_DISPLAY /* same display */); + } else if (displayChanged) { + // Moved to display without config change - report last applied one. + onMovedToDisplay(displayId, mLastConfigurationFromResources); + } - final boolean framesChanged = !mWinFrame.equals(args.arg1) - || !mPendingOverscanInsets.equals(args.arg5) - || !mPendingContentInsets.equals(args.arg2) - || !mPendingStableInsets.equals(args.arg6) - || !mPendingVisibleInsets.equals(args.arg3) - || !mPendingOutsets.equals(args.arg7); - - mWinFrame.set((Rect) args.arg1); - mPendingOverscanInsets.set((Rect) args.arg5); - mPendingContentInsets.set((Rect) args.arg2); - mPendingStableInsets.set((Rect) args.arg6); - mPendingVisibleInsets.set((Rect) args.arg3); - mPendingOutsets.set((Rect) args.arg7); - mPendingBackDropFrame.set((Rect) args.arg8); - mForceNextWindowRelayout = args.argi1 != 0; - mPendingAlwaysConsumeNavBar = args.argi2 != 0; - - args.recycle(); + final boolean framesChanged = !mWinFrame.equals(args.arg1) + || !mPendingOverscanInsets.equals(args.arg5) + || !mPendingContentInsets.equals(args.arg2) + || !mPendingStableInsets.equals(args.arg6) + || !mPendingDisplayCutout.get().equals(args.arg9) + || !mPendingVisibleInsets.equals(args.arg3) + || !mPendingOutsets.equals(args.arg7); + + mWinFrame.set((Rect) args.arg1); + mPendingOverscanInsets.set((Rect) args.arg5); + mPendingContentInsets.set((Rect) args.arg2); + mPendingStableInsets.set((Rect) args.arg6); + mPendingDisplayCutout.set((DisplayCutout) args.arg9); + mPendingVisibleInsets.set((Rect) args.arg3); + mPendingOutsets.set((Rect) args.arg7); + mPendingBackDropFrame.set((Rect) args.arg8); + mForceNextWindowRelayout = args.argi1 != 0; + mPendingAlwaysConsumeNavBar = args.argi2 != 0; + + args.recycle(); + + if (msg.what == MSG_RESIZED_REPORT) { + reportNextDraw(); + } - if (msg.what == MSG_RESIZED_REPORT) { - reportNextDraw(); + if (mView != null && framesChanged) { + forceLayout(mView); + } + requestLayout(); } - - if (mView != null && framesChanged) { - forceLayout(mView); + break; + case MSG_WINDOW_MOVED: + if (mAdded) { + final int w = mWinFrame.width(); + final int h = mWinFrame.height(); + final int l = msg.arg1; + final int t = msg.arg2; + mWinFrame.left = l; + mWinFrame.right = l + w; + mWinFrame.top = t; + mWinFrame.bottom = t + h; + + mPendingBackDropFrame.set(mWinFrame); + maybeHandleWindowMove(mWinFrame); } - requestLayout(); - } - break; - case MSG_WINDOW_MOVED: - if (mAdded) { - final int w = mWinFrame.width(); - final int h = mWinFrame.height(); - final int l = msg.arg1; - final int t = msg.arg2; - mWinFrame.left = l; - mWinFrame.right = l + w; - mWinFrame.top = t; - mWinFrame.bottom = t + h; - - mPendingBackDropFrame.set(mWinFrame); - maybeHandleWindowMove(mWinFrame); - } - break; - case MSG_WINDOW_FOCUS_CHANGED: { - if (mAdded) { - boolean hasWindowFocus = msg.arg1 != 0; - mAttachInfo.mHasWindowFocus = hasWindowFocus; - - profileRendering(hasWindowFocus); - - if (hasWindowFocus) { - boolean inTouchMode = msg.arg2 != 0; - ensureTouchModeLocally(inTouchMode); - - if (mAttachInfo.mThreadedRenderer != null && mSurface.isValid()){ - mFullRedrawNeeded = true; - try { - final WindowManager.LayoutParams lp = mWindowAttributes; - final Rect surfaceInsets = lp != null ? lp.surfaceInsets : null; - mAttachInfo.mThreadedRenderer.initializeIfNeeded( - mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets); - } catch (OutOfResourcesException e) { - Log.e(mTag, "OutOfResourcesException locking surface", e); + break; + case MSG_WINDOW_FOCUS_CHANGED: { + final boolean hasWindowFocus = msg.arg1 != 0; + if (mAdded) { + mAttachInfo.mHasWindowFocus = hasWindowFocus; + + profileRendering(hasWindowFocus); + + if (hasWindowFocus) { + boolean inTouchMode = msg.arg2 != 0; + ensureTouchModeLocally(inTouchMode); + if (mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) { + mFullRedrawNeeded = true; try { - if (!mWindowSession.outOfMemory(mWindow)) { - Slog.w(mTag, "No processes killed for memory; killing self"); - Process.killProcess(Process.myPid()); + final WindowManager.LayoutParams lp = mWindowAttributes; + final Rect surfaceInsets = lp != null ? lp.surfaceInsets : null; + mAttachInfo.mThreadedRenderer.initializeIfNeeded( + mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets); + } catch (OutOfResourcesException e) { + Log.e(mTag, "OutOfResourcesException locking surface", e); + try { + if (!mWindowSession.outOfMemory(mWindow)) { + Slog.w(mTag, "No processes killed for memory;" + + " killing self"); + Process.killProcess(Process.myPid()); + } + } catch (RemoteException ex) { } - } catch (RemoteException ex) { + // Retry in a bit. + sendMessageDelayed(obtainMessage(msg.what, msg.arg1, msg.arg2), + 500); + return; } - // Retry in a bit. - sendMessageDelayed(obtainMessage(msg.what, msg.arg1, msg.arg2), 500); - return; } } - } - mLastWasImTarget = WindowManager.LayoutParams - .mayUseInputMethod(mWindowAttributes.flags); + mLastWasImTarget = WindowManager.LayoutParams + .mayUseInputMethod(mWindowAttributes.flags); - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { - imm.onPreWindowFocus(mView, hasWindowFocus); - } - if (mView != null) { - mAttachInfo.mKeyDispatchState.reset(); - mView.dispatchWindowFocusChanged(hasWindowFocus); - mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus); - - if (mAttachInfo.mTooltipHost != null) { - mAttachInfo.mTooltipHost.hideTooltip(); + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { + imm.onPreWindowFocus(mView, hasWindowFocus); } - } + if (mView != null) { + mAttachInfo.mKeyDispatchState.reset(); + mView.dispatchWindowFocusChanged(hasWindowFocus); + mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus); - // Note: must be done after the focus change callbacks, - // so all of the view state is set up correctly. - if (hasWindowFocus) { - if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { - imm.onPostWindowFocus(mView, mView.findFocus(), - mWindowAttributes.softInputMode, - !mHasHadWindowFocus, mWindowAttributes.flags); + if (mAttachInfo.mTooltipHost != null) { + mAttachInfo.mTooltipHost.hideTooltip(); + } } - // Clear the forward bit. We can just do this directly, since - // the window manager doesn't care about it. - mWindowAttributes.softInputMode &= - ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; - ((WindowManager.LayoutParams)mView.getLayoutParams()) - .softInputMode &= + + // Note: must be done after the focus change callbacks, + // so all of the view state is set up correctly. + if (hasWindowFocus) { + if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { + imm.onPostWindowFocus(mView, mView.findFocus(), + mWindowAttributes.softInputMode, + !mHasHadWindowFocus, mWindowAttributes.flags); + } + // Clear the forward bit. We can just do this directly, since + // the window manager doesn't care about it. + mWindowAttributes.softInputMode &= ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; - mHasHadWindowFocus = true; - } else { - if (mPointerCapture) { - handlePointerCaptureChanged(false); + ((WindowManager.LayoutParams) mView.getLayoutParams()) + .softInputMode &= + ~WindowManager.LayoutParams + .SOFT_INPUT_IS_FORWARD_NAVIGATION; + mHasHadWindowFocus = true; + } else { + if (mPointerCapture) { + handlePointerCaptureChanged(false); + } } } - } - } break; - case MSG_DIE: - doDie(); - break; - case MSG_DISPATCH_INPUT_EVENT: { - SomeArgs args = (SomeArgs)msg.obj; - InputEvent event = (InputEvent)args.arg1; - InputEventReceiver receiver = (InputEventReceiver)args.arg2; - enqueueInputEvent(event, receiver, 0, true); - args.recycle(); - } break; - case MSG_SYNTHESIZE_INPUT_EVENT: { - InputEvent event = (InputEvent)msg.obj; - enqueueInputEvent(event, null, QueuedInputEvent.FLAG_UNHANDLED, true); - } break; - case MSG_DISPATCH_KEY_FROM_IME: { - if (LOCAL_LOGV) Log.v( - TAG, "Dispatching key " - + msg.obj + " from IME to " + mView); - KeyEvent event = (KeyEvent)msg.obj; - if ((event.getFlags()&KeyEvent.FLAG_FROM_SYSTEM) != 0) { - // The IME is trying to say this event is from the - // system! Bad bad bad! - //noinspection UnusedAssignment - event = KeyEvent.changeFlags(event, event.getFlags() & - ~KeyEvent.FLAG_FROM_SYSTEM); - } - enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME, true); - } break; - case MSG_CHECK_FOCUS: { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - imm.checkFocus(); - } - } break; - case MSG_CLOSE_SYSTEM_DIALOGS: { - if (mView != null) { - mView.onCloseSystemDialogs((String)msg.obj); - } - } break; - case MSG_DISPATCH_DRAG_EVENT: - case MSG_DISPATCH_DRAG_LOCATION_EVENT: { - DragEvent event = (DragEvent)msg.obj; - event.mLocalState = mLocalDragState; // only present when this app called startDrag() - handleDragEvent(event); - } break; - case MSG_DISPATCH_SYSTEM_UI_VISIBILITY: { - handleDispatchSystemUiVisibilityChanged((SystemUiVisibilityInfo) msg.obj); - } break; - case MSG_UPDATE_CONFIGURATION: { - Configuration config = (Configuration) msg.obj; - if (config.isOtherSeqNewer( - mLastReportedMergedConfiguration.getMergedConfiguration())) { - // If we already have a newer merged config applied - use its global part. - config = mLastReportedMergedConfiguration.getGlobalConfiguration(); - } + mFirstInputStage.onWindowFocusChanged(hasWindowFocus); + } break; + case MSG_DIE: + doDie(); + break; + case MSG_DISPATCH_INPUT_EVENT: { + SomeArgs args = (SomeArgs) msg.obj; + InputEvent event = (InputEvent) args.arg1; + InputEventReceiver receiver = (InputEventReceiver) args.arg2; + enqueueInputEvent(event, receiver, 0, true); + args.recycle(); + } break; + case MSG_SYNTHESIZE_INPUT_EVENT: { + InputEvent event = (InputEvent) msg.obj; + enqueueInputEvent(event, null, QueuedInputEvent.FLAG_UNHANDLED, true); + } break; + case MSG_DISPATCH_KEY_FROM_IME: { + if (LOCAL_LOGV) { + Log.v(TAG, "Dispatching key " + msg.obj + " from IME to " + mView); + } + KeyEvent event = (KeyEvent) msg.obj; + if ((event.getFlags() & KeyEvent.FLAG_FROM_SYSTEM) != 0) { + // The IME is trying to say this event is from the + // system! Bad bad bad! + //noinspection UnusedAssignment + event = KeyEvent.changeFlags(event, + event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM); + } + enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME, true); + } break; + case MSG_CHECK_FOCUS: { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.checkFocus(); + } + } break; + case MSG_CLOSE_SYSTEM_DIALOGS: { + if (mView != null) { + mView.onCloseSystemDialogs((String) msg.obj); + } + } break; + case MSG_DISPATCH_DRAG_EVENT: { + } // fall through + case MSG_DISPATCH_DRAG_LOCATION_EVENT: { + DragEvent event = (DragEvent) msg.obj; + // only present when this app called startDrag() + event.mLocalState = mLocalDragState; + handleDragEvent(event); + } break; + case MSG_DISPATCH_SYSTEM_UI_VISIBILITY: { + handleDispatchSystemUiVisibilityChanged((SystemUiVisibilityInfo) msg.obj); + } break; + case MSG_UPDATE_CONFIGURATION: { + Configuration config = (Configuration) msg.obj; + if (config.isOtherSeqNewer( + mLastReportedMergedConfiguration.getMergedConfiguration())) { + // If we already have a newer merged config applied - use its global part. + config = mLastReportedMergedConfiguration.getGlobalConfiguration(); + } - // Use the newer global config and last reported override config. - mPendingMergedConfiguration.setConfiguration(config, - mLastReportedMergedConfiguration.getOverrideConfiguration()); + // Use the newer global config and last reported override config. + mPendingMergedConfiguration.setConfiguration(config, + mLastReportedMergedConfiguration.getOverrideConfiguration()); - performConfigurationChange(mPendingMergedConfiguration, false /* force */, - INVALID_DISPLAY /* same display */); - } break; - case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST: { - setAccessibilityFocus(null, null); - } break; - case MSG_INVALIDATE_WORLD: { - if (mView != null) { - invalidateWorld(mView); - } - } break; - case MSG_DISPATCH_WINDOW_SHOWN: { - handleDispatchWindowShown(); - } break; - case MSG_REQUEST_KEYBOARD_SHORTCUTS: { - final IResultReceiver receiver = (IResultReceiver) msg.obj; - final int deviceId = msg.arg1; - handleRequestKeyboardShortcuts(receiver, deviceId); - } break; - case MSG_UPDATE_POINTER_ICON: { - MotionEvent event = (MotionEvent) msg.obj; - resetPointerIcon(event); - } break; - case MSG_POINTER_CAPTURE_CHANGED: { - final boolean hasCapture = msg.arg1 != 0; - handlePointerCaptureChanged(hasCapture); - } break; - case MSG_DRAW_FINISHED: { - pendingDrawFinished(); - } break; + performConfigurationChange(mPendingMergedConfiguration, false /* force */, + INVALID_DISPLAY /* same display */); + } break; + case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST: { + setAccessibilityFocus(null, null); + } break; + case MSG_INVALIDATE_WORLD: { + if (mView != null) { + invalidateWorld(mView); + } + } break; + case MSG_DISPATCH_WINDOW_SHOWN: { + handleDispatchWindowShown(); + } break; + case MSG_REQUEST_KEYBOARD_SHORTCUTS: { + final IResultReceiver receiver = (IResultReceiver) msg.obj; + final int deviceId = msg.arg1; + handleRequestKeyboardShortcuts(receiver, deviceId); + } break; + case MSG_UPDATE_POINTER_ICON: { + MotionEvent event = (MotionEvent) msg.obj; + resetPointerIcon(event); + } break; + case MSG_POINTER_CAPTURE_CHANGED: { + final boolean hasCapture = msg.arg1 != 0; + handlePointerCaptureChanged(hasCapture); + } break; + case MSG_DRAW_FINISHED: { + pendingDrawFinished(); + } break; } } } @@ -4203,6 +4289,18 @@ public final class ViewRootImpl implements ViewParent, } } + protected void onWindowFocusChanged(boolean hasWindowFocus) { + if (mNext != null) { + mNext.onWindowFocusChanged(hasWindowFocus); + } + } + + protected void onDetachedFromWindow() { + if (mNext != null) { + mNext.onDetachedFromWindow(); + } + } + protected boolean shouldDropInputEvent(QueuedInputEvent q) { if (mView == null || !mAdded) { Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent); @@ -4729,6 +4827,13 @@ public final class ViewRootImpl implements ViewParent, private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent)q.mEvent; + mKeyFallbackManager.mDispatched = false; + + if (mKeyFallbackManager.hasFocus() + && mKeyFallbackManager.dispatchUnique(mView, event)) { + return FINISH_HANDLED; + } + // Deliver the key to the view hierarchy. if (mView.dispatchKeyEvent(event)) { return FINISH_HANDLED; @@ -4738,6 +4843,10 @@ public final class ViewRootImpl implements ViewParent, return FINISH_NOT_HANDLED; } + if (mKeyFallbackManager.dispatchUnique(mView, event)) { + return FINISH_HANDLED; + } + int groupNavigationDirection = 0; if (event.getAction() == KeyEvent.ACTION_DOWN @@ -4956,9 +5065,9 @@ public final class ViewRootImpl implements ViewParent, final MotionEvent event = (MotionEvent)q.mEvent; final int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { - mTrackball.cancel(event); + mTrackball.cancel(); } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { - mJoystick.cancel(event); + mJoystick.cancel(); } else if ((source & InputDevice.SOURCE_TOUCH_NAVIGATION) == InputDevice.SOURCE_TOUCH_NAVIGATION) { mTouchNavigation.cancel(event); @@ -4967,6 +5076,18 @@ public final class ViewRootImpl implements ViewParent, } super.onDeliverToNext(q); } + + @Override + protected void onWindowFocusChanged(boolean hasWindowFocus) { + if (!hasWindowFocus) { + mJoystick.cancel(); + } + } + + @Override + protected void onDetachedFromWindow() { + mJoystick.cancel(); + } } /** @@ -5079,7 +5200,7 @@ public final class ViewRootImpl implements ViewParent, } } - public void cancel(MotionEvent event) { + public void cancel() { mLastTime = Integer.MIN_VALUE; // If we reach this, we consumed a trackball event. @@ -5263,14 +5384,11 @@ public final class ViewRootImpl implements ViewParent, * Creates dpad events from unhandled joystick movements. */ final class SyntheticJoystickHandler extends Handler { - private final static String TAG = "SyntheticJoystickHandler"; private final static int MSG_ENQUEUE_X_AXIS_KEY_REPEAT = 1; private final static int MSG_ENQUEUE_Y_AXIS_KEY_REPEAT = 2; - private int mLastXDirection; - private int mLastYDirection; - private int mLastXKeyCode; - private int mLastYKeyCode; + private final JoystickAxesState mJoystickAxesState = new JoystickAxesState(); + private final SparseArray<KeyEvent> mDeviceKeyEvents = new SparseArray<>(); public SyntheticJoystickHandler() { super(true); @@ -5281,11 +5399,10 @@ public final class ViewRootImpl implements ViewParent, switch (msg.what) { case MSG_ENQUEUE_X_AXIS_KEY_REPEAT: case MSG_ENQUEUE_Y_AXIS_KEY_REPEAT: { - KeyEvent oldEvent = (KeyEvent)msg.obj; - KeyEvent e = KeyEvent.changeTimeRepeat(oldEvent, - SystemClock.uptimeMillis(), - oldEvent.getRepeatCount() + 1); if (mAttachInfo.mHasWindowFocus) { + KeyEvent oldEvent = (KeyEvent) msg.obj; + KeyEvent e = KeyEvent.changeTimeRepeat(oldEvent, + SystemClock.uptimeMillis(), oldEvent.getRepeatCount() + 1); enqueueInputEvent(e); Message m = obtainMessage(msg.what, e); m.setAsynchronous(true); @@ -5297,97 +5414,176 @@ public final class ViewRootImpl implements ViewParent, public void process(MotionEvent event) { switch(event.getActionMasked()) { - case MotionEvent.ACTION_CANCEL: - cancel(event); - break; - case MotionEvent.ACTION_MOVE: - update(event, true); - break; - default: - Log.w(mTag, "Unexpected action: " + event.getActionMasked()); + case MotionEvent.ACTION_CANCEL: + cancel(); + break; + case MotionEvent.ACTION_MOVE: + update(event); + break; + default: + Log.w(mTag, "Unexpected action: " + event.getActionMasked()); } } - private void cancel(MotionEvent event) { + private void cancel() { removeMessages(MSG_ENQUEUE_X_AXIS_KEY_REPEAT); removeMessages(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT); - update(event, false); - } - - private void update(MotionEvent event, boolean synthesizeNewKeys) { + for (int i = 0; i < mDeviceKeyEvents.size(); i++) { + final KeyEvent keyEvent = mDeviceKeyEvents.valueAt(i); + if (keyEvent != null) { + enqueueInputEvent(KeyEvent.changeTimeRepeat(keyEvent, + SystemClock.uptimeMillis(), 0)); + } + } + mDeviceKeyEvents.clear(); + mJoystickAxesState.resetState(); + } + + private void update(MotionEvent event) { + final int historySize = event.getHistorySize(); + for (int h = 0; h < historySize; h++) { + final long time = event.getHistoricalEventTime(h); + mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_X, + event.getHistoricalAxisValue(MotionEvent.AXIS_X, 0, h)); + mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_Y, + event.getHistoricalAxisValue(MotionEvent.AXIS_Y, 0, h)); + mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_X, + event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, 0, h)); + mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_Y, + event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, 0, h)); + } final long time = event.getEventTime(); - final int metaState = event.getMetaState(); - final int deviceId = event.getDeviceId(); - final int source = event.getSource(); - - int xDirection = joystickAxisValueToDirection( + mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_X, + event.getAxisValue(MotionEvent.AXIS_X)); + mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_Y, + event.getAxisValue(MotionEvent.AXIS_Y)); + mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_X, event.getAxisValue(MotionEvent.AXIS_HAT_X)); - if (xDirection == 0) { - xDirection = joystickAxisValueToDirection(event.getX()); - } - - int yDirection = joystickAxisValueToDirection( + mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_Y, event.getAxisValue(MotionEvent.AXIS_HAT_Y)); - if (yDirection == 0) { - yDirection = joystickAxisValueToDirection(event.getY()); - } + } + + final class JoystickAxesState { + // State machine: from neutral state (no button press) can go into + // button STATE_UP_OR_LEFT or STATE_DOWN_OR_RIGHT state, emitting an ACTION_DOWN event. + // From STATE_UP_OR_LEFT or STATE_DOWN_OR_RIGHT state can go into neutral state, + // emitting an ACTION_UP event. + private static final int STATE_UP_OR_LEFT = -1; + private static final int STATE_NEUTRAL = 0; + private static final int STATE_DOWN_OR_RIGHT = 1; + + final int[] mAxisStatesHat = {STATE_NEUTRAL, STATE_NEUTRAL}; // {AXIS_HAT_X, AXIS_HAT_Y} + final int[] mAxisStatesStick = {STATE_NEUTRAL, STATE_NEUTRAL}; // {AXIS_X, AXIS_Y} + + void resetState() { + mAxisStatesHat[0] = STATE_NEUTRAL; + mAxisStatesHat[1] = STATE_NEUTRAL; + mAxisStatesStick[0] = STATE_NEUTRAL; + mAxisStatesStick[1] = STATE_NEUTRAL; + } + + void updateStateForAxis(MotionEvent event, long time, int axis, float value) { + // Emit KeyEvent if necessary + // axis can be AXIS_X, AXIS_Y, AXIS_HAT_X, AXIS_HAT_Y + final int axisStateIndex; + final int repeatMessage; + if (isXAxis(axis)) { + axisStateIndex = 0; + repeatMessage = MSG_ENQUEUE_X_AXIS_KEY_REPEAT; + } else if (isYAxis(axis)) { + axisStateIndex = 1; + repeatMessage = MSG_ENQUEUE_Y_AXIS_KEY_REPEAT; + } else { + Log.e(mTag, "Unexpected axis " + axis + " in updateStateForAxis!"); + return; + } + final int newState = joystickAxisValueToState(value); + + final int currentState; + if (axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Y) { + currentState = mAxisStatesStick[axisStateIndex]; + } else { + currentState = mAxisStatesHat[axisStateIndex]; + } - if (xDirection != mLastXDirection) { - if (mLastXKeyCode != 0) { - removeMessages(MSG_ENQUEUE_X_AXIS_KEY_REPEAT); - enqueueInputEvent(new KeyEvent(time, time, - KeyEvent.ACTION_UP, mLastXKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); - mLastXKeyCode = 0; + if (currentState == newState) { + return; } - mLastXDirection = xDirection; + final int metaState = event.getMetaState(); + final int deviceId = event.getDeviceId(); + final int source = event.getSource(); - if (xDirection != 0 && synthesizeNewKeys) { - mLastXKeyCode = xDirection > 0 - ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT; - final KeyEvent e = new KeyEvent(time, time, - KeyEvent.ACTION_DOWN, mLastXKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source); - enqueueInputEvent(e); - Message m = obtainMessage(MSG_ENQUEUE_X_AXIS_KEY_REPEAT, e); - m.setAsynchronous(true); - sendMessageDelayed(m, ViewConfiguration.getKeyRepeatTimeout()); + if (currentState == STATE_DOWN_OR_RIGHT || currentState == STATE_UP_OR_LEFT) { + // send a button release event + final int keyCode = joystickAxisAndStateToKeycode(axis, currentState); + if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { + enqueueInputEvent(new KeyEvent(time, time, KeyEvent.ACTION_UP, keyCode, + 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); + // remove the corresponding pending UP event if focus lost/view detached + mDeviceKeyEvents.put(deviceId, null); + } + removeMessages(repeatMessage); } - } - if (yDirection != mLastYDirection) { - if (mLastYKeyCode != 0) { - removeMessages(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT); - enqueueInputEvent(new KeyEvent(time, time, - KeyEvent.ACTION_UP, mLastYKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source)); - mLastYKeyCode = 0; + if (newState == STATE_DOWN_OR_RIGHT || newState == STATE_UP_OR_LEFT) { + // send a button down event + final int keyCode = joystickAxisAndStateToKeycode(axis, newState); + if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { + KeyEvent keyEvent = new KeyEvent(time, time, KeyEvent.ACTION_DOWN, keyCode, + 0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source); + enqueueInputEvent(keyEvent); + Message m = obtainMessage(repeatMessage, keyEvent); + m.setAsynchronous(true); + sendMessageDelayed(m, ViewConfiguration.getKeyRepeatTimeout()); + // store the corresponding ACTION_UP event so that it can be sent + // if focus is lost or root view is removed + mDeviceKeyEvents.put(deviceId, + new KeyEvent(time, time, KeyEvent.ACTION_UP, keyCode, + 0, metaState, deviceId, 0, + KeyEvent.FLAG_FALLBACK | KeyEvent.FLAG_CANCELED, + source)); + } + } + if (axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Y) { + mAxisStatesStick[axisStateIndex] = newState; + } else { + mAxisStatesHat[axisStateIndex] = newState; } + } - mLastYDirection = yDirection; + private boolean isXAxis(int axis) { + return axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_HAT_X; + } + private boolean isYAxis(int axis) { + return axis == MotionEvent.AXIS_Y || axis == MotionEvent.AXIS_HAT_Y; + } - if (yDirection != 0 && synthesizeNewKeys) { - mLastYKeyCode = yDirection > 0 - ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP; - final KeyEvent e = new KeyEvent(time, time, - KeyEvent.ACTION_DOWN, mLastYKeyCode, 0, metaState, - deviceId, 0, KeyEvent.FLAG_FALLBACK, source); - enqueueInputEvent(e); - Message m = obtainMessage(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT, e); - m.setAsynchronous(true); - sendMessageDelayed(m, ViewConfiguration.getKeyRepeatTimeout()); + private int joystickAxisAndStateToKeycode(int axis, int state) { + if (isXAxis(axis) && state == STATE_UP_OR_LEFT) { + return KeyEvent.KEYCODE_DPAD_LEFT; } + if (isXAxis(axis) && state == STATE_DOWN_OR_RIGHT) { + return KeyEvent.KEYCODE_DPAD_RIGHT; + } + if (isYAxis(axis) && state == STATE_UP_OR_LEFT) { + return KeyEvent.KEYCODE_DPAD_UP; + } + if (isYAxis(axis) && state == STATE_DOWN_OR_RIGHT) { + return KeyEvent.KEYCODE_DPAD_DOWN; + } + Log.e(mTag, "Unknown axis " + axis + " or direction " + state); + return KeyEvent.KEYCODE_UNKNOWN; // should never happen } - } - private int joystickAxisValueToDirection(float value) { - if (value >= 0.5f) { - return 1; - } else if (value <= -0.5f) { - return -1; - } else { - return 0; + private int joystickAxisValueToState(float value) { + if (value >= 0.5f) { + return STATE_DOWN_OR_RIGHT; + } else if (value <= -0.5f) { + return STATE_UP_OR_LEFT; + } else { + return STATE_NEUTRAL; + } } } } @@ -6108,7 +6304,6 @@ public final class ViewRootImpl implements ViewParent, if (DBG) Log.d(mTag, "WindowLayout in layoutWindow:" + params); } - //Log.d(mTag, ">>>>>> CALLING relayout"); if (params != null && mOrigWindowType != params.type) { // For compatibility with old apps, don't crash here. if (mTargetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { @@ -6123,13 +6318,12 @@ public final class ViewRootImpl implements ViewParent, (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets, - mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, + mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout, mPendingMergedConfiguration, mSurface); mPendingAlwaysConsumeNavBar = (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_NAV_BAR) != 0; - //Log.d(mTag, "<<<<<< BACK FROM relayout"); if (restore) { params.restore(); } @@ -6407,7 +6601,8 @@ public final class ViewRootImpl implements ViewParent, private void dispatchResized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout, - boolean alwaysConsumeNavBar, int displayId) { + boolean alwaysConsumeNavBar, int displayId, + DisplayCutout.ParcelableWrapper displayCutout) { if (DEBUG_LAYOUT) Log.v(mTag, "Resizing " + this + ": frame=" + frame.toShortString() + " contentInsets=" + contentInsets.toShortString() + " visibleInsets=" + visibleInsets.toShortString() @@ -6416,7 +6611,7 @@ public final class ViewRootImpl implements ViewParent, // Tell all listeners that we are resizing the window so that the chrome can get // updated as fast as possible on a separate thread, - if (mDragResizing) { + if (mDragResizing && mUseMTRenderer) { boolean fullscreen = frame.equals(backDropFrame); synchronized (mWindowCallbacks) { for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { @@ -6444,6 +6639,7 @@ public final class ViewRootImpl implements ViewParent, args.arg6 = sameProcessCall ? new Rect(stableInsets) : stableInsets; args.arg7 = sameProcessCall ? new Rect(outsets) : outsets; args.arg8 = sameProcessCall ? new Rect(backDropFrame) : backDropFrame; + args.arg9 = displayCutout.get(); // DisplayCutout is immutable. args.argi1 = forceLayout ? 1 : 0; args.argi2 = alwaysConsumeNavBar ? 1 : 0; args.argi3 = displayId; @@ -7409,6 +7605,16 @@ public final class ViewRootImpl implements ViewParent, } } + /** + * Dispatches a KeyEvent to all registered key fallback handlers. + * + * @param event + * @return {@code true} if the event was handled, {@code false} otherwise. + */ + public boolean dispatchKeyFallbackEvent(KeyEvent event) { + return mKeyFallbackManager.dispatch(mView, event); + } + class TakenSurfaceHolder extends BaseSurfaceHolder { @Override public boolean onAllowLockCanvas() { @@ -7466,12 +7672,13 @@ public final class ViewRootImpl implements ViewParent, public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout, - boolean alwaysConsumeNavBar, int displayId) { + boolean alwaysConsumeNavBar, int displayId, + DisplayCutout.ParcelableWrapper displayCutout) { final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor != null) { viewAncestor.dispatchResized(frame, overscanInsets, contentInsets, visibleInsets, stableInsets, outsets, reportDraw, mergedConfiguration, - backDropFrame, forceLayout, alwaysConsumeNavBar, displayId); + backDropFrame, forceLayout, alwaysConsumeNavBar, displayId, displayCutout); } } @@ -7654,9 +7861,11 @@ public final class ViewRootImpl implements ViewParent, Rect stableInsets, int resizeMode) { if (!mDragResizing) { mDragResizing = true; - for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { - mWindowCallbacks.get(i).onWindowDragResizeStart(initialBounds, fullscreen, - systemInsets, stableInsets, resizeMode); + if (mUseMTRenderer) { + for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { + mWindowCallbacks.get(i).onWindowDragResizeStart( + initialBounds, fullscreen, systemInsets, stableInsets, resizeMode); + } } mFullRedrawNeeded = true; } @@ -7668,8 +7877,10 @@ public final class ViewRootImpl implements ViewParent, private void endDragResizing() { if (mDragResizing) { mDragResizing = false; - for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { - mWindowCallbacks.get(i).onWindowDragResizeEnd(); + if (mUseMTRenderer) { + for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { + mWindowCallbacks.get(i).onWindowDragResizeEnd(); + } } mFullRedrawNeeded = true; } @@ -7677,19 +7888,21 @@ public final class ViewRootImpl implements ViewParent, private boolean updateContentDrawBounds() { boolean updated = false; - for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { - updated |= mWindowCallbacks.get(i).onContentDrawn( - mWindowAttributes.surfaceInsets.left, - mWindowAttributes.surfaceInsets.top, - mWidth, mHeight); + if (mUseMTRenderer) { + for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { + updated |= + mWindowCallbacks.get(i).onContentDrawn(mWindowAttributes.surfaceInsets.left, + mWindowAttributes.surfaceInsets.top, mWidth, mHeight); + } } return updated | (mDragResizing && mReportNextDraw); } private void requestDrawWindow() { - if (mReportNextDraw) { - mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size()); + if (!mUseMTRenderer) { + return; } + mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size()); for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { mWindowCallbacks.get(i).onRequestDraw(mReportNextDraw); } @@ -7733,6 +7946,7 @@ public final class ViewRootImpl implements ViewParent, if (!registered) { mAttachInfo.mAccessibilityWindowId = mAccessibilityManager.addAccessibilityInteractionConnection(mWindow, + mContext.getPackageName(), new AccessibilityInteractionConnection(ViewRootImpl.this)); } } @@ -7749,11 +7963,11 @@ public final class ViewRootImpl implements ViewParent, final class HighContrastTextManager implements HighTextContrastChangeListener { HighContrastTextManager() { - mAttachInfo.mHighContrastText = mAccessibilityManager.isHighTextContrastEnabled(); + ThreadedRenderer.setHighContrastText(mAccessibilityManager.isHighTextContrastEnabled()); } @Override public void onHighTextContrastStateChanged(boolean enabled) { - mAttachInfo.mHighContrastText = enabled; + ThreadedRenderer.setHighContrastText(enabled); // Destroy Displaylists so they can be recreated with high contrast recordings destroyHardwareResources(); @@ -7973,4 +8187,92 @@ public final class ViewRootImpl implements ViewParent, run(); } } + + private static class KeyFallbackManager { + + // This is used to ensure that key-fallback events are only dispatched once. We attempt + // to dispatch more than once in order to achieve a certain order. Specifically, if we + // are in an Activity or Dialog (and have a Window.Callback), the keyfallback events should + // be dispatched after the view hierarchy, but before the Activity. However, if we aren't + // in an activity, we still want key fallbacks to be dispatched. + boolean mDispatched = false; + + SparseBooleanArray mCapturedKeys = new SparseBooleanArray(); + WeakReference<View> mFallbackReceiver = null; + int mVisitCount = 0; + + private void updateCaptureState(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + mCapturedKeys.append(event.getKeyCode(), true); + } + if (event.getAction() == KeyEvent.ACTION_UP) { + mCapturedKeys.delete(event.getKeyCode()); + } + } + + boolean dispatch(View root, KeyEvent event) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "KeyFallback dispatch"); + mDispatched = true; + + updateCaptureState(event); + + if (mFallbackReceiver != null) { + View target = mFallbackReceiver.get(); + if (mCapturedKeys.size() == 0) { + mFallbackReceiver = null; + } + if (target != null && target.isAttachedToWindow()) { + return target.onKeyFallback(event); + } + // consume anyways so that we don't feed uncaptured key events to other views + return true; + } + + boolean result = dispatchInZOrder(root, event); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + return result; + } + + private boolean dispatchInZOrder(View view, KeyEvent evt) { + if (view instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) view; + ArrayList<View> orderedViews = vg.buildOrderedChildList(); + if (orderedViews != null) { + try { + for (int i = orderedViews.size() - 1; i >= 0; --i) { + View v = orderedViews.get(i); + if (dispatchInZOrder(v, evt)) { + return true; + } + } + } finally { + orderedViews.clear(); + } + } else { + for (int i = vg.getChildCount() - 1; i >= 0; --i) { + View v = vg.getChildAt(i); + if (dispatchInZOrder(v, evt)) { + return true; + } + } + } + } + if (view.onKeyFallback(evt)) { + mFallbackReceiver = new WeakReference<>(view); + return true; + } + return false; + } + + boolean hasFocus() { + return mFallbackReceiver != null; + } + + boolean dispatchUnique(View root, KeyEvent event) { + if (mDispatched) { + return false; + } + return dispatch(root, event); + } + } } diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java index 0ecd20da21c5..d665dde39afe 100644 --- a/core/java/android/view/ViewStructure.java +++ b/core/java/android/view/ViewStructure.java @@ -365,6 +365,30 @@ public abstract class ViewStructure { public abstract void setDataIsSensitive(boolean sensitive); /** + * Sets the minimum width in ems of the text associated with this view, when supported. + * + * <p>Should only be set when the node is used for autofill purposes - it will be ignored + * when used for Assist. + */ + public void setMinTextEms(@SuppressWarnings("unused") int minEms) {} + + /** + * Sets the maximum width in ems of the text associated with this view, when supported. + * + * <p>Should only be set when the node is used for autofill purposes - it will be ignored + * when used for Assist. + */ + public void setMaxTextEms(@SuppressWarnings("unused") int maxEms) {} + + /** + * Sets the maximum length of the text associated with this view, when supported. + * + * <p>Should only be set when the node is used for autofill purposes - it will be ignored + * when used for Assist. + */ + public void setMaxTextLength(@SuppressWarnings("unused") int maxLength) {} + + /** * Call when done populating a {@link ViewStructure} returned by * {@link #asyncNewChild}. */ @@ -378,7 +402,7 @@ public abstract class ViewStructure { * * <p>Typically used when the view is a container for an HTML document. * - * @param domain URL representing the domain; only the host part will be used. + * @param domain RFC 2396-compliant URI representing the domain. */ public abstract void setWebDomain(@Nullable String domain); diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 3d6af414a3a0..176927fef0eb 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -26,6 +26,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StyleRes; import android.annotation.SystemApi; +import android.app.WindowConfiguration; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; @@ -487,7 +488,7 @@ public abstract class Window { public void onAttachedToWindow(); /** - * Called when the window has been attached to the window manager. + * Called when the window has been detached from the window manager. * See {@link View#onDetachedFromWindow() View.onDetachedFromWindow()} * for more information. */ @@ -611,8 +612,8 @@ public abstract class Window { public interface WindowControllerCallback { /** * Moves the activity from - * {@link android.app.ActivityManager.StackId#FREEFORM_WORKSPACE_STACK_ID} to - * {@link android.app.ActivityManager.StackId#FULLSCREEN_WORKSPACE_STACK_ID} stack. + * Moves the activity from {@link WindowConfiguration#WINDOWING_MODE_FREEFORM} windowing + * mode to {@link WindowConfiguration#WINDOWING_MODE_FULLSCREEN}. */ void exitFreeformMode() throws RemoteException; @@ -622,9 +623,6 @@ public abstract class Window { */ void enterPictureInPictureModeIfPossible(); - /** Returns the current stack Id for the window. */ - int getWindowStackId() throws RemoteException; - /** Returns whether the window belongs to the task root. */ boolean isTaskRoot(); } diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index 750931ab661c..df124ac5be28 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -17,6 +17,7 @@ package android.view; +import android.annotation.NonNull; import android.graphics.Rect; /** @@ -36,6 +37,7 @@ public final class WindowInsets { private Rect mStableInsets; private Rect mTempRect; private boolean mIsRound; + private DisplayCutout mDisplayCutout; /** * In multi-window we force show the navigation bar. Because we don't want that the surface size @@ -47,6 +49,7 @@ public final class WindowInsets { private boolean mSystemWindowInsetsConsumed = false; private boolean mWindowDecorInsetsConsumed = false; private boolean mStableInsetsConsumed = false; + private boolean mCutoutConsumed = false; private static final Rect EMPTY_RECT = new Rect(0, 0, 0, 0); @@ -59,12 +62,12 @@ public final class WindowInsets { public static final WindowInsets CONSUMED; static { - CONSUMED = new WindowInsets(null, null, null, false, false); + CONSUMED = new WindowInsets(null, null, null, false, false, null); } /** @hide */ public WindowInsets(Rect systemWindowInsets, Rect windowDecorInsets, Rect stableInsets, - boolean isRound, boolean alwaysConsumeNavBar) { + boolean isRound, boolean alwaysConsumeNavBar, DisplayCutout displayCutout) { mSystemWindowInsetsConsumed = systemWindowInsets == null; mSystemWindowInsets = mSystemWindowInsetsConsumed ? EMPTY_RECT : systemWindowInsets; @@ -76,6 +79,9 @@ public final class WindowInsets { mIsRound = isRound; mAlwaysConsumeNavBar = alwaysConsumeNavBar; + + mCutoutConsumed = displayCutout == null; + mDisplayCutout = mCutoutConsumed ? DisplayCutout.NO_CUTOUT : displayCutout; } /** @@ -92,11 +98,13 @@ public final class WindowInsets { mStableInsetsConsumed = src.mStableInsetsConsumed; mIsRound = src.mIsRound; mAlwaysConsumeNavBar = src.mAlwaysConsumeNavBar; + mDisplayCutout = src.mDisplayCutout; + mCutoutConsumed = src.mCutoutConsumed; } /** @hide */ public WindowInsets(Rect systemWindowInsets) { - this(systemWindowInsets, null, null, false, false); + this(systemWindowInsets, null, null, false, false, null); } /** @@ -260,9 +268,34 @@ public final class WindowInsets { * @return true if any inset values are nonzero */ public boolean hasInsets() { - return hasSystemWindowInsets() || hasWindowDecorInsets() || hasStableInsets(); + return hasSystemWindowInsets() || hasWindowDecorInsets() || hasStableInsets() + || mDisplayCutout.hasCutout(); + } + + /** + * @return the display cutout + * @see DisplayCutout + * @hide pending API + */ + @NonNull + public DisplayCutout getDisplayCutout() { + return mDisplayCutout; + } + + /** + * Returns a copy of this WindowInsets with the cutout fully consumed. + * + * @return A modified copy of this WindowInsets + * @hide pending API + */ + public WindowInsets consumeCutout() { + final WindowInsets result = new WindowInsets(this); + result.mDisplayCutout = DisplayCutout.NO_CUTOUT; + result.mCutoutConsumed = true; + return result; } + /** * Check if these insets have been fully consumed. * @@ -277,7 +310,8 @@ public final class WindowInsets { * @return true if the insets have been fully consumed. */ public boolean isConsumed() { - return mSystemWindowInsetsConsumed && mWindowDecorInsetsConsumed && mStableInsetsConsumed; + return mSystemWindowInsetsConsumed && mWindowDecorInsetsConsumed && mStableInsetsConsumed + && mCutoutConsumed; } /** @@ -495,7 +529,9 @@ public final class WindowInsets { public String toString() { return "WindowInsets{systemWindowInsets=" + mSystemWindowInsets + " windowDecorInsets=" + mWindowDecorInsets - + " stableInsets=" + mStableInsets + - (isRound() ? " round}" : "}"); + + " stableInsets=" + mStableInsets + + (mDisplayCutout.hasCutout() ? " cutout=" + mDisplayCutout : "") + + (isRound() ? " round" : "") + + "}"; } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 86402a7c6abe..500701de7f23 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -16,8 +16,11 @@ package android.view; +import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT; + import android.Manifest.permission; import android.annotation.IntDef; +import android.annotation.LongDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; @@ -35,6 +38,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; +import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -267,93 +271,93 @@ public interface WindowManager extends ViewManager { */ @ViewDebug.ExportedProperty(mapping = { @ViewDebug.IntToString(from = TYPE_BASE_APPLICATION, - to = "TYPE_BASE_APPLICATION"), + to = "BASE_APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION, - to = "TYPE_APPLICATION"), + to = "APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION_STARTING, - to = "TYPE_APPLICATION_STARTING"), + to = "APPLICATION_STARTING"), @ViewDebug.IntToString(from = TYPE_DRAWN_APPLICATION, - to = "TYPE_DRAWN_APPLICATION"), + to = "DRAWN_APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION_PANEL, - to = "TYPE_APPLICATION_PANEL"), + to = "APPLICATION_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_MEDIA, - to = "TYPE_APPLICATION_MEDIA"), + to = "APPLICATION_MEDIA"), @ViewDebug.IntToString(from = TYPE_APPLICATION_SUB_PANEL, - to = "TYPE_APPLICATION_SUB_PANEL"), + to = "APPLICATION_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_ABOVE_SUB_PANEL, - to = "TYPE_APPLICATION_ABOVE_SUB_PANEL"), + to = "APPLICATION_ABOVE_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_ATTACHED_DIALOG, - to = "TYPE_APPLICATION_ATTACHED_DIALOG"), + to = "APPLICATION_ATTACHED_DIALOG"), @ViewDebug.IntToString(from = TYPE_APPLICATION_MEDIA_OVERLAY, - to = "TYPE_APPLICATION_MEDIA_OVERLAY"), + to = "APPLICATION_MEDIA_OVERLAY"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR, - to = "TYPE_STATUS_BAR"), + to = "STATUS_BAR"), @ViewDebug.IntToString(from = TYPE_SEARCH_BAR, - to = "TYPE_SEARCH_BAR"), + to = "SEARCH_BAR"), @ViewDebug.IntToString(from = TYPE_PHONE, - to = "TYPE_PHONE"), + to = "PHONE"), @ViewDebug.IntToString(from = TYPE_SYSTEM_ALERT, - to = "TYPE_SYSTEM_ALERT"), + to = "SYSTEM_ALERT"), @ViewDebug.IntToString(from = TYPE_TOAST, - to = "TYPE_TOAST"), + to = "TOAST"), @ViewDebug.IntToString(from = TYPE_SYSTEM_OVERLAY, - to = "TYPE_SYSTEM_OVERLAY"), + to = "SYSTEM_OVERLAY"), @ViewDebug.IntToString(from = TYPE_PRIORITY_PHONE, - to = "TYPE_PRIORITY_PHONE"), + to = "PRIORITY_PHONE"), @ViewDebug.IntToString(from = TYPE_SYSTEM_DIALOG, - to = "TYPE_SYSTEM_DIALOG"), + to = "SYSTEM_DIALOG"), @ViewDebug.IntToString(from = TYPE_KEYGUARD_DIALOG, - to = "TYPE_KEYGUARD_DIALOG"), + to = "KEYGUARD_DIALOG"), @ViewDebug.IntToString(from = TYPE_SYSTEM_ERROR, - to = "TYPE_SYSTEM_ERROR"), + to = "SYSTEM_ERROR"), @ViewDebug.IntToString(from = TYPE_INPUT_METHOD, - to = "TYPE_INPUT_METHOD"), + to = "INPUT_METHOD"), @ViewDebug.IntToString(from = TYPE_INPUT_METHOD_DIALOG, - to = "TYPE_INPUT_METHOD_DIALOG"), + to = "INPUT_METHOD_DIALOG"), @ViewDebug.IntToString(from = TYPE_WALLPAPER, - to = "TYPE_WALLPAPER"), + to = "WALLPAPER"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR_PANEL, - to = "TYPE_STATUS_BAR_PANEL"), + to = "STATUS_BAR_PANEL"), @ViewDebug.IntToString(from = TYPE_SECURE_SYSTEM_OVERLAY, - to = "TYPE_SECURE_SYSTEM_OVERLAY"), + to = "SECURE_SYSTEM_OVERLAY"), @ViewDebug.IntToString(from = TYPE_DRAG, - to = "TYPE_DRAG"), + to = "DRAG"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR_SUB_PANEL, - to = "TYPE_STATUS_BAR_SUB_PANEL"), + to = "STATUS_BAR_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_POINTER, - to = "TYPE_POINTER"), + to = "POINTER"), @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR, - to = "TYPE_NAVIGATION_BAR"), + to = "NAVIGATION_BAR"), @ViewDebug.IntToString(from = TYPE_VOLUME_OVERLAY, - to = "TYPE_VOLUME_OVERLAY"), + to = "VOLUME_OVERLAY"), @ViewDebug.IntToString(from = TYPE_BOOT_PROGRESS, - to = "TYPE_BOOT_PROGRESS"), + to = "BOOT_PROGRESS"), @ViewDebug.IntToString(from = TYPE_INPUT_CONSUMER, - to = "TYPE_INPUT_CONSUMER"), + to = "INPUT_CONSUMER"), @ViewDebug.IntToString(from = TYPE_DREAM, - to = "TYPE_DREAM"), + to = "DREAM"), @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR_PANEL, - to = "TYPE_NAVIGATION_BAR_PANEL"), + to = "NAVIGATION_BAR_PANEL"), @ViewDebug.IntToString(from = TYPE_DISPLAY_OVERLAY, - to = "TYPE_DISPLAY_OVERLAY"), + to = "DISPLAY_OVERLAY"), @ViewDebug.IntToString(from = TYPE_MAGNIFICATION_OVERLAY, - to = "TYPE_MAGNIFICATION_OVERLAY"), + to = "MAGNIFICATION_OVERLAY"), @ViewDebug.IntToString(from = TYPE_PRESENTATION, - to = "TYPE_PRESENTATION"), + to = "PRESENTATION"), @ViewDebug.IntToString(from = TYPE_PRIVATE_PRESENTATION, - to = "TYPE_PRIVATE_PRESENTATION"), + to = "PRIVATE_PRESENTATION"), @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION, - to = "TYPE_VOICE_INTERACTION"), + to = "VOICE_INTERACTION"), @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION_STARTING, - to = "TYPE_VOICE_INTERACTION_STARTING"), + to = "VOICE_INTERACTION_STARTING"), @ViewDebug.IntToString(from = TYPE_DOCK_DIVIDER, - to = "TYPE_DOCK_DIVIDER"), + to = "DOCK_DIVIDER"), @ViewDebug.IntToString(from = TYPE_QS_DIALOG, - to = "TYPE_QS_DIALOG"), + to = "QS_DIALOG"), @ViewDebug.IntToString(from = TYPE_SCREENSHOT, - to = "TYPE_SCREENSHOT"), + to = "SCREENSHOT"), @ViewDebug.IntToString(from = TYPE_APPLICATION_OVERLAY, - to = "TYPE_APPLICATION_OVERLAY") + to = "APPLICATION_OVERLAY") }) public int type; @@ -601,8 +605,10 @@ public interface WindowManager extends ViewManager { public static final int TYPE_DRAG = FIRST_SYSTEM_WINDOW+16; /** - * Window type: panel that slides out from under the status bar - * In multiuser systems shows on all users' windows. + * Window type: panel that slides out from over the status bar + * In multiuser systems shows on all users' windows. These windows + * are displayed on top of the stauts bar and any {@link #TYPE_STATUS_BAR_PANEL} + * windows. * @hide */ public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17; @@ -1197,66 +1203,101 @@ public interface WindowManager extends ViewManager { */ @ViewDebug.ExportedProperty(flagMapping = { @ViewDebug.FlagToString(mask = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, equals = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, - name = "FLAG_ALLOW_LOCK_WHILE_SCREEN_ON"), + name = "ALLOW_LOCK_WHILE_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_DIM_BEHIND, equals = FLAG_DIM_BEHIND, - name = "FLAG_DIM_BEHIND"), + name = "DIM_BEHIND"), @ViewDebug.FlagToString(mask = FLAG_BLUR_BEHIND, equals = FLAG_BLUR_BEHIND, - name = "FLAG_BLUR_BEHIND"), + name = "BLUR_BEHIND"), @ViewDebug.FlagToString(mask = FLAG_NOT_FOCUSABLE, equals = FLAG_NOT_FOCUSABLE, - name = "FLAG_NOT_FOCUSABLE"), + name = "NOT_FOCUSABLE"), @ViewDebug.FlagToString(mask = FLAG_NOT_TOUCHABLE, equals = FLAG_NOT_TOUCHABLE, - name = "FLAG_NOT_TOUCHABLE"), + name = "NOT_TOUCHABLE"), @ViewDebug.FlagToString(mask = FLAG_NOT_TOUCH_MODAL, equals = FLAG_NOT_TOUCH_MODAL, - name = "FLAG_NOT_TOUCH_MODAL"), + name = "NOT_TOUCH_MODAL"), @ViewDebug.FlagToString(mask = FLAG_TOUCHABLE_WHEN_WAKING, equals = FLAG_TOUCHABLE_WHEN_WAKING, - name = "FLAG_TOUCHABLE_WHEN_WAKING"), + name = "TOUCHABLE_WHEN_WAKING"), @ViewDebug.FlagToString(mask = FLAG_KEEP_SCREEN_ON, equals = FLAG_KEEP_SCREEN_ON, - name = "FLAG_KEEP_SCREEN_ON"), + name = "KEEP_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_IN_SCREEN, equals = FLAG_LAYOUT_IN_SCREEN, - name = "FLAG_LAYOUT_IN_SCREEN"), + name = "LAYOUT_IN_SCREEN"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_NO_LIMITS, equals = FLAG_LAYOUT_NO_LIMITS, - name = "FLAG_LAYOUT_NO_LIMITS"), + name = "LAYOUT_NO_LIMITS"), @ViewDebug.FlagToString(mask = FLAG_FULLSCREEN, equals = FLAG_FULLSCREEN, - name = "FLAG_FULLSCREEN"), + name = "FULLSCREEN"), @ViewDebug.FlagToString(mask = FLAG_FORCE_NOT_FULLSCREEN, equals = FLAG_FORCE_NOT_FULLSCREEN, - name = "FLAG_FORCE_NOT_FULLSCREEN"), + name = "FORCE_NOT_FULLSCREEN"), @ViewDebug.FlagToString(mask = FLAG_DITHER, equals = FLAG_DITHER, - name = "FLAG_DITHER"), + name = "DITHER"), @ViewDebug.FlagToString(mask = FLAG_SECURE, equals = FLAG_SECURE, - name = "FLAG_SECURE"), + name = "SECURE"), @ViewDebug.FlagToString(mask = FLAG_SCALED, equals = FLAG_SCALED, - name = "FLAG_SCALED"), + name = "SCALED"), @ViewDebug.FlagToString(mask = FLAG_IGNORE_CHEEK_PRESSES, equals = FLAG_IGNORE_CHEEK_PRESSES, - name = "FLAG_IGNORE_CHEEK_PRESSES"), + name = "IGNORE_CHEEK_PRESSES"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_INSET_DECOR, equals = FLAG_LAYOUT_INSET_DECOR, - name = "FLAG_LAYOUT_INSET_DECOR"), + name = "LAYOUT_INSET_DECOR"), @ViewDebug.FlagToString(mask = FLAG_ALT_FOCUSABLE_IM, equals = FLAG_ALT_FOCUSABLE_IM, - name = "FLAG_ALT_FOCUSABLE_IM"), + name = "ALT_FOCUSABLE_IM"), @ViewDebug.FlagToString(mask = FLAG_WATCH_OUTSIDE_TOUCH, equals = FLAG_WATCH_OUTSIDE_TOUCH, - name = "FLAG_WATCH_OUTSIDE_TOUCH"), + name = "WATCH_OUTSIDE_TOUCH"), @ViewDebug.FlagToString(mask = FLAG_SHOW_WHEN_LOCKED, equals = FLAG_SHOW_WHEN_LOCKED, - name = "FLAG_SHOW_WHEN_LOCKED"), + name = "SHOW_WHEN_LOCKED"), @ViewDebug.FlagToString(mask = FLAG_SHOW_WALLPAPER, equals = FLAG_SHOW_WALLPAPER, - name = "FLAG_SHOW_WALLPAPER"), + name = "SHOW_WALLPAPER"), @ViewDebug.FlagToString(mask = FLAG_TURN_SCREEN_ON, equals = FLAG_TURN_SCREEN_ON, - name = "FLAG_TURN_SCREEN_ON"), + name = "TURN_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_DISMISS_KEYGUARD, equals = FLAG_DISMISS_KEYGUARD, - name = "FLAG_DISMISS_KEYGUARD"), + name = "DISMISS_KEYGUARD"), @ViewDebug.FlagToString(mask = FLAG_SPLIT_TOUCH, equals = FLAG_SPLIT_TOUCH, - name = "FLAG_SPLIT_TOUCH"), + name = "SPLIT_TOUCH"), @ViewDebug.FlagToString(mask = FLAG_HARDWARE_ACCELERATED, equals = FLAG_HARDWARE_ACCELERATED, - name = "FLAG_HARDWARE_ACCELERATED"), - @ViewDebug.FlagToString(mask = FLAG_LOCAL_FOCUS_MODE, equals = FLAG_LOCAL_FOCUS_MODE, - name = "FLAG_LOCAL_FOCUS_MODE"), + name = "HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString(mask = FLAG_LAYOUT_IN_OVERSCAN, equals = FLAG_LAYOUT_IN_OVERSCAN, + name = "LOCAL_FOCUS_MODE"), @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_STATUS, equals = FLAG_TRANSLUCENT_STATUS, - name = "FLAG_TRANSLUCENT_STATUS"), + name = "TRANSLUCENT_STATUS"), @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_NAVIGATION, equals = FLAG_TRANSLUCENT_NAVIGATION, - name = "FLAG_TRANSLUCENT_NAVIGATION"), + name = "TRANSLUCENT_NAVIGATION"), + @ViewDebug.FlagToString(mask = FLAG_LOCAL_FOCUS_MODE, equals = FLAG_LOCAL_FOCUS_MODE, + name = "LOCAL_FOCUS_MODE"), + @ViewDebug.FlagToString(mask = FLAG_SLIPPERY, equals = FLAG_SLIPPERY, + name = "FLAG_SLIPPERY"), + @ViewDebug.FlagToString(mask = FLAG_LAYOUT_ATTACHED_IN_DECOR, equals = FLAG_LAYOUT_ATTACHED_IN_DECOR, + name = "FLAG_LAYOUT_ATTACHED_IN_DECOR"), @ViewDebug.FlagToString(mask = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, equals = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - name = "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS") + name = "DRAWS_SYSTEM_BAR_BACKGROUNDS") }, formatToHexString = true) public int flags; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @LongDef( + flag = true, + value = { + LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA, + }) + @interface Flags2 {} + + /** + * Window flag: allow placing the window within the area that overlaps with the + * display cutout. + * + * <p> + * The window must correctly position its contents to take the display cutout into account. + * + * @see DisplayCutout + * @hide for now + */ + public static final long FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA = 0x00000001; + + /** + * Various behavioral options/flags. Default is none. + * + * @see #FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA + * @hide for now + */ + @Flags2 public long flags2; + /** * If the window has requested hardware acceleration, but this is not * allowed in the process it is in, then still render it as if it is @@ -1413,7 +1454,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; /** @@ -1434,9 +1475,104 @@ 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 */ + @ViewDebug.ExportedProperty(flagMapping = { + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED, + equals = PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED, + name = "FAKE_HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED, + equals = PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED, + name = "FORCE_HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS, + equals = PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS, + name = "WANTS_OFFSET_NOTIFICATIONS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SHOW_FOR_ALL_USERS, + equals = PRIVATE_FLAG_SHOW_FOR_ALL_USERS, + name = "SHOW_FOR_ALL_USERS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_NO_MOVE_ANIMATION, + equals = PRIVATE_FLAG_NO_MOVE_ANIMATION, + name = "NO_MOVE_ANIMATION"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_COMPATIBLE_WINDOW, + equals = PRIVATE_FLAG_COMPATIBLE_WINDOW, + name = "COMPATIBLE_WINDOW"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SYSTEM_ERROR, + equals = PRIVATE_FLAG_SYSTEM_ERROR, + name = "SYSTEM_ERROR"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR, + equals = PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR, + name = "INHERIT_TRANSLUCENT_DECOR"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_KEYGUARD, + equals = PRIVATE_FLAG_KEYGUARD, + name = "KEYGUARD"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, + equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, + name = "DISABLE_WALLPAPER_TOUCH_EVENTS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT, + equals = PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT, + name = "FORCE_STATUS_BAR_VISIBLE_TRANSPARENT"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_PRESERVE_GEOMETRY, + equals = PRIVATE_FLAG_PRESERVE_GEOMETRY, + name = "PRESERVE_GEOMETRY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, + equals = PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, + name = "FORCE_DECOR_VIEW_VISIBILITY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH, + equals = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH, + name = "WILL_NOT_REPLACE_ON_RELAUNCH"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME, + equals = PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME, + name = "LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND, + equals = PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND, + name = "FORCE_DRAW_STATUS_BAR_BACKGROUND"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE, + equals = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE, + name = "SUSTAINED_PERFORMANCE_MODE"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + equals = PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + name = "HIDE_NON_SYSTEM_OVERLAY_WINDOWS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY, + equals = PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY, + name = "IS_ROUNDED_CORNERS_OVERLAY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, + equals = PRIVATE_FLAG_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; @@ -1976,7 +2112,7 @@ public interface WindowManager extends ViewManager { * @hide */ @ActivityInfo.ColorMode - private int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT; + private int mColorMode = COLOR_MODE_DEFAULT; public LayoutParams() { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -2441,9 +2577,15 @@ public interface WindowManager extends ViewManager { @Override public String toString() { + return toString(""); + } + + /** + * @hide + */ + public String toString(String prefix) { StringBuilder sb = new StringBuilder(256); - sb.append("WM.LayoutParams{"); - sb.append("("); + sb.append("{("); sb.append(x); sb.append(','); sb.append(y); @@ -2463,26 +2605,19 @@ public interface WindowManager extends ViewManager { sb.append(verticalMargin); } if (gravity != 0) { - sb.append(" gr=#"); - sb.append(Integer.toHexString(gravity)); + sb.append(" gr="); + sb.append(Gravity.toString(gravity)); } if (softInputMode != 0) { - sb.append(" sim=#"); - sb.append(Integer.toHexString(softInputMode)); + sb.append(" sim={"); + sb.append(softInputModeToString(softInputMode)); + sb.append('}'); } sb.append(" ty="); - sb.append(type); - sb.append(" fl=#"); - sb.append(Integer.toHexString(flags)); - if (privateFlags != 0) { - if ((privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) { - sb.append(" compatible=true"); - } - sb.append(" pfl=0x").append(Integer.toHexString(privateFlags)); - } + sb.append(ViewDebug.intToString(LayoutParams.class, "type", type)); if (format != PixelFormat.OPAQUE) { sb.append(" fmt="); - sb.append(format); + sb.append(PixelFormat.formatToString(format)); } if (windowAnimations != 0) { sb.append(" wanim=0x"); @@ -2490,7 +2625,7 @@ public interface WindowManager extends ViewManager { } if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { sb.append(" or="); - sb.append(screenOrientation); + sb.append(ActivityInfo.screenOrientationToString(screenOrientation)); } if (alpha != 1.0f) { sb.append(" alpha="); @@ -2506,7 +2641,7 @@ public interface WindowManager extends ViewManager { } if (rotationAnimation != ROTATION_ANIMATION_ROTATE) { sb.append(" rotAnim="); - sb.append(rotationAnimation); + sb.append(rotationAnimationToString(rotationAnimation)); } if (preferredRefreshRate != 0) { sb.append(" preferredRefreshRate="); @@ -2516,20 +2651,12 @@ public interface WindowManager extends ViewManager { sb.append(" preferredDisplayMode="); sb.append(preferredDisplayModeId); } - if (systemUiVisibility != 0) { - sb.append(" sysui=0x"); - sb.append(Integer.toHexString(systemUiVisibility)); - } - if (subtreeSystemUiVisibility != 0) { - sb.append(" vsysui=0x"); - sb.append(Integer.toHexString(subtreeSystemUiVisibility)); - } if (hasSystemUiListeners) { sb.append(" sysuil="); sb.append(hasSystemUiListeners); } if (inputFeatures != 0) { - sb.append(" if=0x").append(Integer.toHexString(inputFeatures)); + sb.append(" if=").append(inputFeatureToString(inputFeatures)); } if (userActivityTimeout >= 0) { sb.append(" userActivityTimeout=").append(userActivityTimeout); @@ -2545,16 +2672,44 @@ public interface WindowManager extends ViewManager { sb.append(" (!preservePreviousSurfaceInsets)"); } } - if (needsMenuKey != NEEDS_MENU_UNSET) { - sb.append(" needsMenuKey="); - sb.append(needsMenuKey); + if (needsMenuKey == NEEDS_MENU_SET_TRUE) { + sb.append(" needsMenuKey"); + } + if (mColorMode != COLOR_MODE_DEFAULT) { + sb.append(" colorMode=").append(ActivityInfo.colorModeToString(mColorMode)); + } + sb.append(System.lineSeparator()); + sb.append(prefix).append(" fl=").append( + ViewDebug.flagsToString(LayoutParams.class, "flags", flags)); + if (privateFlags != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" pfl=").append(ViewDebug.flagsToString( + LayoutParams.class, "privateFlags", privateFlags)); + } + if (systemUiVisibility != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" sysui=").append(ViewDebug.flagsToString( + View.class, "mSystemUiVisibility", systemUiVisibility)); + } + if (subtreeSystemUiVisibility != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" vsysui=").append(ViewDebug.flagsToString( + View.class, "mSystemUiVisibility", subtreeSystemUiVisibility)); } - sb.append(" colorMode=").append(mColorMode); sb.append('}'); return sb.toString(); } /** + * @hide + */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + proto.write(WindowLayoutParamsProto.TYPE, type); + proto.end(token); + } + + /** * Scale the layout params' coordinates and size. * @hide */ @@ -2624,5 +2779,88 @@ public interface WindowManager extends ViewManager { && width == WindowManager.LayoutParams.MATCH_PARENT && height == WindowManager.LayoutParams.MATCH_PARENT; } + + private static String softInputModeToString(@SoftInputModeFlags int softInputMode) { + final StringBuilder result = new StringBuilder(); + final int state = softInputMode & SOFT_INPUT_MASK_STATE; + if (state != 0) { + result.append("state="); + switch (state) { + case SOFT_INPUT_STATE_UNCHANGED: + result.append("unchanged"); + break; + case SOFT_INPUT_STATE_HIDDEN: + result.append("hidden"); + break; + case SOFT_INPUT_STATE_ALWAYS_HIDDEN: + result.append("always_hidden"); + break; + case SOFT_INPUT_STATE_VISIBLE: + result.append("visible"); + break; + case SOFT_INPUT_STATE_ALWAYS_VISIBLE: + result.append("always_visible"); + break; + default: + result.append(state); + break; + } + result.append(' '); + } + final int adjust = softInputMode & SOFT_INPUT_MASK_ADJUST; + if (adjust != 0) { + result.append("adjust="); + switch (adjust) { + case SOFT_INPUT_ADJUST_RESIZE: + result.append("resize"); + break; + case SOFT_INPUT_ADJUST_PAN: + result.append("pan"); + break; + case SOFT_INPUT_ADJUST_NOTHING: + result.append("nothing"); + break; + default: + result.append(adjust); + break; + } + result.append(' '); + } + if ((softInputMode & SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { + result.append("forwardNavigation").append(' '); + } + result.deleteCharAt(result.length() - 1); + return result.toString(); + } + + private static String rotationAnimationToString(int rotationAnimation) { + switch (rotationAnimation) { + case ROTATION_ANIMATION_UNSPECIFIED: + return "UNSPECIFIED"; + case ROTATION_ANIMATION_ROTATE: + return "ROTATE"; + case ROTATION_ANIMATION_CROSSFADE: + return "CROSSFADE"; + case ROTATION_ANIMATION_JUMPCUT: + return "JUMPCUT"; + case ROTATION_ANIMATION_SEAMLESS: + return "SEAMLESS"; + default: + return Integer.toString(rotationAnimation); + } + } + + private static String inputFeatureToString(int inputFeature) { + switch (inputFeature) { + case INPUT_FEATURE_DISABLE_POINTER_GESTURES: + return "DISABLE_POINTER_GESTURES"; + case INPUT_FEATURE_NO_INPUT_CHANNEL: + return "NO_INPUT_CHANNEL"; + case INPUT_FEATURE_DISABLE_USER_ACTIVITY: + return "DISABLE_USER_ACTIVITY"; + default: + return Integer.toString(inputFeature); + } + } } } diff --git a/core/java/android/view/WindowManagerInternal.java b/core/java/android/view/WindowManagerInternal.java deleted file mode 100644 index 97dff6a860b9..000000000000 --- a/core/java/android/view/WindowManagerInternal.java +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright (C) 2014 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 android.annotation.NonNull; -import android.annotation.Nullable; -import android.graphics.Rect; -import android.graphics.Region; -import android.hardware.display.DisplayManagerInternal; -import android.os.IBinder; -import android.view.animation.Animation; - -import java.util.List; - -/** - * Window manager local system service interface. - * - * @hide Only for use within the system server. - */ -public abstract class WindowManagerInternal { - - /** - * Interface to receive a callback when the windows reported for - * accessibility changed. - */ - public interface WindowsForAccessibilityCallback { - - /** - * Called when the windows for accessibility changed. - * - * @param windows The windows for accessibility. - */ - public void onWindowsForAccessibilityChanged(List<WindowInfo> windows); - } - - /** - * Callbacks for contextual changes that affect the screen magnification - * feature. - */ - public interface MagnificationCallbacks { - - /** - * Called when the region where magnification operates changes. Note that this isn't the - * entire screen. For example, IMEs are not magnified. - * - * @param magnificationRegion the current magnification region - */ - public void onMagnificationRegionChanged(Region magnificationRegion); - - /** - * Called when an application requests a rectangle on the screen to allow - * the client to apply the appropriate pan and scale. - * - * @param left The rectangle left. - * @param top The rectangle top. - * @param right The rectangle right. - * @param bottom The rectangle bottom. - */ - public void onRectangleOnScreenRequested(int left, int top, int right, int bottom); - - /** - * Notifies that the rotation changed. - * - * @param rotation The current rotation. - */ - public void onRotationChanged(int rotation); - - /** - * Notifies that the context of the user changed. For example, an application - * was started. - */ - public void onUserContextChanged(); - } - - /** - * Abstract class to be notified about {@link com.android.server.wm.AppTransition} events. Held - * as an abstract class so a listener only needs to implement the methods of its interest. - */ - public static abstract class AppTransitionListener { - - /** - * Called when an app transition is being setup and about to be executed. - */ - public void onAppTransitionPendingLocked() {} - - /** - * Called when a pending app transition gets cancelled. - * - * @param transit transition type indicating what kind of transition got cancelled - */ - public void onAppTransitionCancelledLocked(int transit) {} - - /** - * Called when an app transition gets started - * - * @param transit transition type indicating what kind of transition gets run, must be one - * of AppTransition.TRANSIT_* values - * @param openToken the token for the opening app - * @param closeToken the token for the closing app - * @param openAnimation the animation for the opening app - * @param closeAnimation the animation for the closing app - * - * @return Return any bit set of {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_LAYOUT}, - * {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_CONFIG}, - * {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_WALLPAPER}, - * or {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_ANIM}. - */ - public int onAppTransitionStartingLocked(int transit, IBinder openToken, IBinder closeToken, - Animation openAnimation, Animation closeAnimation) { - return 0; - } - - /** - * Called when an app transition is finished running. - * - * @param token the token for app whose transition has finished - */ - public void onAppTransitionFinishedLocked(IBinder token) {} - } - - /** - * An interface to be notified about hardware keyboard status. - */ - public interface OnHardKeyboardStatusChangeListener { - public void onHardKeyboardStatusChange(boolean available); - } - - /** - * Request that the window manager call - * {@link DisplayManagerInternal#performTraversalInTransactionFromWindowManager} - * within a surface transaction at a later time. - */ - public abstract void requestTraversalFromDisplayManager(); - - /** - * Set by the accessibility layer to observe changes in the magnified region, - * rotation, and other window transformations related to display magnification - * as the window manager is responsible for doing the actual magnification - * and has access to the raw window data while the accessibility layer serves - * as a controller. - * - * @param callbacks The callbacks to invoke. - */ - public abstract void setMagnificationCallbacks(@Nullable MagnificationCallbacks callbacks); - - /** - * Set by the accessibility layer to specify the magnification and panning to - * be applied to all windows that should be magnified. - * - * @param spec The MagnficationSpec to set. - * - * @see #setMagnificationCallbacks(MagnificationCallbacks) - */ - public abstract void setMagnificationSpec(MagnificationSpec spec); - - /** - * Set by the accessibility framework to indicate whether the magnifiable regions of the display - * should be shown. - * - * @param show {@code true} to show magnifiable region bounds, {@code false} to hide - */ - public abstract void setForceShowMagnifiableBounds(boolean show); - - /** - * Obtains the magnification regions. - * - * @param magnificationRegion the current magnification region - */ - public abstract void getMagnificationRegion(@NonNull Region magnificationRegion); - - /** - * Gets the magnification and translation applied to a window given its token. - * Not all windows are magnified and the window manager policy determines which - * windows are magnified. The returned result also takes into account the compat - * scale if necessary. - * - * @param windowToken The window's token. - * - * @return The magnification spec for the window. - * - * @see #setMagnificationCallbacks(MagnificationCallbacks) - */ - public abstract MagnificationSpec getCompatibleMagnificationSpecForWindow( - IBinder windowToken); - - /** - * Sets a callback for observing which windows are touchable for the purposes - * of accessibility. - * - * @param callback The callback. - */ - public abstract void setWindowsForAccessibilityCallback( - WindowsForAccessibilityCallback callback); - - /** - * Sets a filter for manipulating the input event stream. - * - * @param filter The filter implementation. - */ - public abstract void setInputFilter(IInputFilter filter); - - /** - * Gets the token of the window that has input focus. - * - * @return The token. - */ - public abstract IBinder getFocusedWindowToken(); - - /** - * @return Whether the keyguard is engaged. - */ - public abstract boolean isKeyguardLocked(); - - /** - * @return Whether the keyguard is showing and not occluded. - */ - public abstract boolean isKeyguardShowingAndNotOccluded(); - - /** - * Gets the frame of a window given its token. - * - * @param token The token. - * @param outBounds The frame to populate. - */ - public abstract void getWindowFrame(IBinder token, Rect outBounds); - - /** - * Opens the global actions dialog. - */ - public abstract void showGlobalActions(); - - /** - * Invalidate all visible windows. Then report back on the callback once all windows have - * redrawn. - */ - public abstract void waitForAllWindowsDrawn(Runnable callback, long timeout); - - /** - * Adds a window token for a given window type. - * - * @param token The token to add. - * @param type The window type. - * @param displayId The display to add the token to. - */ - public abstract void addWindowToken(android.os.IBinder token, int type, int displayId); - - /** - * Removes a window token. - * - * @param token The toke to remove. - * @param removeWindows Whether to also remove the windows associated with the token. - * @param displayId The display to remove the token from. - */ - public abstract void removeWindowToken(android.os.IBinder token, boolean removeWindows, - int displayId); - - /** - * Registers a listener to be notified about app transition events. - * - * @param listener The listener to register. - */ - public abstract void registerAppTransitionListener(AppTransitionListener listener); - - /** - * Retrieves a height of input method window. - */ - public abstract int getInputMethodWindowVisibleHeight(); - - /** - * Saves last input method window for transition. - * - * Note that it is assumed that this method is called only by InputMethodManagerService. - */ - public abstract void saveLastInputMethodWindowForTransition(); - - /** - * Clears last input method window for transition. - * - * Note that it is assumed that this method is called only by InputMethodManagerService. - */ - public abstract void clearLastInputMethodWindowForTransition(); - - /** - * Notifies WindowManagerService that the current IME window status is being changed. - * - * <p>Only {@link com.android.server.InputMethodManagerService} is the expected and tested - * caller of this method.</p> - * - * @param imeToken token to track the active input method. Corresponding IME windows can be - * identified by checking {@link android.view.WindowManager.LayoutParams#token}. - * Note that there is no guarantee that the corresponding window is already - * created - * @param imeWindowVisible whether the active IME thinks that its window should be visible or - * hidden, no matter how WindowManagerService will react / has reacted - * to corresponding API calls. Note that this state is not guaranteed - * to be synchronized with state in WindowManagerService. - * @param dismissImeOnBackKeyPressed {@code true} if the software keyboard is shown and the back - * key is expected to dismiss the software keyboard. - * @param targetWindowToken token to identify the target window that the IME is associated with. - * {@code null} when application, system, or the IME itself decided to - * change its window visibility before being associated with any target - * window. - */ - public abstract void updateInputMethodWindowStatus(@NonNull IBinder imeToken, - boolean imeWindowVisible, boolean dismissImeOnBackKeyPressed, - @Nullable IBinder targetWindowToken); - - /** - * Returns true when the hardware keyboard is available. - */ - public abstract boolean isHardKeyboardAvailable(); - - /** - * Sets the callback listener for hardware keyboard status changes. - * - * @param listener The listener to set. - */ - public abstract void setOnHardKeyboardStatusChangeListener( - OnHardKeyboardStatusChangeListener listener); - - /** Returns true if the stack with the input Id is currently visible. */ - public abstract boolean isStackVisible(int stackId); - - /** - * @return True if and only if the docked divider is currently in resize mode. - */ - public abstract boolean isDockedDividerResizing(); - - /** - * Requests the window manager to recompute the windows for accessibility. - */ - public abstract void computeWindowsForAccessibility(); - - /** - * Called after virtual display Id is updated by - * {@link com.android.server.vr.Vr2dDisplay} with a specific - * {@param vr2dDisplayId}. - */ - public abstract void setVr2dDisplayId(int vr2dDisplayId); -} diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java deleted file mode 100644 index c4ffb4c06a26..000000000000 --- a/core/java/android/view/WindowManagerPolicy.java +++ /dev/null @@ -1,1744 +0,0 @@ -/* - * Copyright (C) 2006 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.Manifest.permission; -import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; -import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; -import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; -import static android.view.WindowManager.LayoutParams.TYPE_BOOT_PROGRESS; -import static android.view.WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; -import static android.view.WindowManager.LayoutParams.TYPE_DRAG; -import static android.view.WindowManager.LayoutParams.TYPE_DREAM; -import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER; -import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; -import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG; -import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; -import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; -import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; -import static android.view.WindowManager.LayoutParams.TYPE_PHONE; -import static android.view.WindowManager.LayoutParams.TYPE_POINTER; -import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION; -import static android.view.WindowManager.LayoutParams.TYPE_PRIORITY_PHONE; -import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION; -import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG; -import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; -import static android.view.WindowManager.LayoutParams.TYPE_SEARCH_BAR; -import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; -import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL; -import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; -import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; -import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG; -import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; -import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_TOAST; -import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; -import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION_STARTING; -import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; -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; -import android.content.res.Configuration; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.Bundle; -import android.os.IBinder; -import android.os.Looper; -import android.os.RemoteException; -import android.util.Slog; -import android.view.animation.Animation; - -import com.android.internal.policy.IKeyguardDismissCallback; -import com.android.internal.policy.IShortcutService; - -import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * This interface supplies all UI-specific behavior of the window manager. An - * instance of it is created by the window manager when it starts up, and allows - * customization of window layering, special window types, key dispatching, and - * layout. - * - * <p>Because this provides deep interaction with the system window manager, - * specific methods on this interface can be called from a variety of contexts - * with various restrictions on what they can do. These are encoded through - * a suffixes at the end of a method encoding the thread the method is called - * from and any locks that are held when it is being called; if no suffix - * is attached to a method, then it is not called with any locks and may be - * called from the main window manager thread or another thread calling into - * the window manager. - * - * <p>The current suffixes are: - * - * <dl> - * <dt> Ti <dd> Called from the input thread. This is the thread that - * collects pending input events and dispatches them to the appropriate window. - * It may block waiting for events to be processed, so that the input stream is - * properly serialized. - * <dt> Tq <dd> Called from the low-level input queue thread. This is the - * thread that reads events out of the raw input devices and places them - * into the global input queue that is read by the <var>Ti</var> thread. - * This thread should not block for a long period of time on anything but the - * key driver. - * <dt> Lw <dd> Called with the main window manager lock held. Because the - * window manager is a very low-level system service, there are few other - * system services you can call with this lock held. It is explicitly okay to - * make calls into the package manager and power manager; it is explicitly not - * okay to make calls into the activity manager or most other services. Note that - * {@link android.content.Context#checkPermission(String, int, int)} and - * variations require calling into the activity manager. - * <dt> Li <dd> Called with the input thread lock held. This lock can be - * acquired by the window manager while it holds the window lock, so this is - * even more restrictive than <var>Lw</var>. - * </dl> - * - * @hide - */ -public interface WindowManagerPolicy { - // Policy flags. These flags are also defined in frameworks/base/include/ui/Input.h. - public final static int FLAG_WAKE = 0x00000001; - public final static int FLAG_VIRTUAL = 0x00000002; - - public final static int FLAG_INJECTED = 0x01000000; - public final static int FLAG_TRUSTED = 0x02000000; - public final static int FLAG_FILTERED = 0x04000000; - public final static int FLAG_DISABLE_KEY_REPEAT = 0x08000000; - - public final static int FLAG_INTERACTIVE = 0x20000000; - public final static int FLAG_PASS_TO_USER = 0x40000000; - - // Flags for IActivityManager.keyguardGoingAway() - public final static int KEYGUARD_GOING_AWAY_FLAG_TO_SHADE = 1 << 0; - public final static int KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS = 1 << 1; - public final static int KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER = 1 << 2; - - // Flags used for indicating whether the internal and/or external input devices - // of some type are available. - public final static int PRESENCE_INTERNAL = 1 << 0; - public final static int PRESENCE_EXTERNAL = 1 << 1; - - // Navigation bar position values - int NAV_BAR_LEFT = 1 << 0; - int NAV_BAR_RIGHT = 1 << 1; - int NAV_BAR_BOTTOM = 1 << 2; - - public final static boolean WATCH_POINTER = false; - - /** - * Sticky broadcast of the current HDMI plugged state. - */ - public final static String ACTION_HDMI_PLUGGED = "android.intent.action.HDMI_PLUGGED"; - - /** - * Extra in {@link #ACTION_HDMI_PLUGGED} indicating the state: true if - * plugged in to HDMI, false if not. - */ - public final static String EXTRA_HDMI_PLUGGED_STATE = "state"; - - /** - * Set to {@code true} when intent was invoked from pressing the home key. - * @hide - */ - @SystemApi - public static final String EXTRA_FROM_HOME_KEY = "android.intent.extra.FROM_HOME_KEY"; - - /** - * Pass this event to the user / app. To be returned from - * {@link #interceptKeyBeforeQueueing}. - */ - public final static int ACTION_PASS_TO_USER = 0x00000001; - - /** - * Register shortcuts for window manager to dispatch. - * Shortcut code is packed as (metaState << Integer.SIZE) | keyCode - * @hide - */ - void registerShortcutKey(long shortcutCode, IShortcutService shortcutKeyReceiver) - throws RemoteException; - - /** - * Called when the Keyguard occluded state changed. - * @param occluded Whether Keyguard is currently occluded or not. - */ - void onKeyguardOccludedChangedLw(boolean occluded); - - /** - * Interface to the Window Manager state associated with a particular - * window. You can hold on to an instance of this interface from the call - * to prepareAddWindow() until removeWindow(). - */ - public interface WindowState { - /** - * Return the uid of the app that owns this window. - */ - int getOwningUid(); - - /** - * Return the package name of the app that owns this window. - */ - String getOwningPackage(); - - /** - * Perform standard frame computation. The result can be obtained with - * getFrame() if so desired. Must be called with the window manager - * lock held. - * - * @param parentFrame The frame of the parent container this window - * is in, used for computing its basic position. - * @param displayFrame The frame of the overall display in which this - * window can appear, used for constraining the overall dimensions - * of the window. - * @param overlayFrame The frame within the display that is inside - * of the overlay region. - * @param contentFrame The frame within the display in which we would - * like active content to appear. This will cause windows behind to - * be resized to match the given content frame. - * @param visibleFrame The frame within the display that the window - * is actually visible, used for computing its visible insets to be - * given to windows behind. - * This can be used as a hint for scrolling (avoiding resizing) - * the window to make certain that parts of its content - * are visible. - * @param decorFrame The decor frame specified by policy specific to this window, - * to use for proper cropping during animation. - * @param stableFrame The frame around which stable system decoration is positioned. - * @param outsetFrame The frame that includes areas that aren't part of the surface but we - * want to treat them as such. - */ - public void computeFrameLw(Rect parentFrame, Rect displayFrame, - Rect overlayFrame, Rect contentFrame, Rect visibleFrame, Rect decorFrame, - Rect stableFrame, Rect outsetFrame); - - /** - * Retrieve the current frame of the window that has been assigned by - * the window manager. Must be called with the window manager lock held. - * - * @return Rect The rectangle holding the window frame. - */ - public Rect getFrameLw(); - - /** - * Retrieve the current position of the window that is actually shown. - * Must be called with the window manager lock held. - * - * @return Point The point holding the shown window position. - */ - public Point getShownPositionLw(); - - /** - * Retrieve the frame of the display that this window was last - * laid out in. Must be called with the - * window manager lock held. - * - * @return Rect The rectangle holding the display frame. - */ - public Rect getDisplayFrameLw(); - - /** - * Retrieve the frame of the area inside the overscan region of the - * display that this window was last laid out in. Must be called with the - * window manager lock held. - * - * @return Rect The rectangle holding the display overscan frame. - */ - public Rect getOverscanFrameLw(); - - /** - * Retrieve the frame of the content area that this window was last - * laid out in. This is the area in which the content of the window - * should be placed. It will be smaller than the display frame to - * account for screen decorations such as a status bar or soft - * keyboard. Must be called with the - * window manager lock held. - * - * @return Rect The rectangle holding the content frame. - */ - public Rect getContentFrameLw(); - - /** - * Retrieve the frame of the visible area that this window was last - * laid out in. This is the area of the screen in which the window - * will actually be fully visible. It will be smaller than the - * content frame to account for transient UI elements blocking it - * such as an input method's candidates UI. Must be called with the - * window manager lock held. - * - * @return Rect The rectangle holding the visible frame. - */ - public Rect getVisibleFrameLw(); - - /** - * Returns true if this window is waiting to receive its given - * internal insets from the client app, and so should not impact the - * layout of other windows. - */ - public boolean getGivenInsetsPendingLw(); - - /** - * Retrieve the insets given by this window's client for the content - * area of windows behind it. Must be called with the - * window manager lock held. - * - * @return Rect The left, top, right, and bottom insets, relative - * to the window's frame, of the actual contents. - */ - public Rect getGivenContentInsetsLw(); - - /** - * Retrieve the insets given by this window's client for the visible - * area of windows behind it. Must be called with the - * window manager lock held. - * - * @return Rect The left, top, right, and bottom insets, relative - * to the window's frame, of the actual visible area. - */ - public Rect getGivenVisibleInsetsLw(); - - /** - * Retrieve the current LayoutParams of the window. - * - * @return WindowManager.LayoutParams The window's internal LayoutParams - * instance. - */ - public WindowManager.LayoutParams getAttrs(); - - /** - * Return whether this window needs the menu key shown. Must be called - * with window lock held, because it may need to traverse down through - * window list to determine the result. - * @param bottom The bottom-most window to consider when determining this. - */ - public boolean getNeedsMenuLw(WindowState bottom); - - /** - * Retrieve the current system UI visibility flags associated with - * this window. - */ - public int getSystemUiVisibility(); - - /** - * Get the layer at which this window's surface will be Z-ordered. - */ - public int getSurfaceLayer(); - - /** - * Retrieve the type of the top-level window. - * - * @return the base type of the parent window if attached or its own type otherwise - */ - public int getBaseType(); - - /** - * Return the token for the application (actually activity) that owns - * this window. May return null for system windows. - * - * @return An IApplicationToken identifying the owning activity. - */ - public IApplicationToken getAppToken(); - - /** - * Return true if this window is participating in voice interaction. - */ - public boolean isVoiceInteraction(); - - /** - * Return true if, at any point, the application token associated with - * this window has actually displayed any windows. This is most useful - * with the "starting up" window to determine if any windows were - * displayed when it is closed. - * - * @return Returns true if one or more windows have been displayed, - * else false. - */ - public boolean hasAppShownWindows(); - - /** - * Is this window visible? It is not visible if there is no - * surface, or we are in the process of running an exit animation - * that will remove the surface. - */ - boolean isVisibleLw(); - - /** - * Is this window currently visible to the user on-screen? It is - * displayed either if it is visible or it is currently running an - * animation before no longer being visible. Must be called with the - * window manager lock held. - */ - boolean isDisplayedLw(); - - /** - * Return true if this window (or a window it is attached to, but not - * considering its app token) is currently animating. - */ - boolean isAnimatingLw(); - - /** - * @return Whether the window can affect SystemUI flags, meaning that SystemUI (system bars, - * for example) will be affected by the flags specified in this window. This is the - * case when the surface is on screen but not exiting. - */ - boolean canAffectSystemUiFlags(); - - /** - * Is this window considered to be gone for purposes of layout? - */ - boolean isGoneForLayoutLw(); - - /** - * Returns true if the window has a surface that it has drawn a - * complete UI in to. Note that this is different from {@link #hasDrawnLw()} - * in that it also returns true if the window is READY_TO_SHOW, but was not yet - * promoted to HAS_DRAWN. - */ - boolean isDrawnLw(); - - /** - * Returns true if this window has been shown on screen at some time in - * the past. Must be called with the window manager lock held. - */ - public boolean hasDrawnLw(); - - /** - * Can be called by the policy to force a window to be hidden, - * regardless of whether the client or window manager would like - * it shown. Must be called with the window manager lock held. - * Returns true if {@link #showLw} was last called for the window. - */ - public boolean hideLw(boolean doAnimation); - - /** - * Can be called to undo the effect of {@link #hideLw}, allowing a - * window to be shown as long as the window manager and client would - * also like it to be shown. Must be called with the window manager - * lock held. - * Returns true if {@link #hideLw} was last called for the window. - */ - public boolean showLw(boolean doAnimation); - - /** - * Check whether the process hosting this window is currently alive. - */ - public boolean isAlive(); - - /** - * Check if window is on {@link Display#DEFAULT_DISPLAY}. - * @return true if window is on default display. - */ - public boolean isDefaultDisplay(); - - /** - * Check whether the window is currently dimming. - */ - public boolean isDimming(); - - /** - * @return the stack id this windows belongs to, or {@link StackId#INVALID_STACK_ID} if - * not attached to any stack. - */ - int getStackId(); - - /** - * Returns true if the window is current in multi-windowing mode. i.e. it shares the - * screen with other application windows. - */ - public boolean isInMultiWindowMode(); - - public int getRotationAnimationHint(); - - public boolean isInputMethodWindow(); - - public int getDisplayId(); - - /** - * Returns true if the window owner can add internal system windows. - * That is, they have {@link permission#INTERNAL_SYSTEM_WINDOW}. - */ - default boolean canAddInternalSystemWindow() { - return false; - } - - /** - * Returns true if the window owner has the permission to acquire a sleep token when it's - * visible. That is, they have the permission {@link permission#DEVICE_POWER}. - */ - boolean canAcquireSleepToken(); - } - - /** - * Representation of a input consumer that the policy has added to the - * window manager to consume input events going to windows below it. - */ - public interface InputConsumer { - /** - * Remove the input consumer from the window manager. - */ - void dismiss(); - } - - /** - * Holds the contents of a starting window. {@link #addSplashScreen} needs to wrap the - * contents of the starting window into an class implementing this interface, which then will be - * held by WM and released with {@link #remove} when no longer needed. - */ - interface StartingSurface { - - /** - * Removes the starting window surface. Do not hold the window manager lock when calling - * this method! - */ - void remove(); - } - - /** - * Interface for calling back in to the window manager that is private - * between it and the policy. - */ - public interface WindowManagerFuncs { - public static final int LID_ABSENT = -1; - public static final int LID_CLOSED = 0; - public static final int LID_OPEN = 1; - - public static final int CAMERA_LENS_COVER_ABSENT = -1; - public static final int CAMERA_LENS_UNCOVERED = 0; - public static final int CAMERA_LENS_COVERED = 1; - - /** - * Ask the window manager to re-evaluate the system UI flags. - */ - public void reevaluateStatusBarVisibility(); - - /** - * Add a input consumer which will consume all input events going to any window below it. - */ - public InputConsumer createInputConsumer(Looper looper, String name, - InputEventReceiver.Factory inputEventReceiverFactory); - - /** - * Returns a code that describes the current state of the lid switch. - */ - public int getLidState(); - - /** - * Lock the device now. - */ - public void lockDeviceNow(); - - /** - * Returns a code that descripbes whether the camera lens is covered or not. - */ - public int getCameraLensCoverState(); - - /** - * Switch the input method, to be precise, input method subtype. - * - * @param forwardDirection {@code true} to rotate in a forward direction. - */ - public void switchInputMethod(boolean forwardDirection); - - public void shutdown(boolean confirm); - public void reboot(boolean confirm); - public void rebootSafeMode(boolean confirm); - - /** - * Return the window manager lock needed to correctly call "Lw" methods. - */ - public Object getWindowManagerLock(); - - /** Register a system listener for touch events */ - void registerPointerEventListener(PointerEventListener listener); - - /** Unregister a system listener for touch events */ - void unregisterPointerEventListener(PointerEventListener listener); - - /** - * @return The content insets of the docked divider window. - */ - int getDockedDividerInsetsLw(); - - /** - * Retrieves the {@param outBounds} from the stack with id {@param stackId}. - */ - void getStackBounds(int stackId, Rect outBounds); - - /** - * Notifies window manager that {@link #isShowingDreamLw} has changed. - */ - void notifyShowingDreamChanged(); - - /** - * @return The currently active input method window. - */ - WindowState getInputMethodWindowLw(); - - /** - * Notifies window manager that {@link #isKeyguardTrustedLw} has changed. - */ - void notifyKeyguardTrustedChanged(); - - /** - * Notifies the window manager that screen is being turned off. - * - * @param listener callback to call when display can be turned off - */ - void screenTurningOff(ScreenOffListener listener); - } - - public interface PointerEventListener { - /** - * 1. onPointerEvent will be called on the service.UiThread. - * 2. motionEvent will be recycled after onPointerEvent returns so if it is needed later a - * copy() must be made and the copy must be recycled. - **/ - void onPointerEvent(MotionEvent motionEvent); - - /** - * @see #onPointerEvent(MotionEvent) - **/ - default void onPointerEvent(MotionEvent motionEvent, int displayId) { - if (displayId == DEFAULT_DISPLAY) { - onPointerEvent(motionEvent); - } - } - } - - /** Window has been added to the screen. */ - public static final int TRANSIT_ENTER = 1; - /** Window has been removed from the screen. */ - public static final int TRANSIT_EXIT = 2; - /** Window has been made visible. */ - public static final int TRANSIT_SHOW = 3; - /** Window has been made invisible. - * TODO: Consider removal as this is unused. */ - public static final int TRANSIT_HIDE = 4; - /** The "application starting" preview window is no longer needed, and will - * animate away to show the real window. */ - public static final int TRANSIT_PREVIEW_DONE = 5; - - // NOTE: screen off reasons are in order of significance, with more - // important ones lower than less important ones. - - /** Screen turned off because of a device admin */ - public final int OFF_BECAUSE_OF_ADMIN = 1; - /** Screen turned off because of power button */ - public final int OFF_BECAUSE_OF_USER = 2; - /** Screen turned off because of timeout */ - public final int OFF_BECAUSE_OF_TIMEOUT = 3; - - /** @hide */ - @IntDef({USER_ROTATION_FREE, USER_ROTATION_LOCKED}) - @Retention(RetentionPolicy.SOURCE) - public @interface UserRotationMode {} - - /** When not otherwise specified by the activity's screenOrientation, rotation should be - * determined by the system (that is, using sensors). */ - public final int USER_ROTATION_FREE = 0; - /** When not otherwise specified by the activity's screenOrientation, rotation is set by - * the user. */ - public final int USER_ROTATION_LOCKED = 1; - - /** - * Perform initialization of the policy. - * - * @param context The system context we are running in. - */ - public void init(Context context, IWindowManager windowManager, - WindowManagerFuncs windowManagerFuncs); - - /** - * @return true if com.android.internal.R.bool#config_forceDefaultOrientation is true. - */ - public boolean isDefaultOrientationForced(); - - /** - * Called by window manager once it has the initial, default native - * display dimensions. - */ - 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. - * @param outAppOp First element will be filled with the app op corresponding to - * this window, or OP_NONE. - * - * @return {@link WindowManagerGlobal#ADD_OKAY} if the add can proceed; - * else an error code, usually - * {@link WindowManagerGlobal#ADD_PERMISSION_DENIED}, to abort the add. - */ - public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp); - - /** - * Check permissions when adding a window. - * - * @param attrs The window's LayoutParams. - * - * @return True if the window may only be shown to the current user, false if the window can - * be shown on all users' windows. - */ - public boolean checkShowToOwnerOnly(WindowManager.LayoutParams attrs); - - /** - * Sanitize the layout parameters coming from a client. Allows the policy - * to do things like ensure that windows of a specific type can't take - * input focus. - * - * @param attrs The window layout parameters to be modified. These values - * are modified in-place. - */ - public void adjustWindowParamsLw(WindowManager.LayoutParams attrs); - - /** - * After the window manager has computed the current configuration based - * on its knowledge of the display and input devices, it gives the policy - * a chance to adjust the information contained in it. If you want to - * leave it as-is, simply do nothing. - * - * <p>This method may be called by any thread in the window manager, but - * no internal locks in the window manager will be held. - * - * @param config The Configuration being computed, for you to change as - * desired. - * @param keyboardPresence Flags that indicate whether internal or external - * keyboards are present. - * @param navigationPresence Flags that indicate whether internal or external - * navigation devices are present. - */ - public void adjustConfigurationLw(Configuration config, int keyboardPresence, - int navigationPresence); - - /** - * Returns the layer assignment for the window state. Allows you to control how different - * kinds of windows are ordered on-screen. - * - * @param win The window state - * @return int An arbitrary integer used to order windows, with lower numbers below higher ones. - */ - default int getWindowLayerLw(WindowState win) { - return getWindowLayerFromTypeLw(win.getBaseType(), win.canAddInternalSystemWindow()); - } - - /** - * Returns the layer assignment for the window type. Allows you to control how different - * kinds of windows are ordered on-screen. - * - * @param type The type of window being assigned. - * @return int An arbitrary integer used to order windows, with lower numbers below higher ones. - */ - default int getWindowLayerFromTypeLw(int type) { - if (isSystemAlertWindowType(type)) { - throw new IllegalArgumentException("Use getWindowLayerFromTypeLw() or" - + " getWindowLayerLw() for alert window types"); - } - return getWindowLayerFromTypeLw(type, false /* canAddInternalSystemWindow */); - } - - /** - * Returns the layer assignment for the window type. Allows you to control how different - * kinds of windows are ordered on-screen. - * - * @param type The type of window being assigned. - * @param canAddInternalSystemWindow If the owner window associated with the type we are - * evaluating can add internal system windows. I.e they have - * {@link permission#INTERNAL_SYSTEM_WINDOW}. If true, alert window - * types {@link android.view.WindowManager.LayoutParams#isSystemAlertWindowType(int)} - * can be assigned layers greater than the layer for - * {@link android.view.WindowManager.LayoutParams#TYPE_APPLICATION_OVERLAY} Else, their - * layers would be lesser. - * @return int An arbitrary integer used to order windows, with lower numbers below higher ones. - */ - default int getWindowLayerFromTypeLw(int type, boolean canAddInternalSystemWindow) { - if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) { - return APPLICATION_LAYER; - } - - switch (type) { - case TYPE_WALLPAPER: - // wallpaper is at the bottom, though the window manager may move it. - return 1; - case TYPE_PRESENTATION: - case TYPE_PRIVATE_PRESENTATION: - return APPLICATION_LAYER; - case TYPE_DOCK_DIVIDER: - return APPLICATION_LAYER; - case TYPE_QS_DIALOG: - return APPLICATION_LAYER; - case TYPE_PHONE: - return 3; - case TYPE_SEARCH_BAR: - case TYPE_VOICE_INTERACTION_STARTING: - return 4; - case TYPE_VOICE_INTERACTION: - // voice interaction layer is almost immediately above apps. - return 5; - case TYPE_INPUT_CONSUMER: - return 6; - case TYPE_SYSTEM_DIALOG: - return 7; - case TYPE_TOAST: - // toasts and the plugged-in battery thing - return 8; - case TYPE_PRIORITY_PHONE: - // SIM errors and unlock. Not sure if this really should be in a high layer. - return 9; - case TYPE_SYSTEM_ALERT: - // like the ANR / app crashed dialogs - return canAddInternalSystemWindow ? 11 : 10; - case TYPE_APPLICATION_OVERLAY: - return 12; - case TYPE_DREAM: - // used for Dreams (screensavers with TYPE_DREAM windows) - return 13; - case TYPE_INPUT_METHOD: - // on-screen keyboards and other such input method user interfaces go here. - return 14; - case TYPE_INPUT_METHOD_DIALOG: - // on-screen keyboards and other such input method user interfaces go here. - return 15; - case TYPE_STATUS_BAR_SUB_PANEL: - return 17; - case TYPE_STATUS_BAR: - return 18; - case TYPE_STATUS_BAR_PANEL: - return 19; - case TYPE_KEYGUARD_DIALOG: - return 20; - case TYPE_VOLUME_OVERLAY: - // the on-screen volume indicator and controller shown when the user - // changes the device volume - return 21; - case TYPE_SYSTEM_OVERLAY: - // the on-screen volume indicator and controller shown when the user - // changes the device volume - return canAddInternalSystemWindow ? 22 : 11; - case TYPE_NAVIGATION_BAR: - // the navigation bar, if available, shows atop most things - return 23; - case TYPE_NAVIGATION_BAR_PANEL: - // some panels (e.g. search) need to show on top of the navigation bar - return 24; - case TYPE_SCREENSHOT: - // screenshot selection layer shouldn't go above system error, but it should cover - // navigation bars at the very least. - return 25; - case TYPE_SYSTEM_ERROR: - // system-level error dialogs - return canAddInternalSystemWindow ? 26 : 10; - case TYPE_MAGNIFICATION_OVERLAY: - // used to highlight the magnified portion of a display - return 27; - case TYPE_DISPLAY_OVERLAY: - // used to simulate secondary display devices - return 28; - case TYPE_DRAG: - // the drag layer: input for drag-and-drop is associated with this window, - // which sits above all other focusable windows - return 29; - case TYPE_ACCESSIBILITY_OVERLAY: - // overlay put by accessibility services to intercept user interaction - return 30; - case TYPE_SECURE_SYSTEM_OVERLAY: - return 31; - case TYPE_BOOT_PROGRESS: - return 32; - case TYPE_POINTER: - // the (mouse) pointer layer - return 33; - default: - Slog.e("WindowManager", "Unknown window type: " + type); - return APPLICATION_LAYER; - } - } - - int APPLICATION_LAYER = 2; - int APPLICATION_MEDIA_SUBLAYER = -2; - int APPLICATION_MEDIA_OVERLAY_SUBLAYER = -1; - int APPLICATION_PANEL_SUBLAYER = 1; - int APPLICATION_SUB_PANEL_SUBLAYER = 2; - int APPLICATION_ABOVE_SUB_PANEL_SUBLAYER = 3; - - /** - * Return how to Z-order sub-windows in relation to the window they are attached to. - * Return positive to have them ordered in front, negative for behind. - * - * @param type The sub-window type code. - * - * @return int Layer in relation to the attached window, where positive is - * above and negative is below. - */ - default int getSubWindowLayerFromTypeLw(int type) { - switch (type) { - case TYPE_APPLICATION_PANEL: - case TYPE_APPLICATION_ATTACHED_DIALOG: - return APPLICATION_PANEL_SUBLAYER; - case TYPE_APPLICATION_MEDIA: - return APPLICATION_MEDIA_SUBLAYER; - case TYPE_APPLICATION_MEDIA_OVERLAY: - return APPLICATION_MEDIA_OVERLAY_SUBLAYER; - case TYPE_APPLICATION_SUB_PANEL: - return APPLICATION_SUB_PANEL_SUBLAYER; - case TYPE_APPLICATION_ABOVE_SUB_PANEL: - return APPLICATION_ABOVE_SUB_PANEL_SUBLAYER; - } - Slog.e("WindowManager", "Unknown sub-window type: " + type); - return 0; - } - - /** - * Get the highest layer (actually one more than) that the wallpaper is - * allowed to be in. - */ - public int getMaxWallpaperLayer(); - - /** - * Return the display width available after excluding any screen - * decorations that could never be removed in Honeycomb. That is, system bar or - * button bar. - */ - public int getNonDecorDisplayWidth(int fullWidth, int fullHeight, int rotation, - int uiMode, int displayId); - - /** - * Return the display height available after excluding any screen - * decorations that could never be removed in Honeycomb. That is, system bar or - * button bar. - */ - public int getNonDecorDisplayHeight(int fullWidth, int fullHeight, int rotation, - int uiMode, int displayId); - - /** - * Return the available screen width that we should report for the - * configuration. This must be no larger than - * {@link #getNonDecorDisplayWidth(int, int, int)}; it may be smaller than - * that to account for more transient decoration like a status bar. - */ - public int getConfigDisplayWidth(int fullWidth, int fullHeight, int rotation, - int uiMode, int displayId); - - /** - * Return the available screen height that we should report for the - * configuration. This must be no larger than - * {@link #getNonDecorDisplayHeight(int, int, int)}; it may be smaller than - * that to account for more transient decoration like a status bar. - */ - public int getConfigDisplayHeight(int fullWidth, int fullHeight, int rotation, - int uiMode, int displayId); - - /** - * Return whether the given window can become the Keyguard window. Typically returns true for - * the StatusBar. - */ - public boolean isKeyguardHostWindow(WindowManager.LayoutParams attrs); - - /** - * @return whether {@param win} can be hidden by Keyguard - */ - public boolean canBeHiddenByKeyguardLw(WindowState win); - - /** - * Called when the system would like to show a UI to indicate that an - * application is starting. You can use this to add a - * APPLICATION_STARTING_TYPE window with the given appToken to the window - * manager (using the normal window manager APIs) that will be shown until - * the application displays its own window. This is called without the - * window manager locked so that you can call back into it. - * - * @param appToken Token of the application being started. - * @param packageName The name of the application package being started. - * @param theme Resource defining the application's overall visual theme. - * @param nonLocalizedLabel The default title label of the application if - * no data is found in the resource. - * @param labelRes The resource ID the application would like to use as its name. - * @param icon The resource ID the application would like to use as its icon. - * @param windowFlags Window layout flags. - * @param overrideConfig override configuration to consider when generating - * context to for resources. - * @param displayId Id of the display to show the splash screen at. - * - * @return The starting surface. - * - */ - public StartingSurface addSplashScreen(IBinder appToken, String packageName, int theme, - CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon, - int logo, int windowFlags, Configuration overrideConfig, int displayId); - - /** - * Prepare for a window being added to the window manager. You can throw an - * exception here to prevent the window being added, or do whatever setup - * you need to keep track of the window. - * - * @param win The window being added. - * @param attrs The window's LayoutParams. - * - * @return {@link WindowManagerGlobal#ADD_OKAY} if the add can proceed, else an - * error code to abort the add. - */ - public int prepareAddWindowLw(WindowState win, - WindowManager.LayoutParams attrs); - - /** - * Called when a window is being removed from a window manager. Must not - * throw an exception -- clean up as much as possible. - * - * @param win The window being removed. - */ - public void removeWindowLw(WindowState win); - - /** - * Control the animation to run when a window's state changes. Return a - * non-0 number to force the animation to a specific resource ID, or 0 - * to use the default animation. - * - * @param win The window that is changing. - * @param transit What is happening to the window: {@link #TRANSIT_ENTER}, - * {@link #TRANSIT_EXIT}, {@link #TRANSIT_SHOW}, or - * {@link #TRANSIT_HIDE}. - * - * @return Resource ID of the actual animation to use, or 0 for none. - */ - public int selectAnimationLw(WindowState win, int transit); - - /** - * Determine the animation to run for a rotation transition based on the - * top fullscreen windows {@link WindowManager.LayoutParams#rotationAnimation} - * and whether it is currently fullscreen and frontmost. - * - * @param anim The exiting animation resource id is stored in anim[0], the - * entering animation resource id is stored in anim[1]. - */ - public void selectRotationAnimationLw(int anim[]); - - /** - * Validate whether the current top fullscreen has specified the same - * {@link WindowManager.LayoutParams#rotationAnimation} value as that - * being passed in from the previous top fullscreen window. - * - * @param exitAnimId exiting resource id from the previous window. - * @param enterAnimId entering resource id from the previous window. - * @param forceDefault For rotation animations only, if true ignore the - * animation values and just return false. - * @return true if the previous values are still valid, false if they - * should be replaced with the default. - */ - public boolean validateRotationAnimationLw(int exitAnimId, int enterAnimId, - boolean forceDefault); - - /** - * Create and return an animation to re-display a window that was force hidden by Keyguard. - */ - public Animation createHiddenByKeyguardExit(boolean onWallpaper, - boolean goingToNotificationShade); - - /** - * Create and return an animation to let the wallpaper disappear after being shown behind - * Keyguard. - */ - public Animation createKeyguardWallpaperExit(boolean goingToNotificationShade); - - /** - * Called from the input reader thread before a key is enqueued. - * - * <p>There are some actions that need to be handled here because they - * affect the power state of the device, for example, the power keys. - * Generally, it's best to keep as little as possible in the queue thread - * because it's the most fragile. - * @param event The key event. - * @param policyFlags The policy flags associated with the key. - * - * @return Actions flags: may be {@link #ACTION_PASS_TO_USER}. - */ - public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags); - - /** - * Called from the input reader thread before a motion is enqueued when the device is in a - * non-interactive state. - * - * <p>There are some actions that need to be handled here because they - * affect the power state of the device, for example, waking on motions. - * Generally, it's best to keep as little as possible in the queue thread - * because it's the most fragile. - * @param policyFlags The policy flags associated with the motion. - * - * @return Actions flags: may be {@link #ACTION_PASS_TO_USER}. - */ - public int interceptMotionBeforeQueueingNonInteractive(long whenNanos, int policyFlags); - - /** - * Called from the input dispatcher thread before a key is dispatched to a window. - * - * <p>Allows you to define - * behavior for keys that can not be overridden by applications. - * This method is called from the input thread, with no locks held. - * - * @param win The window that currently has focus. This is where the key - * event will normally go. - * @param event The key event. - * @param policyFlags The policy flags associated with the key. - * @return 0 if the key should be dispatched immediately, -1 if the key should - * not be dispatched ever, or a positive value indicating the number of - * milliseconds by which the key dispatch should be delayed before trying - * again. - */ - public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags); - - /** - * Called from the input dispatcher thread when an application did not handle - * a key that was dispatched to it. - * - * <p>Allows you to define default global behavior for keys that were not handled - * by applications. This method is called from the input thread, with no locks held. - * - * @param win The window that currently has focus. This is where the key - * event will normally go. - * @param event The key event. - * @param policyFlags The policy flags associated with the key. - * @return Returns an alternate key event to redispatch as a fallback, or null to give up. - * The caller is responsible for recycling the key event. - */ - public KeyEvent dispatchUnhandledKey(WindowState win, KeyEvent event, int policyFlags); - - /** - * 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 uiMode The current uiMode in configuration. - */ - public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight, - int displayRotation, int uiMode); - - /** - * Returns the bottom-most layer of the system decor, above which no policy decor should - * be applied. - */ - 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(). - * - * @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. - */ - public void layoutWindowLw(WindowState win, WindowState attached); - - - /** - * 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 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. - * @param outOutsets The areas that are not real display, but we would like to treat as such. - * @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(); - - /** Layout state may have changed (so another layout will be performed) */ - static final int FINISH_LAYOUT_REDO_LAYOUT = 0x0001; - /** Configuration state may have changed */ - static final int FINISH_LAYOUT_REDO_CONFIG = 0x0002; - /** Wallpaper may need to move */ - static final int FINISH_LAYOUT_REDO_WALLPAPER = 0x0004; - /** Need to recompute animations */ - static final int FINISH_LAYOUT_REDO_ANIM = 0x0008; - - /** - * Called following layout of all windows before each window has policy applied. - * - * @param displayWidth The current full width of the screen. - * @param displayHeight The current full height of the screen. - */ - public void beginPostLayoutPolicyLw(int displayWidth, int displayHeight); - - /** - * Called following layout of all window to apply policy to each window. - * - * @param win The window being positioned. - * @param attrs The LayoutParams of the window. - * @param attached For sub-windows, the window it is attached to. Otherwise null. - */ - public void applyPostLayoutPolicyLw(WindowState win, - WindowManager.LayoutParams attrs, WindowState attached, WindowState imeTarget); - - /** - * Called following layout of all windows and after policy has been applied - * to each window. If in this function you do - * something that may have modified the animation state of another window, - * be sure to return non-zero in order to perform another pass through layout. - * - * @return Return any bit set of {@link #FINISH_LAYOUT_REDO_LAYOUT}, - * {@link #FINISH_LAYOUT_REDO_CONFIG}, {@link #FINISH_LAYOUT_REDO_WALLPAPER}, - * or {@link #FINISH_LAYOUT_REDO_ANIM}. - */ - public int finishPostLayoutPolicyLw(); - - /** - * Return true if it is okay to perform animations for an app transition - * that is about to occur. You may return false for this if, for example, - * the lock screen is currently displayed so the switch should happen - * immediately. - */ - public boolean allowAppAnimationsLw(); - - - /** - * A new window has been focused. - */ - public int focusChangedLw(WindowState lastFocus, WindowState newFocus); - - /** - * Called when the device has started waking up. - */ - public void startedWakingUp(); - - /** - * Called when the device has finished waking up. - */ - public void finishedWakingUp(); - - /** - * Called when the device has started going to sleep. - * - * @param why {@link #OFF_BECAUSE_OF_USER}, {@link #OFF_BECAUSE_OF_ADMIN}, - * or {@link #OFF_BECAUSE_OF_TIMEOUT}. - */ - public void startedGoingToSleep(int why); - - /** - * Called when the device has finished going to sleep. - * - * @param why {@link #OFF_BECAUSE_OF_USER}, {@link #OFF_BECAUSE_OF_ADMIN}, - * or {@link #OFF_BECAUSE_OF_TIMEOUT}. - */ - public void finishedGoingToSleep(int why); - - /** - * Called when the device is about to turn on the screen to show content. - * When waking up, this method will be called once after the call to wakingUp(). - * When dozing, the method will be called sometime after the call to goingToSleep() and - * may be called repeatedly in the case where the screen is pulsing on and off. - * - * Must call back on the listener to tell it when the higher-level system - * is ready for the screen to go on (i.e. the lock screen is shown). - */ - public void screenTurningOn(ScreenOnListener screenOnListener); - - /** - * Called when the device has actually turned on the screen, i.e. the display power state has - * been set to ON and the screen is unblocked. - */ - public void screenTurnedOn(); - - /** - * Called when the display would like to be turned off. This gives policy a chance to do some - * things before the display power state is actually changed to off. - * - * @param screenOffListener Must be called to tell that the display power state can actually be - * changed now after policy has done its work. - */ - public void screenTurningOff(ScreenOffListener screenOffListener); - - /** - * Called when the device has turned the screen off. - */ - public void screenTurnedOff(); - - public interface ScreenOnListener { - void onScreenOn(); - } - - /** - * See {@link #screenTurnedOff} - */ - public interface ScreenOffListener { - void onScreenOff(); - } - - /** - * Return whether the default display is on and not blocked by a black surface. - */ - public boolean isScreenOn(); - - /** - * @return whether the device is currently allowed to animate. - * - * Note: this can be true even if it is not appropriate to animate for reasons that are outside - * of the policy's authority. - */ - boolean okToAnimate(); - - /** - * Tell the policy that the lid switch has changed state. - * @param whenNanos The time when the change occurred in uptime nanoseconds. - * @param lidOpen True if the lid is now open. - */ - public void notifyLidSwitchChanged(long whenNanos, boolean lidOpen); - - /** - * Tell the policy that the camera lens has been covered or uncovered. - * @param whenNanos The time when the change occurred in uptime nanoseconds. - * @param lensCovered True if the lens is covered. - */ - public void notifyCameraLensCoverSwitchChanged(long whenNanos, boolean lensCovered); - - /** - * Tell the policy if anyone is requesting that keyguard not come on. - * - * @param enabled Whether keyguard can be on or not. does not actually - * turn it on, unless it was previously disabled with this function. - * - * @see android.app.KeyguardManager.KeyguardLock#disableKeyguard() - * @see android.app.KeyguardManager.KeyguardLock#reenableKeyguard() - */ - @SuppressWarnings("javadoc") - public void enableKeyguard(boolean enabled); - - /** - * Callback used by {@link WindowManagerPolicy#exitKeyguardSecurely} - */ - interface OnKeyguardExitResult { - void onKeyguardExitResult(boolean success); - } - - /** - * Tell the policy if anyone is requesting the keyguard to exit securely - * (this would be called after the keyguard was disabled) - * @param callback Callback to send the result back. - * @see android.app.KeyguardManager#exitKeyguardSecurely(android.app.KeyguardManager.OnKeyguardExitResult) - */ - @SuppressWarnings("javadoc") - void exitKeyguardSecurely(OnKeyguardExitResult callback); - - /** - * isKeyguardLocked - * - * Return whether the keyguard is currently locked. - * - * @return true if in keyguard is locked. - */ - public boolean isKeyguardLocked(); - - /** - * isKeyguardSecure - * - * Return whether the keyguard requires a password to unlock. - * @param userId - * - * @return true if in keyguard is secure. - */ - public boolean isKeyguardSecure(int userId); - - /** - * Return whether the keyguard is currently occluded. - * - * @return true if in keyguard is occluded, false otherwise - */ - public boolean isKeyguardOccluded(); - - /** - * @return true if in keyguard is on and not occluded. - */ - public boolean isKeyguardShowingAndNotOccluded(); - - /** - * @return whether Keyguard is in trusted state and can be dismissed without credentials - */ - public boolean isKeyguardTrustedLw(); - - /** - * inKeyguardRestrictedKeyInputMode - * - * if keyguard screen is showing or in restricted key input mode (i.e. in - * keyguard password emergency screen). When in such mode, certain keys, - * such as the Home key and the right soft keys, don't work. - * - * @return true if in keyguard restricted input mode. - */ - public boolean inKeyguardRestrictedKeyInputMode(); - - /** - * Ask the policy to dismiss the keyguard, if it is currently shown. - * - * @param callback Callback to be informed about the result. - */ - public void dismissKeyguardLw(@Nullable IKeyguardDismissCallback callback); - - /** - * Ask the policy whether the Keyguard has drawn. If the Keyguard is disabled, this method - * returns true as soon as we know that Keyguard is disabled. - * - * @return true if the keyguard has drawn. - */ - public boolean isKeyguardDrawnLw(); - - public boolean isShowingDreamLw(); - - /** - * Given an orientation constant, returns the appropriate surface rotation, - * taking into account sensors, docking mode, rotation lock, and other factors. - * - * @param orientation An orientation constant, such as - * {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE}. - * @param lastRotation The most recently used rotation. - * @return The surface rotation to use. - */ - public int rotationForOrientationLw(@ActivityInfo.ScreenOrientation int orientation, - int lastRotation); - - /** - * Given an orientation constant and a rotation, returns true if the rotation - * has compatible metrics to the requested orientation. For example, if - * the application requested landscape and got seascape, then the rotation - * has compatible metrics; if the application requested portrait and got landscape, - * then the rotation has incompatible metrics; if the application did not specify - * a preference, then anything goes. - * - * @param orientation An orientation constant, such as - * {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE}. - * @param rotation The rotation to check. - * @return True if the rotation is compatible with the requested orientation. - */ - public boolean rotationHasCompatibleMetricsLw(@ActivityInfo.ScreenOrientation int orientation, - int rotation); - - /** - * Called by the window manager when the rotation changes. - * - * @param rotation The new rotation. - */ - public void setRotationLw(int rotation); - - /** - * Called when the system is mostly done booting to set whether - * the system should go into safe mode. - */ - public void setSafeMode(boolean safeMode); - - /** - * Called when the system is mostly done booting. - */ - public void systemReady(); - - /** - * Called when the system is done booting to the point where the - * user can start interacting with it. - */ - public void systemBooted(); - - /** - * Show boot time message to the user. - */ - public void showBootMessage(final CharSequence msg, final boolean always); - - /** - * Hide the UI for showing boot messages, never to be displayed again. - */ - public void hideBootMessages(); - - /** - * Called when userActivity is signalled in the power manager. - * This is safe to call from any thread, with any window manager locks held or not. - */ - public void userActivity(); - - /** - * Called when we have finished booting and can now display the home - * screen to the user. This will happen after systemReady(), and at - * this point the display is active. - */ - public void enableScreenAfterBoot(); - - public void setCurrentOrientationLw(@ActivityInfo.ScreenOrientation int newOrientation); - - /** - * Call from application to perform haptic feedback on its window. - */ - public boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always); - - /** - * Called when we have started keeping the screen on because a window - * requesting this has become visible. - */ - public void keepScreenOnStartedLw(); - - /** - * Called when we have stopped keeping the screen on because the last window - * requesting this is no longer visible. - */ - public void keepScreenOnStoppedLw(); - - /** - * Gets the current user rotation mode. - * - * @return The rotation mode. - * - * @see WindowManagerPolicy#USER_ROTATION_LOCKED - * @see WindowManagerPolicy#USER_ROTATION_FREE - */ - @UserRotationMode - public int getUserRotationMode(); - - /** - * Inform the policy that the user has chosen a preferred orientation ("rotation lock"). - * - * @param mode One of {@link WindowManagerPolicy#USER_ROTATION_LOCKED} or - * {@link WindowManagerPolicy#USER_ROTATION_FREE}. - * @param rotation One of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90}, - * {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}. - */ - public void setUserRotationMode(@UserRotationMode int mode, @Surface.Rotation int rotation); - - /** - * Called when a new system UI visibility is being reported, allowing - * the policy to adjust what is actually reported. - * @param visibility The raw visibility reported by the status bar. - * @return The new desired visibility. - */ - public int adjustSystemUiVisibilityLw(int visibility); - - /** - * Called by System UI to notify of changes to the visibility of Recents. - */ - public void setRecentsVisibilityLw(boolean visible); - - /** - * Called by System UI to notify of changes to the visibility of PIP. - */ - void setPipVisibilityLw(boolean visible); - - /** - * Specifies whether there is an on-screen navigation bar separate from the status bar. - */ - public boolean hasNavigationBar(); - - /** - * Lock the device now. - */ - public void lockNow(Bundle options); - - /** - * Set the last used input method window state. This state is used to make IME transition - * smooth. - * @hide - */ - public void setLastInputMethodWindowLw(WindowState ime, WindowState target); - - /** - * An internal callback (from InputMethodManagerService) to notify a state change regarding - * whether the back key should dismiss the software keyboard (IME) or not. - * - * @param newValue {@code true} if the software keyboard is shown and the back key is expected - * to dismiss the software keyboard. - * @hide - */ - default void setDismissImeOnBackKeyPressed(boolean newValue) { - // Default implementation does nothing. - } - - /** - * Show the recents task list app. - * @hide - */ - public void showRecentApps(boolean fromHome); - - /** - * Show the global actions dialog. - * @hide - */ - 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. - * - * @param newUserId The id of the incoming user. - */ - public void setCurrentUserLw(int newUserId); - - /** - * For a given user-switch operation, this will be called once with switching=true before the - * user-switch and once with switching=false afterwards (or if the user-switch was cancelled). - * This gives the policy a chance to alter its behavior for the duration of a user-switch. - * - * @param switching true if a user-switch is in progress - */ - void setSwitchingUser(boolean switching); - - /** - * Print the WindowManagerPolicy's state into the given stream. - * - * @param prefix Text to print at the front of each line. - * @param writer The PrintWriter to which you should dump your state. This will be - * closed for you after you return. - * @param args additional arguments to the dump request. - */ - public void dump(String prefix, PrintWriter writer, String[] args); - - /** - * Returns whether a given window type can be magnified. - * - * @param windowType The window type. - * @return True if the window can be magnified. - */ - public boolean canMagnifyWindow(int windowType); - - /** - * Returns whether a given window type is considered a top level one. - * A top level window does not have a container, i.e. attached window, - * or if it has a container it is laid out as a top-level window, not - * as a child of its container. - * - * @param windowType The window type. - * @return True if the window is a top level one. - */ - public boolean isTopLevelWindow(int windowType); - - /** - * Notifies the keyguard to start fading out. - * - * @param startTime the start time of the animation in uptime milliseconds - * @param fadeoutDuration the duration of the exit animation, in milliseconds - */ - public void startKeyguardExitAnimation(long startTime, long fadeoutDuration); - - /** - * Calculates the stable insets without running a layout. - * - * @param displayRotation the current display rotation - * @param displayWidth the current display width - * @param displayHeight the current display height - * @param outInsets the insets to return - */ - public void getStableInsetsLw(int displayRotation, int displayWidth, int displayHeight, - Rect outInsets); - - - /** - * @return true if the navigation bar is forced to stay visible - */ - public boolean isNavBarForcedShownLw(WindowState win); - - /** - * @return The side of the screen where navigation bar is positioned. - * @see #NAV_BAR_LEFT - * @see #NAV_BAR_RIGHT - * @see #NAV_BAR_BOTTOM - */ - int getNavBarPosition(); - - /** - * Calculates the insets for the areas that could never be removed in Honeycomb, i.e. system - * bar or button bar. See {@link #getNonDecorDisplayWidth}. - * - * @param displayRotation the current display rotation - * @param displayWidth the current display width - * @param displayHeight the current display height - * @param outInsets the insets to return - */ - public void getNonDecorInsetsLw(int displayRotation, int displayWidth, int displayHeight, - Rect outInsets); - - /** - * @return True if a specified {@param dockSide} is allowed on the current device, or false - * otherwise. It is guaranteed that at least one dock side for a particular orientation - * is allowed, so for example, if DOCKED_RIGHT is not allowed, DOCKED_LEFT is allowed. - */ - public boolean isDockSideAllowed(int dockSide); - - /** - * Called when the configuration has changed, and it's safe to load new values from resources. - */ - public void onConfigurationChanged(); - - public boolean shouldRotateSeamlessly(int oldRotation, int newRotation); - - /** - * Called when System UI has been started. - */ - void onSystemUiStarted(); - - /** - * Checks whether the policy is ready for dismissing the boot animation and completing the boot. - * - * @return true if ready; false otherwise. - */ - boolean canDismissBootAnimation(); -} diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java new file mode 100644 index 000000000000..21943bd6ba31 --- /dev/null +++ b/core/java/android/view/WindowManagerPolicyConstants.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2006 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.Display.DEFAULT_DISPLAY; + +import android.annotation.SystemApi; + +/** + * Constants for interfacing with WindowManagerService and WindowManagerPolicyInternal. + * @hide + */ +public interface WindowManagerPolicyConstants { + // Policy flags. These flags are also defined in frameworks/base/include/ui/Input.h. + int FLAG_WAKE = 0x00000001; + int FLAG_VIRTUAL = 0x00000002; + + int FLAG_INJECTED = 0x01000000; + int FLAG_TRUSTED = 0x02000000; + int FLAG_FILTERED = 0x04000000; + int FLAG_DISABLE_KEY_REPEAT = 0x08000000; + + int FLAG_INTERACTIVE = 0x20000000; + int FLAG_PASS_TO_USER = 0x40000000; + + // Flags for IActivityManager.keyguardGoingAway() + int KEYGUARD_GOING_AWAY_FLAG_TO_SHADE = 1 << 0; + int KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS = 1 << 1; + int KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER = 1 << 2; + + // Flags used for indicating whether the internal and/or external input devices + // of some type are available. + int PRESENCE_INTERNAL = 1 << 0; + int PRESENCE_EXTERNAL = 1 << 1; + + /** + * Sticky broadcast of the current HDMI plugged state. + */ + String ACTION_HDMI_PLUGGED = "android.intent.action.HDMI_PLUGGED"; + + /** + * Extra in {@link #ACTION_HDMI_PLUGGED} indicating the state: true if + * plugged in to HDMI, false if not. + */ + String EXTRA_HDMI_PLUGGED_STATE = "state"; + + /** + * Set to {@code true} when intent was invoked from pressing the home key. + * @hide + */ + @SystemApi + String EXTRA_FROM_HOME_KEY = "android.intent.extra.FROM_HOME_KEY"; + + // TODO: move this to a more appropriate place. + interface PointerEventListener { + /** + * 1. onPointerEvent will be called on the service.UiThread. + * 2. motionEvent will be recycled after onPointerEvent returns so if it is needed later a + * copy() must be made and the copy must be recycled. + **/ + void onPointerEvent(MotionEvent motionEvent); + + /** + * @see #onPointerEvent(MotionEvent) + **/ + default void onPointerEvent(MotionEvent motionEvent, int displayId) { + if (displayId == DEFAULT_DISPLAY) { + onPointerEvent(motionEvent); + } + } + } + + /** Screen turned off because of a device admin */ + int OFF_BECAUSE_OF_ADMIN = 1; + /** Screen turned off because of power button */ + int OFF_BECAUSE_OF_USER = 2; + /** Screen turned off because of timeout */ + int OFF_BECAUSE_OF_TIMEOUT = 3; + + int APPLICATION_LAYER = 2; + int APPLICATION_MEDIA_SUBLAYER = -2; + int APPLICATION_MEDIA_OVERLAY_SUBLAYER = -1; + int APPLICATION_PANEL_SUBLAYER = 1; + int APPLICATION_SUB_PANEL_SUBLAYER = 2; + int APPLICATION_ABOVE_SUB_PANEL_SUBLAYER = 3; + + /** + * Convert the off reason to a human readable format. + */ + static String offReasonToString(int why) { + switch (why) { + case OFF_BECAUSE_OF_ADMIN: + return "OFF_BECAUSE_OF_ADMIN"; + case OFF_BECAUSE_OF_USER: + return "OFF_BECAUSE_OF_USER"; + case OFF_BECAUSE_OF_TIMEOUT: + return "OFF_BECAUSE_OF_TIMEOUT"; + default: + return Integer.toString(why); + } + } +} diff --git a/core/java/android/view/accessibility/AccessibilityCache.java b/core/java/android/view/accessibility/AccessibilityCache.java index d7851171cd67..da5a1cd67922 100644 --- a/core/java/android/view/accessibility/AccessibilityCache.java +++ b/core/java/android/view/accessibility/AccessibilityCache.java @@ -23,8 +23,6 @@ import android.util.LongArray; import android.util.LongSparseArray; import android.util.SparseArray; -import com.android.internal.annotations.VisibleForTesting; - import java.util.ArrayList; import java.util.List; @@ -33,8 +31,7 @@ import java.util.List; * It is updated when windows change or nodes change. * @hide */ -@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) -public final class AccessibilityCache { +public class AccessibilityCache { private static final String LOG_TAG = "AccessibilityCache"; @@ -329,6 +326,8 @@ public final class AccessibilityCache { final long oldParentId = oldInfo.getParentNodeId(); if (info.getParentNodeId() != oldParentId) { clearSubTreeLocked(windowId, oldParentId); + } else { + oldInfo.recycle(); } } diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index eaa4b4b3ba08..1d19a9f5969a 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -16,6 +16,7 @@ package android.view.accessibility; +import android.annotation.IntDef; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -23,6 +24,8 @@ import android.util.Pools.SynchronizedPool; import com.android.internal.util.BitUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -709,6 +712,38 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par */ public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004; + + /** @hide */ + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_VIEW_CLICKED, + TYPE_VIEW_LONG_CLICKED, + TYPE_VIEW_SELECTED, + TYPE_VIEW_FOCUSED, + TYPE_VIEW_TEXT_CHANGED, + TYPE_WINDOW_STATE_CHANGED, + TYPE_NOTIFICATION_STATE_CHANGED, + TYPE_VIEW_HOVER_ENTER, + TYPE_VIEW_HOVER_EXIT, + TYPE_TOUCH_EXPLORATION_GESTURE_START, + TYPE_TOUCH_EXPLORATION_GESTURE_END, + TYPE_WINDOW_CONTENT_CHANGED, + TYPE_VIEW_SCROLLED, + TYPE_VIEW_TEXT_SELECTION_CHANGED, + TYPE_ANNOUNCEMENT, + TYPE_VIEW_ACCESSIBILITY_FOCUSED, + TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, + TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, + TYPE_GESTURE_DETECTION_START, + TYPE_GESTURE_DETECTION_END, + TYPE_TOUCH_INTERACTION_START, + TYPE_TOUCH_INTERACTION_END, + TYPE_WINDOWS_CHANGED, + TYPE_VIEW_CONTEXT_CLICKED, + TYPE_ASSIST_READING_CONTEXT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EventType {} + /** * Mask for {@link AccessibilityEvent} all types. * @@ -741,7 +776,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par private static final SynchronizedPool<AccessibilityEvent> sPool = new SynchronizedPool<AccessibilityEvent>(MAX_POOL_SIZE); - private int mEventType; + private @EventType int mEventType; private CharSequence mPackageName; private long mEventTime; int mMovementGranularity; @@ -833,7 +868,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * * @return The event type. */ - public int getEventType() { + public @EventType int getEventType() { return mEventType; } @@ -890,7 +925,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * * @throws IllegalStateException If called from an AccessibilityService. */ - public void setEventType(int eventType) { + public void setEventType(@EventType int eventType) { enforceNotSealed(); mEventType = eventType; } @@ -1118,6 +1153,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par record.mToIndex = parcel.readInt(); record.mScrollX = parcel.readInt(); record.mScrollY = parcel.readInt(); + record.mScrollDeltaX = parcel.readInt(); + record.mScrollDeltaY = parcel.readInt(); record.mMaxScrollX = parcel.readInt(); record.mMaxScrollY = parcel.readInt(); record.mAddedCount = parcel.readInt(); @@ -1170,6 +1207,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par parcel.writeInt(record.mToIndex); parcel.writeInt(record.mScrollX); parcel.writeInt(record.mScrollY); + parcel.writeInt(record.mScrollDeltaX); + parcel.writeInt(record.mScrollDeltaY); parcel.writeInt(record.mMaxScrollX); parcel.writeInt(record.mMaxScrollY); parcel.writeInt(record.mAddedCount); diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 19213ca06c5e..72af203e5fab 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -28,6 +28,9 @@ import android.util.Log; import android.util.LongSparseArray; import android.util.SparseArray; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; + import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -86,6 +89,12 @@ public final class AccessibilityInteractionClient private static final LongSparseArray<AccessibilityInteractionClient> sClients = new LongSparseArray<>(); + private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = + new SparseArray<>(); + + private static AccessibilityCache sAccessibilityCache = + new AccessibilityCache(new AccessibilityCache.AccessibilityNodeRefresher()); + private final AtomicInteger mInteractionIdCounter = new AtomicInteger(); private final Object mInstanceLock = new Object(); @@ -100,12 +109,6 @@ public final class AccessibilityInteractionClient private Message mSameThreadMessage; - private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = - new SparseArray<>(); - - private static final AccessibilityCache sAccessibilityCache = - new AccessibilityCache(new AccessibilityCache.AccessibilityNodeRefresher()); - /** * @return The client for the current thread. */ @@ -133,6 +136,50 @@ public final class AccessibilityInteractionClient } } + /** + * Gets a cached accessibility service connection. + * + * @param connectionId The connection id. + * @return The cached connection if such. + */ + public static IAccessibilityServiceConnection getConnection(int connectionId) { + synchronized (sConnectionCache) { + return sConnectionCache.get(connectionId); + } + } + + /** + * Adds a cached accessibility service connection. + * + * @param connectionId The connection id. + * @param connection The connection. + */ + public static void addConnection(int connectionId, IAccessibilityServiceConnection connection) { + synchronized (sConnectionCache) { + sConnectionCache.put(connectionId, connection); + } + } + + /** + * Removes a cached accessibility service connection. + * + * @param connectionId The connection id. + */ + public static void removeConnection(int connectionId) { + synchronized (sConnectionCache) { + sConnectionCache.remove(connectionId); + } + } + + /** + * This method is only for testing. Replacing the cache is a generally terrible idea, but + * tests need to be able to verify this class's interactions with the cache + */ + @VisibleForTesting + public static void setCache(AccessibilityCache cache) { + sAccessibilityCache = cache; + } + private AccessibilityInteractionClient() { /* reducing constructor visibility */ } @@ -167,7 +214,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @return The {@link AccessibilityWindowInfo}. */ @@ -187,8 +234,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 +275,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; @@ -247,7 +300,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -283,14 +336,19 @@ 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); - if (success) { + final String[] packageNames; + try { + packageNames = connection.findAccessibilityNodeInfoByAccessibilityId( + accessibilityWindowId, accessibilityNodeId, interactionId, this, + prefetchFlags, Thread.currentThread().getId(), arguments); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (packageNames != null) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, + bypassCache, packageNames); if (infos != null && !infos.isEmpty()) { for (int i = 1; i < infos.size(); i++) { infos.get(i).recycle(); @@ -317,7 +375,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -333,15 +391,21 @@ 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); - if (success) { + final String[] packageNames; + try { + packageNames = connection.findAccessibilityNodeInfosByViewId( + accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this, + Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + + if (packageNames != null) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); if (infos != null) { - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, + false, packageNames); return infos; } } @@ -365,7 +429,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -381,15 +445,21 @@ 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); - if (success) { + final String[] packageNames; + try { + packageNames = connection.findAccessibilityNodeInfosByText( + accessibilityWindowId, accessibilityNodeId, text, interactionId, this, + Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + + if (packageNames != null) { List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear( interactionId); if (infos != null) { - finalizeAndCacheAccessibilityNodeInfos(infos, connectionId); + finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, + false, packageNames); return infos; } } @@ -412,7 +482,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -428,14 +498,19 @@ 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); - if (success) { + final String[] packageNames; + try { + packageNames = connection.findFocus(accessibilityWindowId, + accessibilityNodeId, focusType, interactionId, this, + Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + + if (packageNames != null) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames); return info; } } else { @@ -456,7 +531,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -472,14 +547,19 @@ 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); - if (success) { + final String[] packageNames; + try { + packageNames = connection.focusSearch(accessibilityWindowId, + accessibilityNodeId, direction, interactionId, this, + Thread.currentThread().getId()); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + + if (packageNames != null) { AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear( interactionId); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames); return info; } } else { @@ -498,7 +578,7 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID} * to query the currently active window. * @param accessibilityNodeId A unique view id or virtual descendant id from * where to start the search. Use @@ -515,10 +595,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); } @@ -580,7 +665,7 @@ public final class AccessibilityInteractionClient int interactionId) { synchronized (mInstanceLock) { final boolean success = waitForResultTimedLocked(interactionId); - List<AccessibilityNodeInfo> result = null; + final List<AccessibilityNodeInfo> result; if (success) { result = mFindAccessibilityNodeInfosResult; } else { @@ -696,13 +781,28 @@ public final class AccessibilityInteractionClient * * @param info The info. * @param connectionId The id of the connection to the system. + * @param bypassCache Whether or not to bypass the cache. The node is added to the cache if + * this value is {@code false} + * @param packageNames The valid package names a node can come from. */ private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info, - int connectionId) { + int connectionId, boolean bypassCache, String[] packageNames) { if (info != null) { info.setConnectionId(connectionId); + // Empty array means any package name is Okay + if (!ArrayUtils.isEmpty(packageNames)) { + CharSequence packageName = info.getPackageName(); + if (packageName == null + || !ArrayUtils.contains(packageNames, packageName.toString())) { + // If the node package not one of the valid ones, pick the top one - this + // is one of the packages running in the introspected UID. + info.setPackageName(packageNames[0]); + } + } info.setSealed(true); - sAccessibilityCache.add(info); + if (!bypassCache) { + sAccessibilityCache.add(info); + } } } @@ -711,14 +811,18 @@ public final class AccessibilityInteractionClient * * @param infos The {@link AccessibilityNodeInfo}s. * @param connectionId The id of the connection to the system. + * @param bypassCache Whether or not to bypass the cache. The nodes are added to the cache if + * this value is {@code false} + * @param packageNames The valid package names a node can come from. */ private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos, - int connectionId) { + int connectionId, boolean bypassCache, String[] packageNames) { if (infos != null) { final int infosCount = infos.size(); for (int i = 0; i < infosCount; i++) { AccessibilityNodeInfo info = infos.get(i); - finalizeAndCacheAccessibilityNodeInfo(info, connectionId); + finalizeAndCacheAccessibilityNodeInfo(info, connectionId, + bypassCache, packageNames); } } } @@ -738,41 +842,6 @@ public final class AccessibilityInteractionClient } /** - * Gets a cached accessibility service connection. - * - * @param connectionId The connection id. - * @return The cached connection if such. - */ - public IAccessibilityServiceConnection getConnection(int connectionId) { - synchronized (sConnectionCache) { - return sConnectionCache.get(connectionId); - } - } - - /** - * Adds a cached accessibility service connection. - * - * @param connectionId The connection id. - * @param connection The connection. - */ - public void addConnection(int connectionId, IAccessibilityServiceConnection connection) { - synchronized (sConnectionCache) { - sConnectionCache.put(connectionId, connection); - } - } - - /** - * Removes a cached accessibility service connection. - * - * @param connectionId The connection id. - */ - public void removeConnection(int connectionId) { - synchronized (sConnectionCache) { - sConnectionCache.remove(connectionId); - } - } - - /** * Checks whether the infos are a fully connected tree with no duplicates. * * @param infos The result list to check. diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 0b9bc5760fa8..b4499d1acac3 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/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"); } @@ -885,7 +888,7 @@ public final class AccessibilityManager { * @hide */ public int addAccessibilityInteractionConnection(IWindow windowToken, - IAccessibilityInteractionConnection connection) { + String packageName, IAccessibilityInteractionConnection connection) { final IAccessibilityManager service; final int userId; synchronized (mLock) { @@ -896,7 +899,8 @@ public final class AccessibilityManager { userId = mUserId; } try { - return service.addAccessibilityInteractionConnection(windowToken, connection, userId); + return service.addAccessibilityInteractionConnection(windowToken, connection, + packageName, userId); } catch (RemoteException re) { Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 53efe1833db1..9c2f6bb8cc33 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -635,6 +635,8 @@ public class AccessibilityNodeInfo implements Parcelable { private static final int BOOLEAN_PROPERTY_IMPORTANCE = 0x0040000; + private static final int BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE = 0x0080000; + private static final int BOOLEAN_PROPERTY_IS_SHOWING_HINT = 0x0100000; /** @@ -870,6 +872,11 @@ public class AccessibilityNodeInfo implements Parcelable { if (refreshedInfo == null) { return false; } + // Hard-to-reproduce bugs seem to be due to some tools recycling a node on another + // thread. If that happens, the init will re-seal the node, which then is in a bad state + // when it is obtained. Enforce sealing again before we init to fail when a node has been + // recycled during a refresh to catch such errors earlier. + enforceSealed(); init(refreshedInfo); refreshedInfo.recycle(); return true; @@ -2316,6 +2323,37 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Returns whether the node is explicitly marked as a focusable unit by a screen reader. Note + * that {@code false} indicates that it is not explicitly marked, not that the node is not + * a focusable unit. Screen readers should generally used other signals, such as + * {@link #isFocusable()}, or the presence of text in a node, to determine what should receive + * focus. + * + * @return {@code true} if the node is specifically marked as a focusable unit for screen + * readers, {@code false} otherwise. + * + * @see View#isScreenReaderFocusable() + */ + public boolean isScreenReaderFocusable() { + return getBooleanProperty(BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE); + } + + /** + * Sets whether the node should be considered a focusable unit by a screen reader. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param screenReaderFocusable {@code true} if the node is a focusable unit for screen readers, + * {@code false} otherwise. + */ + public void setScreenReaderFocusable(boolean screenReaderFocusable) { + setBooleanProperty(BOOLEAN_PROPERTY_SCREEN_READER_FOCUSABLE, screenReaderFocusable); + } + + /** * Returns whether the node's text represents a hint for the user to enter text. It should only * be {@code true} if the node has editable text. * @@ -3657,8 +3695,9 @@ public class AccessibilityNodeInfo implements Parcelable { if (DEBUG) { builder.append("; sourceNodeId: " + mSourceNodeId); - builder.append("; accessibilityViewId: " + getAccessibilityViewId(mSourceNodeId)); - builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId)); + builder.append("; windowId: " + mWindowId); + builder.append("; accessibilityViewId: ").append(getAccessibilityViewId(mSourceNodeId)); + builder.append("; virtualDescendantId: ").append(getVirtualDescendantId(mSourceNodeId)); builder.append("; mParentNodeId: " + mParentNodeId); builder.append("; traversalBefore: ").append(mTraversalBefore); builder.append("; traversalAfter: ").append(mTraversalAfter); @@ -3688,8 +3727,8 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append("]"); } - builder.append("; boundsInParent: " + mBoundsInParent); - builder.append("; boundsInScreen: " + mBoundsInScreen); + builder.append("; boundsInParent: ").append(mBoundsInParent); + builder.append("; boundsInScreen: ").append(mBoundsInScreen); builder.append("; packageName: ").append(mPackageName); builder.append("; className: ").append(mClassName); diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index 02b618503250..fa505c97ba48 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -86,6 +86,9 @@ public class AccessibilityRecord { int mToIndex = UNDEFINED; int mScrollX = UNDEFINED; int mScrollY = UNDEFINED; + + int mScrollDeltaX = UNDEFINED; + int mScrollDeltaY = UNDEFINED; int mMaxScrollX = UNDEFINED; int mMaxScrollY = UNDEFINED; @@ -465,6 +468,48 @@ public class AccessibilityRecord { } /** + * Gets the difference in pixels between the horizontal position before the scroll and the + * current horizontal position + * + * @return the scroll delta x + */ + public int getScrollDeltaX() { + return mScrollDeltaX; + } + + /** + * Sets the difference in pixels between the horizontal position before the scroll and the + * current horizontal position + * + * @param scrollDeltaX the scroll delta x + */ + public void setScrollDeltaX(int scrollDeltaX) { + enforceNotSealed(); + mScrollDeltaX = scrollDeltaX; + } + + /** + * Gets the difference in pixels between the vertical position before the scroll and the + * current vertical position + * + * @return the scroll delta y + */ + public int getScrollDeltaY() { + return mScrollDeltaY; + } + + /** + * Sets the difference in pixels between the vertical position before the scroll and the + * current vertical position + * + * @param scrollDeltaY the scroll delta y + */ + public void setScrollDeltaY(int scrollDeltaY) { + enforceNotSealed(); + mScrollDeltaY = scrollDeltaY; + } + + /** * Gets the max scroll offset of the source left edge in pixels. * * @return The max scroll. diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 3f499abd2e4d..c93e2c15407b 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -45,7 +45,8 @@ interface IAccessibilityManager { List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType, int userId); int addAccessibilityInteractionConnection(IWindow windowToken, - in IAccessibilityInteractionConnection connection, int userId); + in IAccessibilityInteractionConnection connection, + String packageName, int userId); void removeAccessibilityInteractionConnection(IWindow windowToken); diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index d33e1484c7e8..419aeb3507a6 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -24,7 +24,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; -import android.app.Activity; +import android.annotation.TestApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -37,6 +37,7 @@ import android.os.Parcelable; import android.os.RemoteException; import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; +import android.service.autofill.UserData; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -97,10 +98,10 @@ import sun.misc.Cleaner; * </ul> * * <p>When the service returns datasets, the Android System displays an autofill dataset picker - * UI affordance associated with the view, when the view is focused on and is part of a dataset. - * The application can be notified when the affordance is shown by registering an + * UI associated with the view, when the view is focused on and is part of a dataset. + * The application can be notified when the UI is shown by registering an * {@link AutofillCallback} through {@link #registerCallback(AutofillCallback)}. When the user - * selects a dataset from the affordance, all views present in the dataset are autofilled, through + * selects a dataset from the UI, all views present in the dataset are autofilled, through * calls to {@link View#autofill(AutofillValue)} or {@link View#autofill(SparseArray)}. * * <p>When the service returns ids of savable views, the Android System keeps track of changes @@ -114,7 +115,7 @@ import sun.misc.Cleaner; * </ul> * * <p>Finally, after the autofill context is commited (i.e., not cancelled), the Android System - * shows a save UI affordance if the value of savable views have changed. If the user selects the + * shows an autofill save UI if the value of savable views have changed. If the user selects the * option to Save, the current value of the views is then sent to the autofill service. * * <p>It is safe to call into its methods from any thread. @@ -156,6 +157,12 @@ public final class AutofillManager { * service authentication will contain the Bundle set by * {@link android.service.autofill.FillResponse.Builder#setClientState(Bundle)} on this extra. * + * <p>On Android {@link android.os.Build.VERSION_CODES#P} and higher, the autofill service + * can also add this bundle to the {@link Intent} set as the + * {@link android.app.Activity#setResult(int, Intent) result} for an authentication request, + * so the bundle can be recovered later on + * {@link android.service.autofill.SaveRequest#getClientState()}. + * * <p> * Type: {@link android.os.Bundle} */ @@ -224,7 +231,7 @@ public final class AutofillManager { /** * State where the autofill context was finished by the server because the autofill - * service could not autofill the page. + * service could not autofill the activity. * * <p>In this state, most apps callback (such as {@link #notifyViewEntered(View)}) are ignored, * exception {@link #requestAutofill(View)} (and {@link #requestAutofill(View, int, Rect)}). @@ -242,6 +249,16 @@ public final class AutofillManager { public static final int STATE_SHOWING_SAVE_UI = 3; /** + * State where the autofill is disabled because the service cannot autofill the activity at all. + * + * <p>In this state, every call is ignored, even {@link #requestAutofill(View)} + * (and {@link #requestAutofill(View, int, Rect)}). + * + * @hide + */ + public static final int STATE_DISABLED_BY_SERVICE = 4; + + /** * Makes an authentication id from a request id and a dataset id. * * @param requestId The request id. @@ -320,6 +337,14 @@ public final class AutofillManager { @GuardedBy("mLock") @Nullable private ArraySet<AutofillId> mFillableIds; + /** If set, session is commited when the field is clicked. */ + @GuardedBy("mLock") + @Nullable private AutofillId mSaveTriggerId; + + /** If set, session is commited when the activity is finished; otherwise session is canceled. */ + @GuardedBy("mLock") + private boolean mSaveOnFinish; + /** @hide */ public interface AutofillClient { /** @@ -393,6 +418,11 @@ public final class AutofillManager { * Runs the specified action on the UI thread. */ void runOnUiThread(Runnable action); + + /** + * Gets the complete component name of this client. + */ + ComponentName getComponentName(); } /** @@ -428,7 +458,7 @@ public final class AutofillManager { if (mSessionId != NO_SESSION) { ensureServiceClientAddedIfNeededLocked(); - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client != null) { try { final boolean sessionWasRestored = mService.restoreSession(mSessionId, @@ -501,7 +531,7 @@ public final class AutofillManager { * @return whether autofill is enabled for the current user. */ public boolean isEnabled() { - if (!hasAutofillFeature()) { + if (!hasAutofillFeature() || isDisabledByService()) { return false; } synchronized (mLock) { @@ -575,19 +605,31 @@ public final class AutofillManager { notifyViewEntered(view, 0); } + private boolean shouldIgnoreViewEnteredLocked(@NonNull View view, int flags) { + if (isDisabledByService()) { + if (sVerbose) { + Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + view + + ") on state " + getStateAsStringLocked()); + } + return true; + } + if (mState == STATE_FINISHED && (flags & FLAG_MANUAL_REQUEST) == 0) { + if (sVerbose) { + Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + view + + ") on state " + getStateAsStringLocked()); + } + return true; + } + return false; + } + private void notifyViewEntered(@NonNull View view, int flags) { if (!hasAutofillFeature()) { return; } AutofillCallback callback = null; synchronized (mLock) { - if (isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { - if (sVerbose) { - Log.v(TAG, "notifyViewEntered(flags=" + flags + ", view=" + view - + "): ignored on state " + getStateAsStringLocked()); - } - return; - } + if (shouldIgnoreViewEnteredLocked(view, flags)) return; ensureServiceClientAddedIfNeededLocked(); @@ -712,14 +754,8 @@ public final class AutofillManager { } AutofillCallback callback = null; synchronized (mLock) { - if (isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { - if (sVerbose) { - Log.v(TAG, "notifyViewEntered(flags=" + flags + ", view=" + view - + ", virtualId=" + virtualId - + "): ignored on state " + getStateAsStringLocked()); - } - return; - } + if (shouldIgnoreViewEnteredLocked(view, flags)) return; + ensureServiceClientAddedIfNeededLocked(); if (!mEnabled) { @@ -843,6 +879,47 @@ public final class AutofillManager { } } + + /** + * Called when a {@link View} is clicked. Currently only used by views that should trigger save. + * + * @hide + */ + public void notifyViewClicked(View view) { + final AutofillId id = view.getAutofillId(); + + if (sVerbose) Log.v(TAG, "notifyViewClicked(): id=" + id + ", trigger=" + mSaveTriggerId); + + synchronized (mLock) { + if (mSaveTriggerId != null && mSaveTriggerId.equals(id)) { + if (sDebug) Log.d(TAG, "triggering commit by click of " + id); + commitLocked(); + mMetricsLogger.action(MetricsEvent.AUTOFILL_SAVE_EXPLICITLY_TRIGGERED, + mContext.getPackageName()); + } + } + } + + /** + * Called by {@link android.app.Activity} to commit or cancel the session on finish. + * + * @hide + */ + public void onActivityFinished() { + if (!hasAutofillFeature()) { + return; + } + synchronized (mLock) { + if (mSaveOnFinish) { + if (sDebug) Log.d(TAG, "Committing session on finish() as requested by service"); + commitLocked(); + } else { + if (sDebug) Log.d(TAG, "Cancelling session on finish() as requested by service"); + cancelLocked(); + } + } + } + /** * Called to indicate the current autofill context should be commited. * @@ -859,12 +936,15 @@ public final class AutofillManager { return; } synchronized (mLock) { - if (!mEnabled && !isActiveLocked()) { - return; - } + commitLocked(); + } + } - finishSessionLocked(); + private void commitLocked() { + if (!mEnabled && !isActiveLocked()) { + return; } + finishSessionLocked(); } /** @@ -879,16 +959,20 @@ public final class AutofillManager { * methods such as {@link android.app.Activity#finish()}. */ public void cancel() { + if (sVerbose) Log.v(TAG, "cancel() called by app"); if (!hasAutofillFeature()) { return; } synchronized (mLock) { - if (!mEnabled && !isActiveLocked()) { - return; - } + cancelLocked(); + } + } - cancelSessionLocked(); + private void cancelLocked() { + if (!mEnabled && !isActiveLocked()) { + return; } + cancelSessionLocked(); } /** @hide */ @@ -926,6 +1010,71 @@ public final class AutofillManager { } /** + * Gets the user data used for <a href="#FieldsClassification">fields classification</a>. + * + * <p><b>Note:</b> This method should only be called by an app providing an autofill service. + * + * TODO(b/67867469): + * - proper javadoc + * - unhide / remove testApi + * + * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was + * reset or if the caller currently does not have an enabled autofill service for the user. + * + * @hide + */ + @TestApi + @Nullable public UserData getUserData() { + try { + return mService.getUserData(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return null; + } + } + + /** + * Sets the user data used for <a href="#FieldsClassification">fields classification</a>. + * + * <p><b>Note:</b> This method should only be called by an app providing an autofill service, + * and it's ignored if the caller currently doesn't have an enabled autofill service for + * the user. + * + * TODO(b/67867469): + * - proper javadoc + * - unhide / remove testApi + * - add unit tests: + * - call set / get / verify + * + * @hide + */ + @TestApi + public void setUserData(@Nullable UserData userData) { + try { + mService.setUserData(userData); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * TODO(b/67867469): + * - proper javadoc + * - mention this method in other places + * - unhide / remove testApi + * @hide + */ + @TestApi + public boolean isFieldClassificationEnabled() { + try { + return mService.isFieldClassificationEnabled(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return false; + } + } + + /** * Returns {@code true} if autofill is supported by the current device and * is supported for this user. * @@ -945,15 +1094,14 @@ public final class AutofillManager { } } - private AutofillClient getClientLocked() { - return mContext.getAutofillClient(); - } - - private ComponentName getComponentNameFromContext() { - if (mContext instanceof Activity) { - return ((Activity) mContext).getComponentName(); + // Note: don't need to use locked suffix because mContext is final. + private AutofillClient getClient() { + final AutofillClient client = mContext.getAutofillClient(); + if (client == null && sDebug) { + Log.d(TAG, "No AutofillClient for " + mContext.getPackageName() + " on context " + + mContext); } - return null; + return client; } /** @hide */ @@ -975,6 +1123,10 @@ public final class AutofillManager { final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT); final Bundle responseData = new Bundle(); responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result); + final Bundle newClientState = data.getBundleExtra(EXTRA_CLIENT_STATE); + if (newClientState != null) { + responseData.putBundle(EXTRA_CLIENT_STATE, newClientState); + } try { mService.setAuthenticationResult(responseData, mSessionId, authenticationId, mContext.getUserId()); @@ -1006,21 +1158,16 @@ public final class AutofillManager { return; } try { - final ComponentName componentName = getComponentNameFromContext(); - if (componentName == null) { - Log.w(TAG, "startSessionLocked(): context is not activity: " + mContext); - return; - } + final AutofillClient client = getClient(); + if (client == null) return; // NOTE: getClient() already logd it.. + mSessionId = mService.startSession(mContext.getActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), - mCallback != null, flags, componentName); + mCallback != null, flags, client.getComponentName()); if (mSessionId != NO_SESSION) { mState = STATE_ACTIVE; } - final AutofillClient client = getClientLocked(); - if (client != null) { - client.autofillCallbackResetableStateAvailable(); - } + client.autofillCallbackResetableStateAvailable(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1059,6 +1206,7 @@ public final class AutofillManager { mState = STATE_UNKNOWN; mTrackedViews = null; mFillableIds = null; + mSaveTriggerId = null; } private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action, @@ -1071,22 +1219,17 @@ public final class AutofillManager { try { if (restartIfNecessary) { - final ComponentName componentName = getComponentNameFromContext(); - if (componentName == null) { - Log.w(TAG, "startSessionLocked(): context is not activity: " + mContext); - return; - } + final AutofillClient client = getClient(); + if (client == null) return; // NOTE: getClient() already logd it.. + final int newId = mService.updateOrRestartSession(mContext.getActivityToken(), mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(), - mCallback != null, flags, componentName, mSessionId, action); + mCallback != null, flags, client.getComponentName(), mSessionId, action); if (newId != mSessionId) { if (sDebug) Log.d(TAG, "Session restarted: " + mSessionId + "=>" + newId); mSessionId = newId; mState = (mSessionId == NO_SESSION) ? STATE_UNKNOWN : STATE_ACTIVE; - final AutofillClient client = getClientLocked(); - if (client != null) { - client.autofillCallbackResetableStateAvailable(); - } + client.autofillCallbackResetableStateAvailable(); } } else { mService.updateSession(mSessionId, id, bounds, value, action, flags, @@ -1099,7 +1242,7 @@ public final class AutofillManager { } private void ensureServiceClientAddedIfNeededLocked() { - if (getClientLocked() == null) { + if (getClient() == null) { return; } @@ -1182,7 +1325,7 @@ public final class AutofillManager { AutofillCallback callback = null; synchronized (mLock) { if (mSessionId == sessionId) { - AutofillClient client = getClientLocked(); + AutofillClient client = getClient(); if (client != null) { if (client.autofillCallbackRequestShowFillUi(anchor, width, height, @@ -1207,7 +1350,7 @@ public final class AutofillManager { Intent fillInIntent) { synchronized (mLock) { if (sessionId == mSessionId) { - AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client != null) { client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent); } @@ -1215,14 +1358,26 @@ 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) { @@ -1231,6 +1386,8 @@ public final class AutofillManager { } } } + sDebug = (flags & SET_STATE_FLAG_DEBUG) != 0; + sVerbose = (flags & SET_STATE_FLAG_VERBOSE) != 0; } /** @@ -1258,7 +1415,7 @@ public final class AutofillManager { return; } - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client == null) { return; } @@ -1328,12 +1485,15 @@ public final class AutofillManager { /** * Set the tracked views. * - * @param trackedIds The views to be tracked + * @param trackedIds The views to be tracked. * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible. + * @param saveOnFinish Finish the session once the activity is finished. * @param fillableIds Views that might anchor FillUI. + * @param saveTriggerId View that when clicked triggers commit(). */ private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds, - boolean saveOnAllViewsInvisible, @Nullable AutofillId[] fillableIds) { + boolean saveOnAllViewsInvisible, boolean saveOnFinish, + @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) { synchronized (mLock) { if (mEnabled && mSessionId == sessionId) { if (saveOnAllViewsInvisible) { @@ -1341,6 +1501,7 @@ public final class AutofillManager { } else { mTrackedViews = null; } + mSaveOnFinish = saveOnFinish; if (fillableIds != null) { if (mFillableIds == null) { mFillableIds = new ArraySet<>(fillableIds.length); @@ -1353,10 +1514,30 @@ public final class AutofillManager { + ", mFillableIds" + mFillableIds); } } + + if (mSaveTriggerId != null && !mSaveTriggerId.equals(saveTriggerId)) { + // Turn off trigger on previous view id. + setNotifyOnClickLocked(mSaveTriggerId, false); + } + + if (saveTriggerId != null && !saveTriggerId.equals(mSaveTriggerId)) { + // Turn on trigger on new view id. + mSaveTriggerId = saveTriggerId; + setNotifyOnClickLocked(mSaveTriggerId, true); + } } } } + private void setNotifyOnClickLocked(@NonNull AutofillId id, boolean notify) { + final View view = findView(id); + if (view == null) { + Log.w(TAG, "setNotifyOnClick(): invalid id: " + id); + return; + } + view.setNotifyAutofillManagerOnClick(notify); + } + private void setSaveUiState(int sessionId, boolean shown) { if (sDebug) Log.d(TAG, "setSaveUiState(" + sessionId + "): " + shown); synchronized (mLock) { @@ -1382,7 +1563,9 @@ public final class AutofillManager { * Marks the state of the session as finished. * * @param newState {@link #STATE_FINISHED} (because the autofill service returned a {@code null} - * FillResponse) or {@link #STATE_UNKNOWN} (because the session was removed). + * FillResponse), {@link #STATE_UNKNOWN} (because the session was removed), or + * {@link #STATE_DISABLED_BY_SERVICE} (because the autofill service disabled further autofill + * requests for the activity). */ private void setSessionFinished(int newState) { synchronized (mLock) { @@ -1409,7 +1592,7 @@ public final class AutofillManager { // 1. If local and remote session id are off sync the UI would be stuck shown // 2. There is a race between the user state being destroyed due the fill // service being uninstalled and the UI being dismissed. - AutofillClient client = getClientLocked(); + AutofillClient client = getClient(); if (client != null) { if (client.autofillCallbackRequestHideFillUi() && mCallback != null) { callback = mCallback; @@ -1427,10 +1610,10 @@ public final class AutofillManager { } } - private void notifyNoFillUi(int sessionId, AutofillId id, boolean sessionFinished) { + private void notifyNoFillUi(int sessionId, AutofillId id, int sessionFinishedState) { if (sVerbose) { Log.v(TAG, "notifyNoFillUi(): sessionId=" + sessionId + ", autofillId=" + id - + ", finished=" + sessionFinished); + + ", sessionFinishedState=" + sessionFinishedState); } final View anchor = findView(id); if (anchor == null) { @@ -1439,7 +1622,7 @@ public final class AutofillManager { AutofillCallback callback = null; synchronized (mLock) { - if (mSessionId == sessionId && getClientLocked() != null) { + if (mSessionId == sessionId && getClient() != null) { callback = mCallback; } } @@ -1453,9 +1636,9 @@ public final class AutofillManager { } } - if (sessionFinished) { + if (sessionFinishedState != 0) { // Callback call was "hijacked" to also update the session state. - setSessionFinished(STATE_FINISHED); + setSessionFinished(sessionFinishedState); } } @@ -1496,7 +1679,7 @@ public final class AutofillManager { * @return The view or {@code null} if view was not found */ private View findView(@NonNull AutofillId autofillId) { - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client == null) { return null; @@ -1529,6 +1712,8 @@ public final class AutofillManager { final String pfx = outerPrefix + " "; 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(getClient()); 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); @@ -1543,6 +1728,10 @@ public final class AutofillManager { pw.print(pfx2); pw.print("invisible:"); pw.println(mTrackedViews.mInvisibleTrackedIds); } 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() { @@ -1555,6 +1744,8 @@ public final class AutofillManager { return "STATE_FINISHED"; case STATE_SHOWING_SAVE_UI: return "STATE_SHOWING_SAVE_UI"; + case STATE_DISABLED_BY_SERVICE: + return "STATE_DISABLED_BY_SERVICE"; default: return "INVALID:" + mState; } @@ -1564,12 +1755,12 @@ public final class AutofillManager { return mState == STATE_ACTIVE; } - private boolean isFinishedLocked() { - return mState == STATE_FINISHED; + private boolean isDisabledByService() { + return mState == STATE_DISABLED_BY_SERVICE; } private void post(Runnable runnable) { - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (client == null) { if (sVerbose) Log.v(TAG, "ignoring post() because client is null"); return; @@ -1652,7 +1843,7 @@ public final class AutofillManager { * @param trackedIds The views to be tracked */ TrackedViews(@Nullable AutofillId[] trackedIds) { - final AutofillClient client = getClientLocked(); + final AutofillClient client = getClient(); if (trackedIds != null && client != null) { final boolean[] isVisible; @@ -1693,7 +1884,7 @@ public final class AutofillManager { * @param isVisible visible if the view is visible in the view hierarchy. */ void notifyViewVisibilityChanged(@NonNull AutofillId id, boolean isVisible) { - AutofillClient client = getClientLocked(); + AutofillClient client = getClient(); if (sDebug) { Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible=" @@ -1730,7 +1921,7 @@ public final class AutofillManager { void onVisibleForAutofillLocked() { // The visibility of the views might have changed while the client was not be visible, // hence update the visibility state for all views. - AutofillClient client = getClientLocked(); + AutofillClient client = getClient(); ArraySet<AutofillId> updatedVisibleTrackedIds = null; ArraySet<AutofillId> updatedInvisibleTrackedIds = null; if (client != null) { @@ -1791,7 +1982,7 @@ public final class AutofillManager { * Callback for autofill related events. * * <p>Typically used for applications that display their own "auto-complete" views, so they can - * enable / disable such views when the autofill UI affordance is shown / hidden. + * enable / disable such views when the autofill UI is shown / hidden. */ public abstract static class AutofillCallback { @@ -1801,26 +1992,26 @@ public final class AutofillManager { public @interface AutofillEventType {} /** - * The autofill input UI affordance associated with the view was shown. + * The autofill input UI associated with the view was shown. * - * <p>If the view provides its own auto-complete UI affordance and its currently shown, it + * <p>If the view provides its own auto-complete UI and its currently shown, it * should be hidden upon receiving this event. */ public static final int EVENT_INPUT_SHOWN = 1; /** - * The autofill input UI affordance associated with the view was hidden. + * The autofill input UI associated with the view was hidden. * - * <p>If the view provides its own auto-complete UI affordance that was hidden upon a + * <p>If the view provides its own auto-complete UI that was hidden upon a * {@link #EVENT_INPUT_SHOWN} event, it could be shown again now. */ public static final int EVENT_INPUT_HIDDEN = 2; /** - * The autofill input UI affordance associated with the view isn't shown because + * The autofill input UI associated with the view isn't shown because * autofill is not available. * - * <p>If the view provides its own auto-complete UI affordance but was not displaying it + * <p>If the view provides its own auto-complete UI but was not displaying it * to avoid flickering, it could shown it upon receiving this event. */ public static final int EVENT_INPUT_UNAVAILABLE = 3; @@ -1856,10 +2047,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)); } } @@ -1899,10 +2090,10 @@ public final class AutofillManager { } @Override - public void notifyNoFillUi(int sessionId, AutofillId id, boolean sessionFinished) { + public void notifyNoFillUi(int sessionId, AutofillId id, int sessionFinishedState) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() -> afm.notifyNoFillUi(sessionId, id, sessionFinished)); + afm.post(() -> afm.notifyNoFillUi(sessionId, id, sessionFinishedState)); } } @@ -1922,12 +2113,12 @@ public final class AutofillManager { @Override public void setTrackedViews(int sessionId, AutofillId[] ids, - boolean saveOnAllViewsInvisible, AutofillId[] fillableIds) { + boolean saveOnAllViewsInvisible, boolean saveOnFinish, AutofillId[] fillableIds, + AutofillId saveTriggerId) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() -> - afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible, fillableIds) - ); + afm.post(() -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible, + saveOnFinish, fillableIds, saveTriggerId)); } } diff --git a/core/java/android/view/autofill/AutofillPopupWindow.java b/core/java/android/view/autofill/AutofillPopupWindow.java index b4688bb1d48b..5cba21e3cc07 100644 --- a/core/java/android/view/autofill/AutofillPopupWindow.java +++ b/core/java/android/view/autofill/AutofillPopupWindow.java @@ -108,11 +108,12 @@ public class AutofillPopupWindow extends PopupWindow { // symmetrically when the dropdown is below and above the anchor. final View actualAnchor; if (virtualBounds != null) { + final int[] mLocationOnScreen = new int[] {virtualBounds.left, virtualBounds.top}; actualAnchor = new View(anchor.getContext()) { @Override public void getLocationOnScreen(int[] location) { - location[0] = virtualBounds.left; - location[1] = virtualBounds.top; + location[0] = mLocationOnScreen[0]; + location[1] = mLocationOnScreen[1]; } @Override @@ -178,6 +179,12 @@ public class AutofillPopupWindow extends PopupWindow { virtualBounds.right, virtualBounds.bottom); actualAnchor.setScrollX(anchor.getScrollX()); actualAnchor.setScrollY(anchor.getScrollY()); + + anchor.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + mLocationOnScreen[0] = mLocationOnScreen[0] - (scrollX - oldScrollX); + mLocationOnScreen[1] = mLocationOnScreen[1] - (scrollY - oldScrollY); + }); + actualAnchor.setWillNotDraw(true); } else { actualAnchor = anchor; } diff --git a/core/java/android/view/autofill/AutofillValue.java b/core/java/android/view/autofill/AutofillValue.java index 3beae11cf38c..8e649de52c97 100644 --- a/core/java/android/view/autofill/AutofillValue.java +++ b/core/java/android/view/autofill/AutofillValue.java @@ -177,7 +177,7 @@ public final class AutofillValue implements Parcelable { .append("[type=").append(mType) .append(", value="); if (isText()) { - string.append(((CharSequence) mValue).length()).append("_chars"); + Helper.appendRedacted(string, (CharSequence) mValue); } else { string.append(mValue); } diff --git a/core/java/android/view/autofill/Helper.java b/core/java/android/view/autofill/Helper.java index 829e7f3aa5ac..4b2c53c7eb84 100644 --- a/core/java/android/view/autofill/Helper.java +++ b/core/java/android/view/autofill/Helper.java @@ -16,11 +16,8 @@ package android.view.autofill; -import android.os.Bundle; - -import java.util.Arrays; -import java.util.Objects; -import java.util.Set; +import android.annotation.NonNull; +import android.annotation.Nullable; /** @hide */ public final class Helper { @@ -29,25 +26,37 @@ public final class Helper { public static boolean sDebug = false; public static boolean sVerbose = false; - public static final String REDACTED = "[REDACTED]"; + /** + * Appends {@code value} to the {@code builder} redacting its contents. + */ + public static void appendRedacted(@NonNull StringBuilder builder, + @Nullable CharSequence value) { + builder.append(getRedacted(value)); + } - static StringBuilder append(StringBuilder builder, Bundle bundle) { - if (bundle == null || !sDebug) { + /** + * Gets the redacted version of a value. + */ + @NonNull + public static String getRedacted(@Nullable CharSequence value) { + return (value == null) ? "null" : value.length() + "_chars"; + } + + /** + * Appends {@code values} to the {@code builder} redacting its contents. + */ + public static void appendRedacted(@NonNull StringBuilder builder, @Nullable String[] values) { + if (values == null) { builder.append("N/A"); - } else if (!sVerbose) { - builder.append(REDACTED); - } else { - final Set<String> keySet = bundle.keySet(); - builder.append("[Bundle with ").append(keySet.size()).append(" extras:"); - for (String key : keySet) { - final Object value = bundle.get(key); - builder.append(' ').append(key).append('='); - builder.append((value instanceof Object[]) - ? Arrays.toString((Objects[]) value) : value); - } - builder.append(']'); + return; + } + builder.append("["); + for (String value : values) { + builder.append(" '"); + appendRedacted(builder, value); + builder.append("'"); } - return builder; + builder.append(" ]"); } private Helper() { diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl index d6db3fe573f5..f49aa5b4d442 100644 --- a/core/java/android/view/autofill/IAutoFillManager.aidl +++ b/core/java/android/view/autofill/IAutoFillManager.aidl @@ -21,6 +21,7 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.service.autofill.FillEventHistory; +import android.service.autofill.UserData; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; @@ -53,4 +54,7 @@ interface IAutoFillManager { boolean isServiceSupported(int userId); boolean isServiceEnabled(int userId, String packageName); void onPendingSaveUi(int operation, IBinder token); + UserData getUserData(); + void setUserData(in UserData userData); + boolean isFieldClassificationEnabled(); } diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl index 3dabcec8636a..254c8a5ac20c 100644 --- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl +++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl @@ -35,7 +35,7 @@ oneway interface IAutoFillManagerClient { /** * Notifies the client when the autofill enabled state changed. */ - void setState(boolean enabled, boolean resetSession, boolean resetClient); + void setState(int flags); /** * Autofills the activity with the contents of a dataset. @@ -53,7 +53,8 @@ oneway interface IAutoFillManagerClient { * the session is finished automatically. */ void setTrackedViews(int sessionId, in @nullable AutofillId[] savableIds, - boolean saveOnAllViewsInvisible, in @nullable AutofillId[] fillableIds); + boolean saveOnAllViewsInvisible, boolean saveOnFinish, + in @nullable AutofillId[] fillableIds, in AutofillId saveTriggerId); /** * Requests showing the fill UI. @@ -67,9 +68,10 @@ oneway interface IAutoFillManagerClient { void requestHideFillUi(int sessionId, in AutofillId id); /** - * Notifies no fill UI will be shown, and also mark the state as finished if necessary. + * Notifies no fill UI will be shown, and also mark the state as finished if necessary (if + * sessionFinishedState != 0). */ - void notifyNoFillUi(int sessionId, in AutofillId id, boolean sessionFinished); + void notifyNoFillUi(int sessionId, in AutofillId id, int sessionFinishedState); /** * Starts the provided intent sender. diff --git a/core/java/android/view/inputmethod/ExtractedText.java b/core/java/android/view/inputmethod/ExtractedText.java index 0c5d9e9f259a..003f221d08b2 100644 --- a/core/java/android/view/inputmethod/ExtractedText.java +++ b/core/java/android/view/inputmethod/ExtractedText.java @@ -87,6 +87,11 @@ public class ExtractedText implements Parcelable { public int flags; /** + * The hint that has been extracted. + */ + public CharSequence hint; + + /** * Used to package this object into a {@link Parcel}. * * @param dest The {@link Parcel} to be written. @@ -100,6 +105,7 @@ public class ExtractedText implements Parcelable { dest.writeInt(selectionStart); dest.writeInt(selectionEnd); dest.writeInt(this.flags); + TextUtils.writeToParcel(hint, dest, flags); } /** @@ -107,17 +113,18 @@ public class ExtractedText implements Parcelable { */ public static final Parcelable.Creator<ExtractedText> CREATOR = new Parcelable.Creator<ExtractedText>() { - public ExtractedText createFromParcel(Parcel source) { - ExtractedText res = new ExtractedText(); - res.text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - res.startOffset = source.readInt(); - res.partialStartOffset = source.readInt(); - res.partialEndOffset = source.readInt(); - res.selectionStart = source.readInt(); - res.selectionEnd = source.readInt(); - res.flags = source.readInt(); - return res; - } + public ExtractedText createFromParcel(Parcel source) { + ExtractedText res = new ExtractedText(); + res.text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.startOffset = source.readInt(); + res.partialStartOffset = source.readInt(); + res.partialEndOffset = source.readInt(); + res.selectionStart = source.readInt(); + res.selectionEnd = source.readInt(); + res.flags = source.readInt(); + res.hint = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + return res; + } public ExtractedText[] newArray(int size) { return new ExtractedText[size]; diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java index 0922422c5125..ab8886bb8479 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -16,6 +16,7 @@ package android.view.inputmethod; +import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; @@ -90,8 +91,9 @@ public interface InputMethod { * accept the first token given to you. Any after that may come from the * client. */ + @MainThread public void attachToken(IBinder token); - + /** * Bind a new application environment in to the input method, so that it * can later start and stop input processing. @@ -104,6 +106,7 @@ public interface InputMethod { * @see InputBinding * @see #unbindInput() */ + @MainThread public void bindInput(InputBinding binding); /** @@ -114,6 +117,7 @@ public interface InputMethod { * Typically this method is called when the application changes to be * non-foreground. */ + @MainThread public void unbindInput(); /** @@ -129,6 +133,7 @@ public interface InputMethod { * * @see EditorInfo */ + @MainThread public void startInput(InputConnection inputConnection, EditorInfo info); /** @@ -147,6 +152,7 @@ public interface InputMethod { * * @see EditorInfo */ + @MainThread public void restartInput(InputConnection inputConnection, EditorInfo attribute); /** @@ -177,6 +183,7 @@ public interface InputMethod { * @see EditorInfo * @hide */ + @MainThread default void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo, boolean restarting, @NonNull IBinder startInputToken) { @@ -195,6 +202,7 @@ public interface InputMethod { * * @param callback Interface that is called with the newly created session. */ + @MainThread public void createSession(SessionCallback callback); /** @@ -203,6 +211,7 @@ public interface InputMethod { * @param session The {@link InputMethodSession} previously provided through * SessionCallback.sessionCreated() that is to be changed. */ + @MainThread public void setSessionEnabled(InputMethodSession session, boolean enabled); /** @@ -214,6 +223,7 @@ public interface InputMethod { * @param session The {@link InputMethodSession} previously provided through * SessionCallback.sessionCreated() that is to be revoked. */ + @MainThread public void revokeSession(InputMethodSession session); /** @@ -244,6 +254,7 @@ public interface InputMethod { * {@link InputMethodManager#RESULT_SHOWN InputMethodManager.RESULT_SHOWN}, or * {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}. */ + @MainThread public void showSoftInput(int flags, ResultReceiver resultReceiver); /** @@ -258,11 +269,13 @@ public interface InputMethod { * {@link InputMethodManager#RESULT_SHOWN InputMethodManager.RESULT_SHOWN}, or * {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}. */ + @MainThread public void hideSoftInput(int flags, ResultReceiver resultReceiver); /** * Notify that the input method subtype is being changed in the same input method. * @param subtype New subtype of the notified input method */ + @MainThread public void changeInputMethodSubtype(InputMethodSubtype subtype); } diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index f0645b895afa..c69543f6d2d8 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -67,6 +67,11 @@ public final class InputMethodInfo implements Parcelable { final ResolveInfo mService; /** + * IME only supports VR mode. + */ + final boolean mIsVrOnly; + + /** * The unique string Id to identify the input method. This is generated * from the input method component. */ @@ -149,6 +154,7 @@ public final class InputMethodInfo implements Parcelable { PackageManager pm = context.getPackageManager(); String settingsActivityComponent = null; + boolean isVrOnly; int isDefaultResId = 0; XmlResourceParser parser = null; @@ -179,6 +185,7 @@ public final class InputMethodInfo implements Parcelable { com.android.internal.R.styleable.InputMethod); settingsActivityComponent = sa.getString( com.android.internal.R.styleable.InputMethod_settingsActivity); + isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly, false); isDefaultResId = sa.getResourceId( com.android.internal.R.styleable.InputMethod_isDefault, 0); supportsSwitchingToNextInputMethod = sa.getBoolean( @@ -254,6 +261,8 @@ public final class InputMethodInfo implements Parcelable { mIsDefaultResId = isDefaultResId; mIsAuxIme = isAuxIme; mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod; + // TODO(b/68948291): remove this meta-data before release. + mIsVrOnly = isVrOnly || service.serviceInfo.metaData.getBoolean("isVrOnly", false); } InputMethodInfo(Parcel source) { @@ -262,6 +271,7 @@ public final class InputMethodInfo implements Parcelable { mIsDefaultResId = source.readInt(); mIsAuxIme = source.readInt() == 1; mSupportsSwitchingToNextInputMethod = source.readInt() == 1; + mIsVrOnly = source.readBoolean(); mService = ResolveInfo.CREATOR.createFromParcel(source); mSubtypes = new InputMethodSubtypeArray(source); mForceDefault = false; @@ -274,7 +284,8 @@ public final class InputMethodInfo implements Parcelable { CharSequence label, String settingsActivity) { this(buildDummyResolveInfo(packageName, className, label), false /* isAuxIme */, settingsActivity, null /* subtypes */, 0 /* isDefaultResId */, - false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */); + false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */, + false /* isVrOnly */); } /** @@ -285,7 +296,7 @@ public final class InputMethodInfo implements Parcelable { String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault) { this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault, - true /* supportsSwitchingToNextInputMethod */); + true /* supportsSwitchingToNextInputMethod */, false /* isVrOnly */); } /** @@ -294,7 +305,7 @@ public final class InputMethodInfo implements Parcelable { */ public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault, - boolean supportsSwitchingToNextInputMethod) { + boolean supportsSwitchingToNextInputMethod, boolean isVrOnly) { final ServiceInfo si = ri.serviceInfo; mService = ri; mId = new ComponentName(si.packageName, si.name).flattenToShortString(); @@ -304,6 +315,7 @@ public final class InputMethodInfo implements Parcelable { mSubtypes = new InputMethodSubtypeArray(subtypes); mForceDefault = forceDefault; mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod; + mIsVrOnly = isVrOnly; } private static ResolveInfo buildDummyResolveInfo(String packageName, String className, @@ -398,6 +410,14 @@ public final class InputMethodInfo implements Parcelable { } /** + * Returns true if IME supports VR mode only. + * @hide + */ + public boolean isVrOnly() { + return mIsVrOnly; + } + + /** * Return the count of the subtypes of Input Method. */ public int getSubtypeCount() { @@ -444,6 +464,7 @@ public final class InputMethodInfo implements Parcelable { public void dump(Printer pw, String prefix) { pw.println(prefix + "mId=" + mId + " mSettingsActivityName=" + mSettingsActivityName + + " mIsVrOnly=" + mIsVrOnly + " mSupportsSwitchingToNextInputMethod=" + mSupportsSwitchingToNextInputMethod); pw.println(prefix + "mIsDefaultResId=0x" + Integer.toHexString(mIsDefaultResId)); @@ -509,6 +530,7 @@ public final class InputMethodInfo implements Parcelable { dest.writeInt(mIsDefaultResId); dest.writeInt(mIsAuxIme ? 1 : 0); dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0); + dest.writeBoolean(mIsVrOnly); mService.writeToParcel(dest, flags); mSubtypes.writeToParcel(dest); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 92d1de8e5a24..3cd8d4a2417d 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -24,6 +24,7 @@ import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.content.Context; import android.graphics.Rect; +import android.inputmethodservice.InputMethodService; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -697,6 +698,19 @@ public final class InputMethodManager { } } + /** + * Returns a list of VR InputMethod currently installed. + * @hide + */ + @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + public List<InputMethodInfo> getVrInputMethodList() { + try { + return mService.getVrInputMethodList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + public List<InputMethodInfo> getEnabledInputMethodList() { try { return mService.getEnabledInputMethodList(); @@ -722,7 +736,20 @@ public final class InputMethodManager { } } + /** + * @deprecated Use {@link InputMethodService#showStatusIcon(int)} instead. This method was + * intended for IME developers who should be accessing APIs through the service. APIs in this + * class are intended for app developers interacting with the IME. + */ + @Deprecated public void showStatusIcon(IBinder imeToken, String packageName, int iconId) { + showStatusIconInternal(imeToken, packageName, iconId); + } + + /** + * @hide + */ + public void showStatusIconInternal(IBinder imeToken, String packageName, int iconId) { try { mService.updateStatusIcon(imeToken, packageName, iconId); } catch (RemoteException e) { @@ -730,7 +757,20 @@ public final class InputMethodManager { } } + /** + * @deprecated Use {@link InputMethodService#hideStatusIcon()} instead. This method was + * intended for IME developers who should be accessing APIs through the service. APIs in + * this class are intended for app developers interacting with the IME. + */ + @Deprecated public void hideStatusIcon(IBinder imeToken) { + hideStatusIconInternal(imeToken); + } + + /** + * @hide + */ + public void hideStatusIconInternal(IBinder imeToken) { try { mService.updateStatusIcon(imeToken, null, 0); } catch (RemoteException e) { @@ -1108,7 +1148,6 @@ public final class InputMethodManager { } } - /** * This method toggles the input method window display. * If the input window is already displayed, it gets hidden. @@ -1787,8 +1826,19 @@ public final class InputMethodManager { * when it was started, which allows it to perform this operation on * itself. * @param id The unique identifier for the new input method to be switched to. + * @deprecated Use {@link InputMethodService#setInputMethod(String)} instead. This method + * was intended for IME developers who should be accessing APIs through the service. APIs in + * this class are intended for app developers interacting with the IME. */ + @Deprecated public void setInputMethod(IBinder token, String id) { + setInputMethodInternal(token, id); + } + + /** + * @hide + */ + public void setInputMethodInternal(IBinder token, String id) { try { mService.setInputMethod(token, id); } catch (RemoteException e) { @@ -1804,8 +1854,21 @@ public final class InputMethodManager { * itself. * @param id The unique identifier for the new input method to be switched to. * @param subtype The new subtype of the new input method to be switched to. + * @deprecated Use + * {@link InputMethodService#setInputMethodAndSubtype(String, InputMethodSubtype)} + * instead. This method was intended for IME developers who should be accessing APIs through + * the service. APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public void setInputMethodAndSubtype(IBinder token, String id, InputMethodSubtype subtype) { + setInputMethodAndSubtypeInternal(token, id, subtype); + } + + /** + * @hide + */ + public void setInputMethodAndSubtypeInternal( + IBinder token, String id, InputMethodSubtype subtype) { try { mService.setInputMethodAndSubtype(token, id, subtype); } catch (RemoteException e) { @@ -1824,8 +1887,19 @@ public final class InputMethodManager { * @param flags Provides additional operating flags. Currently may be * 0 or have the {@link #HIDE_IMPLICIT_ONLY}, * {@link #HIDE_NOT_ALWAYS} bit set. + * @deprecated Use {@link InputMethodService#hideSoftInputFromInputMethod(int)} + * instead. This method was intended for IME developers who should be accessing APIs through + * the service. APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public void hideSoftInputFromInputMethod(IBinder token, int flags) { + hideSoftInputFromInputMethodInternal(token, flags); + } + + /** + * @hide + */ + public void hideSoftInputFromInputMethodInternal(IBinder token, int flags) { try { mService.hideMySoftInput(token, flags); } catch (RemoteException e) { @@ -1845,8 +1919,19 @@ public final class InputMethodManager { * @param flags Provides additional operating flags. Currently may be * 0 or have the {@link #SHOW_IMPLICIT} or * {@link #SHOW_FORCED} bit set. + * @deprecated Use {@link InputMethodService#showSoftInputFromInputMethod(int)} + * instead. This method was intended for IME developers who should be accessing APIs through + * the service. APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public void showSoftInputFromInputMethod(IBinder token, int flags) { + showSoftInputFromInputMethodInternal(token, flags); + } + + /** + * @hide + */ + public void showSoftInputFromInputMethodInternal(IBinder token, int flags) { try { mService.showMySoftInput(token, flags); } catch (RemoteException e) { @@ -2226,8 +2311,19 @@ public final class InputMethodManager { * which allows it to perform this operation on itself. * @return true if the current input method and subtype was successfully switched to the last * used input method and subtype. + * @deprecated Use {@link InputMethodService#switchToLastInputMethod()} instead. This method + * was intended for IME developers who should be accessing APIs through the service. APIs in + * this class are intended for app developers interacting with the IME. */ + @Deprecated public boolean switchToLastInputMethod(IBinder imeToken) { + return switchToLastInputMethodInternal(imeToken); + } + + /** + * @hide + */ + public boolean switchToLastInputMethodInternal(IBinder imeToken) { synchronized (mH) { try { return mService.switchToLastInputMethod(imeToken); @@ -2246,8 +2342,19 @@ public final class InputMethodManager { * belongs to the current IME * @return true if the current input method and subtype was successfully switched to the next * input method and subtype. + * @deprecated Use {@link InputMethodService#switchToNextInputMethod(boolean)} instead. This + * method was intended for IME developers who should be accessing APIs through the service. + * APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public boolean switchToNextInputMethod(IBinder imeToken, boolean onlyCurrentIme) { + return switchToNextInputMethodInternal(imeToken, onlyCurrentIme); + } + + /** + * @hide + */ + public boolean switchToNextInputMethodInternal(IBinder imeToken, boolean onlyCurrentIme) { synchronized (mH) { try { return mService.switchToNextInputMethod(imeToken, onlyCurrentIme); @@ -2267,8 +2374,19 @@ public final class InputMethodManager { * between IMEs and subtypes. * @param imeToken Supplies the identifying token given to an input method when it was started, * which allows it to perform this operation on itself. + * @deprecated Use {@link InputMethodService#shouldOfferSwitchingToNextInputMethod()} + * instead. This method was intended for IME developers who should be accessing APIs through + * the service. APIs in this class are intended for app developers interacting with the IME. */ + @Deprecated public boolean shouldOfferSwitchingToNextInputMethod(IBinder imeToken) { + return shouldOfferSwitchingToNextInputMethodInternal(imeToken); + } + + /** + * @hide + */ + public boolean shouldOfferSwitchingToNextInputMethodInternal(IBinder imeToken) { synchronized (mH) { try { return mService.shouldOfferSwitchingToNextInputMethod(imeToken); diff --git a/core/java/android/view/inputmethod/InputMethodManagerInternal.java b/core/java/android/view/inputmethod/InputMethodManagerInternal.java index 77df4e3883a7..e13813e5199b 100644 --- a/core/java/android/view/inputmethod/InputMethodManagerInternal.java +++ b/core/java/android/view/inputmethod/InputMethodManagerInternal.java @@ -16,6 +16,8 @@ package android.view.inputmethod; +import android.content.ComponentName; + /** * Input method manager local system service interface. * @@ -37,4 +39,9 @@ public interface InputMethodManagerInternal { * Hides the current input method, if visible. */ void hideCurrentInputMethod(); + + /** + * Switches to VR InputMethod defined in the packageName of {@param componentName}. + */ + void startVrInputMethodNoCheck(ComponentName componentName); } diff --git a/core/java/android/view/textclassifier/EntityConfidence.java b/core/java/android/view/textclassifier/EntityConfidence.java index 0589d204ac3f..19660d95e927 100644 --- a/core/java/android/view/textclassifier/EntityConfidence.java +++ b/core/java/android/view/textclassifier/EntityConfidence.java @@ -18,13 +18,12 @@ package android.view.textclassifier; import android.annotation.FloatRange; import android.annotation.NonNull; +import android.util.ArrayMap; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,42 +35,43 @@ import java.util.Map; */ final class EntityConfidence<T> { - private final Map<T, Float> mEntityConfidence = new HashMap<>(); - - private final Comparator<T> mEntityComparator = (e1, e2) -> { - float score1 = mEntityConfidence.get(e1); - float score2 = mEntityConfidence.get(e2); - if (score1 > score2) { - return -1; - } - if (score1 < score2) { - return 1; - } - return 0; - }; + private final ArrayMap<T, Float> mEntityConfidence = new ArrayMap<>(); + private final ArrayList<T> mSortedEntities = new ArrayList<>(); EntityConfidence() {} EntityConfidence(@NonNull EntityConfidence<T> source) { Preconditions.checkNotNull(source); mEntityConfidence.putAll(source.mEntityConfidence); + mSortedEntities.addAll(source.mSortedEntities); } /** - * Sets an entity type for the classified text and assigns a confidence score. + * Constructs an EntityConfidence from a map of entity to confidence. * - * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence). - * 0 implies the entity does not exist for the classified text. - * Values greater than 1 are clamped to 1. + * Map entries that have 0 confidence are removed, and values greater than 1 are clamped to 1. + * + * @param source a map from entity to a confidence value in the range 0 (low confidence) to + * 1 (high confidence). */ - public void setEntityType( - @NonNull T type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { - Preconditions.checkNotNull(type); - if (confidenceScore > 0) { - mEntityConfidence.put(type, Math.min(1, confidenceScore)); - } else { - mEntityConfidence.remove(type); + EntityConfidence(@NonNull Map<T, Float> source) { + Preconditions.checkNotNull(source); + + // Prune non-existent entities and clamp to 1. + mEntityConfidence.ensureCapacity(source.size()); + for (Map.Entry<T, Float> it : source.entrySet()) { + if (it.getValue() <= 0) continue; + mEntityConfidence.put(it.getKey(), Math.min(1, it.getValue())); } + + // Create a list of entities sorted by decreasing confidence for getEntities(). + mSortedEntities.ensureCapacity(mEntityConfidence.size()); + mSortedEntities.addAll(mEntityConfidence.keySet()); + mSortedEntities.sort((e1, e2) -> { + float score1 = mEntityConfidence.get(e1); + float score2 = mEntityConfidence.get(e2); + return Float.compare(score2, score1); + }); } /** @@ -80,10 +80,7 @@ final class EntityConfidence<T> { */ @NonNull public List<T> getEntities() { - List<T> entities = new ArrayList<>(mEntityConfidence.size()); - entities.addAll(mEntityConfidence.keySet()); - entities.sort(mEntityComparator); - return Collections.unmodifiableList(entities); + return Collections.unmodifiableList(mSortedEntities); } /** diff --git a/core/java/android/view/textclassifier/Log.java b/core/java/android/view/textclassifier/Log.java new file mode 100644 index 000000000000..83ca15df92f4 --- /dev/null +++ b/core/java/android/view/textclassifier/Log.java @@ -0,0 +1,46 @@ +/* + * 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.textclassifier; + +import android.util.Slog; + +/** + * Logging for android.view.textclassifier package. + */ +final class Log { + + /** + * true: Enables full logging. + * false: Limits logging to debug level. + */ + private static final boolean ENABLE_FULL_LOGGING = false; + + private Log() {} + + public static void d(String tag, String msg) { + Slog.d(tag, msg); + } + + public static void e(String tag, String msg, Throwable tr) { + if (ENABLE_FULL_LOGGING) { + Slog.e(tag, msg, tr); + } else { + final String trString = (tr != null) ? tr.getClass().getSimpleName() : "??"; + Slog.d(tag, String.format("%s (%s)", msg, trString)); + } + } +} diff --git a/core/java/android/view/textclassifier/SmartSelection.java b/core/java/android/view/textclassifier/SmartSelection.java index f0e83d1fd85f..2c93a19bbe0e 100644 --- a/core/java/android/view/textclassifier/SmartSelection.java +++ b/core/java/android/view/textclassifier/SmartSelection.java @@ -16,6 +16,8 @@ package android.view.textclassifier; +import android.content.res.AssetFileDescriptor; + /** * Java wrapper for SmartSelection native library interface. * This library is used for detecting entities in text. @@ -42,6 +44,26 @@ final class SmartSelection { } /** + * Creates a new instance of SmartSelect predictor, using the provided model image, given as a + * file path. + */ + SmartSelection(String path) { + mCtx = nativeNewFromPath(path); + } + + /** + * Creates a new instance of SmartSelect predictor, using the provided model image, given as an + * AssetFileDescriptor. + */ + SmartSelection(AssetFileDescriptor afd) { + mCtx = nativeNewFromAssetFileDescriptor(afd, afd.getStartOffset(), afd.getLength()); + if (mCtx == 0L) { + throw new IllegalArgumentException( + "Couldn't initialize TC from given AssetFileDescriptor"); + } + } + + /** * Given a string context and current selection, computes the SmartSelection suggestion. * * The begin and end are character indices into the context UTF8 string. selectionBegin is the @@ -69,6 +91,15 @@ final class SmartSelection { } /** + * Annotates given input text. Every word of the input is a part of some annotation. + * The annotations are sorted by their position in the context string. + * The annotations do not overlap. + */ + public AnnotatedSpan[] annotate(String text) { + return nativeAnnotate(mCtx, text); + } + + /** * Frees up the allocated memory. */ public void close() { @@ -91,12 +122,19 @@ final class SmartSelection { private static native long nativeNew(int fd); + private static native long nativeNewFromPath(String path); + + private static native long nativeNewFromAssetFileDescriptor(AssetFileDescriptor afd, + long offset, long size); + private static native int[] nativeSuggest( long context, String text, int selectionBegin, int selectionEnd); private static native ClassificationResult[] nativeClassifyText( long context, String text, int selectionBegin, int selectionEnd, int hintFlags); + private static native AnnotatedSpan[] nativeAnnotate(long context, String text); + private static native void nativeClose(long context); private static native String nativeGetLanguage(int fd); @@ -114,4 +152,29 @@ final class SmartSelection { mScore = score; } } + + /** Represents a result of Annotate call. */ + public static final class AnnotatedSpan { + final int mStartIndex; + final int mEndIndex; + final ClassificationResult[] mClassification; + + AnnotatedSpan(int startIndex, int endIndex, ClassificationResult[] classification) { + mStartIndex = startIndex; + mEndIndex = endIndex; + mClassification = classification; + } + + public int getStartIndex() { + return mStartIndex; + } + + public int getEndIndex() { + return mEndIndex; + } + + public ClassificationResult[] getClassification() { + return mClassification; + } + } } diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java index 1849368f6ae9..7ffbf6357f45 100644 --- a/core/java/android/view/textclassifier/TextClassification.java +++ b/core/java/android/view/textclassifier/TextClassification.java @@ -23,15 +23,74 @@ import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; +import android.os.LocaleList; +import android.util.ArrayMap; import android.view.View.OnClickListener; import android.view.textclassifier.TextClassifier.EntityType; import com.android.internal.util.Preconditions; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.Map; /** * Information for generating a widget to handle classified text. + * + * <p>A TextClassification object contains icons, labels, onClickListeners and intents that may + * be used to build a widget that can be used to act on classified text. There is the concept of a + * <i>primary action</i> and other <i>secondary actions</i>. + * + * <p>e.g. building a view that, when clicked, shares the classified text with the preferred app: + * + * <pre>{@code + * // Called preferably outside the UiThread. + * TextClassification classification = textClassifier.classifyText(allText, 10, 25); + * + * // Called on the UiThread. + * Button button = new Button(context); + * button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null); + * button.setText(classification.getLabel()); + * button.setOnClickListener(classification.getOnClickListener()); + * }</pre> + * + * <p>e.g. starting an action mode with menu items that can handle the classified text: + * + * <pre>{@code + * // Called preferably outside the UiThread. + * final TextClassification classification = textClassifier.classifyText(allText, 10, 25); + * + * // Called on the UiThread. + * view.startActionMode(new ActionMode.Callback() { + * + * public boolean onCreateActionMode(ActionMode mode, Menu menu) { + * // Add the "primary" action. + * if (thisAppHasPermissionToInvokeIntent(classification.getIntent())) { + * menu.add(Menu.NONE, 0, 20, classification.getLabel()) + * .setIcon(classification.getIcon()) + * .setIntent(classification.getIntent()); + * } + * // Add the "secondary" actions. + * for (int i = 0; i < classification.getSecondaryActionsCount(); i++) { + * if (thisAppHasPermissionToInvokeIntent(classification.getSecondaryIntent(i))) { + * menu.add(Menu.NONE, i + 1, 20, classification.getSecondaryLabel(i)) + * .setIcon(classification.getSecondaryIcon(i)) + * .setIntent(classification.getSecondaryIntent(i)); + * } + * } + * return true; + * } + * + * public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + * context.startActivity(item.getIntent()); + * return true; + * } + * + * ... + * }); + * }</pre> + * */ public final class TextClassification { @@ -41,33 +100,43 @@ public final class TextClassification { static final TextClassification EMPTY = new TextClassification.Builder().build(); @NonNull private final String mText; - @Nullable private final Drawable mIcon; - @Nullable private final String mLabel; - @Nullable private final Intent mIntent; - @Nullable private final OnClickListener mOnClickListener; + @Nullable private final Drawable mPrimaryIcon; + @Nullable private final String mPrimaryLabel; + @Nullable private final Intent mPrimaryIntent; + @Nullable private final OnClickListener mPrimaryOnClickListener; + @NonNull private final List<Drawable> mSecondaryIcons; + @NonNull private final List<String> mSecondaryLabels; + @NonNull private final List<Intent> mSecondaryIntents; + @NonNull private final List<OnClickListener> mSecondaryOnClickListeners; @NonNull private final EntityConfidence<String> mEntityConfidence; - @NonNull private final List<String> mEntities; - private int mLogType; - @NonNull private final String mVersionInfo; + @NonNull private final String mSignature; private TextClassification( @Nullable String text, - @Nullable Drawable icon, - @Nullable String label, - @Nullable Intent intent, - @Nullable OnClickListener onClickListener, - @NonNull EntityConfidence<String> entityConfidence, - int logType, - @NonNull String versionInfo) { + @Nullable Drawable primaryIcon, + @Nullable String primaryLabel, + @Nullable Intent primaryIntent, + @Nullable OnClickListener primaryOnClickListener, + @NonNull List<Drawable> secondaryIcons, + @NonNull List<String> secondaryLabels, + @NonNull List<Intent> secondaryIntents, + @NonNull List<OnClickListener> secondaryOnClickListeners, + @NonNull Map<String, Float> entityConfidence, + @NonNull String signature) { + Preconditions.checkArgument(secondaryLabels.size() == secondaryIntents.size()); + Preconditions.checkArgument(secondaryIcons.size() == secondaryIntents.size()); + Preconditions.checkArgument(secondaryOnClickListeners.size() == secondaryIntents.size()); mText = text; - mIcon = icon; - mLabel = label; - mIntent = intent; - mOnClickListener = onClickListener; + mPrimaryIcon = primaryIcon; + mPrimaryLabel = primaryLabel; + mPrimaryIntent = primaryIntent; + mPrimaryOnClickListener = primaryOnClickListener; + mSecondaryIcons = secondaryIcons; + mSecondaryLabels = secondaryLabels; + mSecondaryIntents = secondaryIntents; + mSecondaryOnClickListeners = secondaryOnClickListeners; mEntityConfidence = new EntityConfidence<>(entityConfidence); - mEntities = mEntityConfidence.getEntities(); - mLogType = logType; - mVersionInfo = versionInfo; + mSignature = signature; } /** @@ -83,7 +152,7 @@ public final class TextClassification { */ @IntRange(from = 0) public int getEntityCount() { - return mEntities.size(); + return mEntityConfidence.getEntities().size(); } /** @@ -95,7 +164,7 @@ public final class TextClassification { */ @NonNull public @EntityType String getEntity(int index) { - return mEntities.get(index); + return mEntityConfidence.getEntities().get(index); } /** @@ -109,59 +178,160 @@ public final class TextClassification { } /** - * Returns an icon that may be rendered on a widget used to act on the classified text. + * Returns the number of <i>secondary</i> actions that are available to act on the classified + * text. + * + * <p><strong>Note: </strong> that there may or may not be a <i>primary</i> action. + * + * @see #getSecondaryIntent(int) + * @see #getSecondaryLabel(int) + * @see #getSecondaryIcon(int) + * @see #getSecondaryOnClickListener(int) + */ + @IntRange(from = 0) + public int getSecondaryActionsCount() { + return mSecondaryIntents.size(); + } + + /** + * Returns one of the <i>secondary</i> icons that maybe rendered on a widget used to act on the + * classified text. + * + * @param index Index of the action to get the icon for. + * + * @throws IndexOutOfBoundsException if the specified index is out of range. + * + * @see #getSecondaryActionsCount() for the number of actions available. + * @see #getSecondaryIntent(int) + * @see #getSecondaryLabel(int) + * @see #getSecondaryOnClickListener(int) + * @see #getIcon() + */ + @Nullable + public Drawable getSecondaryIcon(int index) { + return mSecondaryIcons.get(index); + } + + /** + * Returns an icon for the <i>primary</i> intent that may be rendered on a widget used to act + * on the classified text. + * + * @see #getSecondaryIcon(int) */ @Nullable public Drawable getIcon() { - return mIcon; + return mPrimaryIcon; } /** - * Returns a label that may be rendered on a widget used to act on the classified text. + * Returns one of the <i>secondary</i> labels that may be rendered on a widget used to act on + * the classified text. + * + * @param index Index of the action to get the label for. + * + * @throws IndexOutOfBoundsException if the specified index is out of range. + * + * @see #getSecondaryActionsCount() + * @see #getSecondaryIntent(int) + * @see #getSecondaryIcon(int) + * @see #getSecondaryOnClickListener(int) + * @see #getLabel() + */ + @Nullable + public CharSequence getSecondaryLabel(int index) { + return mSecondaryLabels.get(index); + } + + /** + * Returns a label for the <i>primary</i> intent that may be rendered on a widget used to act + * on the classified text. + * + * @see #getSecondaryLabel(int) */ @Nullable public CharSequence getLabel() { - return mLabel; + return mPrimaryLabel; + } + + /** + * Returns one of the <i>secondary</i> intents that may be fired to act on the classified text. + * + * @param index Index of the action to get the intent for. + * + * @throws IndexOutOfBoundsException if the specified index is out of range. + * + * @see #getSecondaryActionsCount() + * @see #getSecondaryLabel(int) + * @see #getSecondaryIcon(int) + * @see #getSecondaryOnClickListener(int) + * @see #getIntent() + */ + @Nullable + public Intent getSecondaryIntent(int index) { + return mSecondaryIntents.get(index); } /** - * Returns an intent that may be fired to act on the classified text. + * Returns the <i>primary</i> intent that may be fired to act on the classified text. + * + * @see #getSecondaryIntent(int) */ @Nullable public Intent getIntent() { - return mIntent; + return mPrimaryIntent; } /** - * Returns an OnClickListener that may be triggered to act on the classified text. + * Returns one of the <i>secondary</i> OnClickListeners that may be triggered to act on the + * classified text. + * + * @param index Index of the action to get the click listener for. + * + * @throws IndexOutOfBoundsException if the specified index is out of range. + * + * @see #getSecondaryActionsCount() + * @see #getSecondaryIntent(int) + * @see #getSecondaryLabel(int) + * @see #getSecondaryIcon(int) + * @see #getOnClickListener() */ @Nullable - public OnClickListener getOnClickListener() { - return mOnClickListener; + public OnClickListener getSecondaryOnClickListener(int index) { + return mSecondaryOnClickListeners.get(index); } /** - * Returns the MetricsLogger subtype for the action that is performed for this result. - * @hide + * Returns the <i>primary</i> OnClickListener that may be triggered to act on the classified + * text. + * + * @see #getSecondaryOnClickListener(int) */ - public int getLogType() { - return mLogType; + @Nullable + public OnClickListener getOnClickListener() { + return mPrimaryOnClickListener; } /** - * Returns information about the classifier model used to generate this TextClassification. - * @hide + * Returns the signature for this object. + * The TextClassifier that generates this object may use it as a way to internally identify + * this object. */ @NonNull - public String getVersionInfo() { - return mVersionInfo; + public String getSignature() { + return mSignature; } @Override public String toString() { - return String.format("TextClassification {" - + "text=%s, entities=%s, label=%s, intent=%s}", - mText, mEntityConfidence, mLabel, mIntent); + return String.format(Locale.US, "TextClassification {" + + "text=%s, entities=%s, " + + "primaryLabel=%s, secondaryLabels=%s, " + + "primaryIntent=%s, secondaryIntents=%s, " + + "signature=%s}", + mText, mEntityConfidence, + mPrimaryLabel, mSecondaryLabels, + mPrimaryIntent, mSecondaryIntents, + mSignature); } /** @@ -180,18 +350,33 @@ public final class TextClassification { /** * Builder for building {@link TextClassification} objects. + * + * <p>e.g. + * + * <pre>{@code + * TextClassification classification = new TextClassification.Builder() + * .setText(classifiedText) + * .setEntityType(TextClassifier.TYPE_EMAIL, 0.9) + * .setEntityType(TextClassifier.TYPE_OTHER, 0.1) + * .setPrimaryAction(intent, label, icon, onClickListener) + * .addSecondaryAction(intent1, label1, icon1, onClickListener1) + * .addSecondaryAction(intent2, label2, icon2, onClickListener2) + * .build(); + * }</pre> */ public static final class Builder { @NonNull private String mText; - @Nullable private Drawable mIcon; - @Nullable private String mLabel; - @Nullable private Intent mIntent; - @Nullable private OnClickListener mOnClickListener; - @NonNull private final EntityConfidence<String> mEntityConfidence = - new EntityConfidence<>(); - private int mLogType; - @NonNull private String mVersionInfo = ""; + @NonNull private final List<Drawable> mSecondaryIcons = new ArrayList<>(); + @NonNull private final List<String> mSecondaryLabels = new ArrayList<>(); + @NonNull private final List<Intent> mSecondaryIntents = new ArrayList<>(); + @NonNull private final List<OnClickListener> mSecondaryOnClickListeners = new ArrayList<>(); + @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>(); + @Nullable Drawable mPrimaryIcon; + @Nullable String mPrimaryLabel; + @Nullable Intent mPrimaryIntent; + @Nullable OnClickListener mPrimaryOnClickListener; + @NonNull private String mSignature = ""; /** * Sets the classified text. @@ -203,6 +388,8 @@ public final class TextClassification { /** * Sets an entity type for the classification result and assigns a confidence score. + * If a confidence score had already been set for the specified entity type, this will + * override that score. * * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence). * 0 implies the entity does not exist for the classified text. @@ -211,57 +398,114 @@ public final class TextClassification { public Builder setEntityType( @NonNull @EntityType String type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { - mEntityConfidence.setEntityType(type, confidenceScore); + mEntityConfidence.put(type, confidenceScore); return this; } /** - * Sets an icon that may be rendered on a widget used to act on the classified text. + * Adds an <i>secondary</i> action that may be performed on the classified text. + * Secondary actions are in addition to the <i>primary</i> action which may or may not + * exist. + * + * <p>The label and icon are used for rendering of widgets that offer the intent. + * Actions should be added in order of priority. + * + * <p><stong>Note: </stong> If all input parameters are set to null, this method will be a + * no-op. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ - public Builder setIcon(@Nullable Drawable icon) { - mIcon = icon; + public Builder addSecondaryAction( + @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon, + @Nullable OnClickListener onClickListener) { + if (intent != null || label != null || icon != null || onClickListener != null) { + mSecondaryIntents.add(intent); + mSecondaryLabels.add(label); + mSecondaryIcons.add(icon); + mSecondaryOnClickListeners.add(onClickListener); + } return this; } /** - * Sets a label that may be rendered on a widget used to act on the classified text. + * Removes all the <i>secondary</i> actions. */ - public Builder setLabel(@Nullable String label) { - mLabel = label; + public Builder clearSecondaryActions() { + mSecondaryIntents.clear(); + mSecondaryOnClickListeners.clear(); + mSecondaryLabels.clear(); + mSecondaryIcons.clear(); return this; } /** - * Sets an intent that may be fired to act on the classified text. + * Sets the <i>primary</i> action that may be performed on the classified text. This is + * equivalent to calling {@code + * setIntent(intent).setLabel(label).setIcon(icon).setOnClickListener(onClickListener)}. + * + * <p><strong>Note: </strong>If all input parameters are null, there will be no + * <i>primary</i> action but there may still be <i>secondary</i> actions. + * + * @see #addSecondaryAction(Intent, String, Drawable, OnClickListener) + */ + public Builder setPrimaryAction( + @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon, + @Nullable OnClickListener onClickListener) { + return setIntent(intent).setLabel(label).setIcon(icon) + .setOnClickListener(onClickListener); + } + + /** + * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act + * on the classified text. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ - public Builder setIntent(@Nullable Intent intent) { - mIntent = intent; + public Builder setIcon(@Nullable Drawable icon) { + mPrimaryIcon = icon; return this; } /** - * Sets the MetricsLogger subtype for the action that is performed for this result. - * @hide + * Sets the label for the <i>primary</i> action that may be rendered on a widget used to + * act on the classified text. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ - public Builder setLogType(int type) { - mLogType = type; + public Builder setLabel(@Nullable String label) { + mPrimaryLabel = label; return this; } /** - * Sets an OnClickListener that may be triggered to act on the classified text. + * Sets the intent for the <i>primary</i> action that may be fired to act on the classified + * text. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) + */ + public Builder setIntent(@Nullable Intent intent) { + mPrimaryIntent = intent; + return this; + } + + /** + * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on + * the classified text. + * + * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener) */ public Builder setOnClickListener(@Nullable OnClickListener onClickListener) { - mOnClickListener = onClickListener; + mPrimaryOnClickListener = onClickListener; return this; } /** - * Sets information about the classifier model used to generate this TextClassification. - * @hide + * Sets a signature for the TextClassification object. + * The TextClassifier that generates the TextClassification object may use it as a way to + * internally identify the TextClassification object. */ - Builder setVersionInfo(@NonNull String versionInfo) { - mVersionInfo = Preconditions.checkNotNull(versionInfo); + public Builder setSignature(@NonNull String signature) { + mSignature = Preconditions.checkNotNull(signature); return this; } @@ -270,8 +514,39 @@ public final class TextClassification { */ public TextClassification build() { return new TextClassification( - mText, mIcon, mLabel, mIntent, mOnClickListener, mEntityConfidence, - mLogType, mVersionInfo); + mText, + mPrimaryIcon, mPrimaryLabel, + mPrimaryIntent, mPrimaryOnClickListener, + mSecondaryIcons, mSecondaryLabels, + mSecondaryIntents, mSecondaryOnClickListeners, + mEntityConfidence, mSignature); + } + } + + /** + * Optional input parameters for generating TextClassification. + */ + 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/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index c3601d9d32be..fdc9f92347db 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -23,22 +23,23 @@ import android.annotation.StringDef; import android.annotation.WorkerThread; import android.os.LocaleList; +import com.android.internal.util.Preconditions; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Interface for providing text classification related features. * - * <p>Unless otherwise stated, methods of this interface are blocking operations and you should - * avoid calling them on the UI thread. + * <p>Unless otherwise stated, methods of this interface are blocking operations. + * Avoid calling them on the UI thread. */ public interface TextClassifier { /** @hide */ - String DEFAULT_LOG_TAG = "TextClassifierImpl"; + String DEFAULT_LOG_TAG = "androidtc"; - /** @hide */ - String TYPE_UNKNOWN = ""; // TODO: Make this public API. + String TYPE_UNKNOWN = ""; String TYPE_OTHER = "other"; String TYPE_EMAIL = "email"; String TYPE_PHONE = "phone"; @@ -47,8 +48,13 @@ public interface TextClassifier { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @StringDef({ - TYPE_UNKNOWN, TYPE_OTHER, TYPE_EMAIL, TYPE_PHONE, TYPE_ADDRESS, TYPE_URL + @StringDef(prefix = { "TYPE_" }, value = { + TYPE_UNKNOWN, + TYPE_OTHER, + TYPE_EMAIL, + TYPE_PHONE, + TYPE_ADDRESS, + TYPE_URL, }) @interface EntityType {} @@ -56,47 +62,82 @@ 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(); - } + TextClassifier NO_OP = new TextClassifier() {}; - @Override - public TextClassification classifyText( - CharSequence text, int startIndex, int endIndex, LocaleList defaultLocales) { - return TextClassification.EMPTY; - } - }; + /** + * Returns suggested text selection start and end indices, recognized entity types, and their + * associated confidence scores. The entity types are ordered from highest to lowest scoring. + * + * @param text text providing context for the selected text (which is specified + * 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 options optional input parameters + * + * @throws IllegalArgumentException if text is null; selectionStartIndex is negative; + * selectionEndIndex is greater than text.length() or not greater than selectionStartIndex + * + * @see #suggestSelection(CharSequence, int, int) + */ + @WorkerThread + @NonNull + default TextSelection suggestSelection( + @NonNull CharSequence text, + @IntRange(from = 0) int selectionStartIndex, + @IntRange(from = 0) int selectionEndIndex, + @Nullable TextSelection.Options options) { + Utils.validateInput(text, selectionStartIndex, selectionEndIndex); + return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build(); + } /** - * Returns suggested text selection indices, recognized types and their associated confidence - * scores. The selections are ordered from highest to lowest scoring. + * Returns suggested text selection start and end indices, recognized entity types, and their + * associated confidence scores. The entity types are ordered from highest to lowest scoring. + * + * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls + * {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method + * calls this method, a stack overflow error will happen. * * @param text text providing context for the selected text (which is specified * 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. * * @throws IllegalArgumentException if text is null; selectionStartIndex is negative; * selectionEndIndex is greater than text.length() or not greater than selectionStartIndex + * + * @see #suggestSelection(CharSequence, int, int, TextSelection.Options) */ @WorkerThread @NonNull - TextSelection suggestSelection( + default TextSelection suggestSelection( + @NonNull CharSequence text, + @IntRange(from = 0) int selectionStartIndex, + @IntRange(from = 0) int selectionEndIndex) { + return suggestSelection(text, selectionStartIndex, selectionEndIndex, + (TextSelection.Options) null); + } + + /** + * See {@link #suggestSelection(CharSequence, int, int)} or + * {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. + * + * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls + * {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method + * calls this method, a stack overflow error will happen. + */ + @WorkerThread + @NonNull + default TextSelection suggestSelection( @NonNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, - @Nullable LocaleList defaultLocales); + @Nullable LocaleList defaultLocales) { + final TextSelection.Options options = (defaultLocales != null) + ? new TextSelection.Options().setDefaultLocales(defaultLocales) + : null; + return suggestSelection(text, selectionStartIndex, selectionEndIndex, options); + } /** * Classifies the specified text and returns a {@link TextClassification} object that can be @@ -106,41 +147,107 @@ 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 + * + * @see #classifyText(CharSequence, int, int) */ @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) { + Utils.validateInput(text, startIndex, endIndex); + return TextClassification.EMPTY; + } /** - * Returns a {@link LinksInfo} that may be applied to the text to annotate it with links + * Classifies the specified text and returns a {@link TextClassification} object that can be + * used to generate a widget for handling the classified text. + * + * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls + * {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method + * calls this method, a stack overflow error will happen. + * + * @param text text providing context for the text to classify (which is specified + * 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 + * + * @throws IllegalArgumentException if text is null; startIndex is negative; + * endIndex is greater than text.length() or not greater than startIndex + * + * @see #classifyText(CharSequence, int, int, TextClassification.Options) + */ + @WorkerThread + @NonNull + default TextClassification classifyText( + @NonNull CharSequence text, + @IntRange(from = 0) int startIndex, + @IntRange(from = 0) int endIndex) { + return classifyText(text, startIndex, endIndex, (TextClassification.Options) null); + } + + /** + * See {@link #classifyText(CharSequence, int, int, TextClassification.Options)} or + * {@link #classifyText(CharSequence, int, int)}. + * + * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls + * {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method + * calls this method, a stack overflow error will happen. + */ + @WorkerThread + @NonNull + default TextClassification classifyText( + @NonNull CharSequence text, + @IntRange(from = 0) int startIndex, + @IntRange(from = 0) int endIndex, + @Nullable LocaleList defaultLocales) { + final TextClassification.Options options = (defaultLocales != null) + ? new TextClassification.Options().setDefaultLocales(defaultLocales) + : null; + return classifyText(text, startIndex, endIndex, options); + } + + /** + * 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 * * @throws IllegalArgumentException if text is null - * @hide + * + * @see #generateLinks(CharSequence) */ @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) { + Utils.validateInput(text); + return new TextLinks.Builder(text.toString()).build(); + } + + /** + * Returns a {@link TextLinks} that may be applied to the text to annotate it with links + * information. + * + * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls + * {@link #generateLinks(CharSequence, TextLinks.Options)}. If that method calls this method, + * a stack overflow error will happen. + * + * @param text the text to generate annotations for + * + * @throws IllegalArgumentException if text is null + * + * @see #generateLinks(CharSequence, TextLinks.Options) + */ + @WorkerThread + default TextLinks generateLinks(@NonNull CharSequence text) { + return generateLinks(text, null); } /** @@ -160,4 +267,38 @@ public interface TextClassifier { default TextClassifierConstants getSettings() { return TextClassifierConstants.DEFAULT; } + + + /** + * Utility functions for TextClassifier methods. + * + * <ul> + * <li>Provides validation of input parameters to TextClassifier methods + * </ul> + * + * Intended to be used only in this package. + * @hide + */ + final class Utils { + + /** + * @throws IllegalArgumentException if text is null; startIndex is negative; + * endIndex is greater than text.length() or is not greater than startIndex; + * options is null + */ + static void validateInput( + @NonNull CharSequence text, int startIndex, int endIndex) { + Preconditions.checkArgument(text != null); + Preconditions.checkArgument(startIndex >= 0); + Preconditions.checkArgument(endIndex <= text.length()); + Preconditions.checkArgument(endIndex > startIndex); + } + + /** + * @throws IllegalArgumentException if text is null or options is null + */ + static void validateInput(@NonNull CharSequence text) { + Preconditions.checkArgument(text != null); + } + } } diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index 2e41404dde0a..d7aaee73f976 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -28,16 +28,11 @@ import android.net.Uri; import android.os.LocaleList; 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.Log; import android.util.Patterns; -import android.view.View; -import android.widget.TextViewMetrics; +import android.view.View.OnClickListener; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; @@ -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) { - validateInput(text, selectionStartIndex, selectionEndIndex); + @NonNull TextSelection.Options options) { + Utils.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) { @@ -123,8 +121,8 @@ final class TextClassifierImpl implements TextClassifier { tsBuilder.setEntityType(results[i].mCollection, results[i].mScore); } return tsBuilder - .setLogSource(LOG_TAG) - .setVersionInfo(getVersionInfo()) + .setSignature( + getSignature(string, selectionStartIndex, selectionEndIndex)) .build(); } else { // We can not trust the result. Log the issue and ignore the result. @@ -139,49 +137,59 @@ 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 TextClassification classifyText( @NonNull CharSequence text, int startIndex, int endIndex, - @Nullable LocaleList defaultLocales) { - validateInput(text, startIndex, endIndex); + @NonNull TextClassification.Options options) { + Utils.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) { final TextClassification classificationResult = - createClassificationResult( - results, string.subSequence(startIndex, endIndex)); + createClassificationResult(results, string, startIndex, endIndex); return classificationResult; } } } catch (Throwable t) { // Avoid throwing from this method. Log the error. - Log.e(LOG_TAG, "Error getting assist info.", t); + 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 TextLinks generateLinks( + @NonNull CharSequence text, @NonNull TextLinks.Options options) { + Utils.validateInput(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 +218,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; } @@ -218,31 +228,39 @@ final class TextClassifierImpl implements TextClassifier { } } - @NonNull - private String getVersionInfo() { + private String getSignature(String text, int start, int end) { synchronized (mSmartSelectionLock) { - if (mLocale != null) { - return String.format("%s_v%d", mLocale.toLanguageTag(), mVersion); - } - return ""; + final String versionInfo = (mLocale != null) + ? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion) + : ""; + final int hash = Objects.hash(text, start, end, mContext.getPackageName()); + return String.format(Locale.US, "%s|%s|%d", LOG_TAG, versionInfo, hash); } } @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 +296,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; } } @@ -356,9 +370,11 @@ final class TextClassifierImpl implements TextClassifier { } private TextClassification createClassificationResult( - SmartSelection.ClassificationResult[] classifications, CharSequence text) { + SmartSelection.ClassificationResult[] classifications, + String text, int start, int end) { + final String classifiedText = text.substring(start, end); final TextClassification.Builder builder = new TextClassification.Builder() - .setText(text.toString()); + .setText(classifiedText); final int size = classifications.length; for (int i = 0; i < size; i++) { @@ -366,43 +382,55 @@ final class TextClassifierImpl implements TextClassifier { } final String type = getHighestScoringType(classifications); - builder.setLogType(IntentFactory.getLogType(type)); - - final Intent intent = IntentFactory.create(mContext, type, text.toString()); - final PackageManager pm; - final ResolveInfo resolveInfo; - if (intent != null) { - pm = mContext.getPackageManager(); - resolveInfo = pm.resolveActivity(intent, 0); - } else { - pm = null; - resolveInfo = null; - } - if (resolveInfo != null && resolveInfo.activityInfo != null) { - builder.setIntent(intent) - .setOnClickListener(TextClassification.createStartActivityOnClickListener( - mContext, intent)); - - final String packageName = resolveInfo.activityInfo.packageName; - if ("android".equals(packageName)) { - // Requires the chooser to find an activity to handle the intent. - builder.setLabel(IntentFactory.getLabel(mContext, type)); + addActions(builder, IntentFactory.create(mContext, type, classifiedText)); + + return builder.setSignature(getSignature(text, start, end)).build(); + } + + /** Extends the classification with the intents that can be resolved. */ + private void addActions( + TextClassification.Builder builder, List<Intent> intents) { + final PackageManager pm = mContext.getPackageManager(); + final int size = intents.size(); + for (int i = 0; i < size; i++) { + final Intent intent = intents.get(i); + final ResolveInfo resolveInfo; + if (intent != null) { + resolveInfo = pm.resolveActivity(intent, 0); } else { - // A default activity will handle the intent. - intent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name)); - Drawable icon = resolveInfo.activityInfo.loadIcon(pm); - if (icon == null) { - icon = resolveInfo.loadIcon(pm); + resolveInfo = null; + } + if (resolveInfo != null && resolveInfo.activityInfo != null) { + final String packageName = resolveInfo.activityInfo.packageName; + CharSequence label; + Drawable icon; + if ("android".equals(packageName)) { + // Requires the chooser to find an activity to handle the intent. + label = IntentFactory.getLabel(mContext, intent); + icon = null; + } else { + // A default activity will handle the intent. + intent.setComponent( + new ComponentName(packageName, resolveInfo.activityInfo.name)); + icon = resolveInfo.activityInfo.loadIcon(pm); + if (icon == null) { + icon = resolveInfo.loadIcon(pm); + } + label = resolveInfo.activityInfo.loadLabel(pm); + if (label == null) { + label = resolveInfo.loadLabel(pm); + } } - builder.setIcon(icon); - CharSequence label = resolveInfo.activityInfo.loadLabel(pm); - if (label == null) { - label = resolveInfo.loadLabel(pm); + final String labelString = (label != null) ? label.toString() : null; + final OnClickListener onClickListener = + TextClassification.createStartActivityOnClickListener(mContext, intent); + if (i == 0) { + builder.setPrimaryAction(intent, labelString, icon, onClickListener); + } else { + builder.addSecondaryAction(intent, labelString, icon, onClickListener); } - builder.setLabel(label != null ? label.toString() : null); } } - return builder.setVersionInfo(getVersionInfo()).build(); } private static int getHintFlags(CharSequence text, int start, int end) { @@ -447,210 +475,38 @@ final class TextClassifierImpl implements TextClassifier { } /** - * @throws IllegalArgumentException if text is null; startIndex is negative; - * endIndex is greater than text.length() or is not greater than startIndex - */ - private static void validateInput(@NonNull CharSequence text, int startIndex, int endIndex) { - Preconditions.checkArgument(text != null); - Preconditions.checkArgument(startIndex >= 0); - Preconditions.checkArgument(endIndex <= text.length()); - Preconditions.checkArgument(endIndex > startIndex); - } - - /** - * 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)) { - final Intent intent = IntentFactory.create( - context, type, text.substring(selectionStart, selectionEnd)); - if (hasActivityHandler(context, intent)) { - final ClickableSpan span = createSpan(context, intent); - 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, @Nullable 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 { private IntentFactory() {} - @Nullable - public static Intent create(Context context, String type, String text) { + @NonNull + public static List<Intent> create(Context context, String type, String text) { + final List<Intent> intents = new ArrayList<>(); type = type.trim().toLowerCase(Locale.ENGLISH); text = text.trim(); switch (type) { case TextClassifier.TYPE_EMAIL: - return new Intent(Intent.ACTION_SENDTO) - .setData(Uri.parse(String.format("mailto:%s", text))); + 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)); + break; case TextClassifier.TYPE_PHONE: - return new Intent(Intent.ACTION_DIAL) - .setData(Uri.parse(String.format("tel:%s", text))); + intents.add(new Intent(Intent.ACTION_DIAL) + .setData(Uri.parse(String.format("tel:%s", text)))); + intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) + .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) + .putExtra(ContactsContract.Intents.Insert.PHONE, text)); + intents.add(new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse(String.format("smsto:%s", text)))); + break; case TextClassifier.TYPE_ADDRESS: - return new Intent(Intent.ACTION_VIEW) - .setData(Uri.parse(String.format("geo:0,0?q=%s", text))); + intents.add(new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(String.format("geo:0,0?q=%s", text)))); + break; case TextClassifier.TYPE_URL: final String httpPrefix = "http://"; final String httpsPrefix = "https://"; @@ -661,45 +517,50 @@ final class TextClassifierImpl implements TextClassifier { } else { text = httpPrefix + text; } - return new Intent(Intent.ACTION_VIEW, Uri.parse(text)) - .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); - default: - return null; + intents.add(new Intent(Intent.ACTION_VIEW, Uri.parse(text)) + .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName())); + break; } + return intents; } @Nullable - public static String getLabel(Context context, String type) { - type = type.trim().toLowerCase(Locale.ENGLISH); - switch (type) { - case TextClassifier.TYPE_EMAIL: - return context.getString(com.android.internal.R.string.email); - case TextClassifier.TYPE_PHONE: + public static String getLabel(Context context, @Nullable Intent intent) { + if (intent == null || intent.getAction() == null) { + return null; + } + switch (intent.getAction()) { + case Intent.ACTION_DIAL: return context.getString(com.android.internal.R.string.dial); - case TextClassifier.TYPE_ADDRESS: - return context.getString(com.android.internal.R.string.map); - case TextClassifier.TYPE_URL: - return context.getString(com.android.internal.R.string.browse); + case Intent.ACTION_SENDTO: + switch (intent.getScheme()) { + case "mailto": + return context.getString(com.android.internal.R.string.email); + case "smsto": + return context.getString(com.android.internal.R.string.sms); + default: + return null; + } + case Intent.ACTION_INSERT_OR_EDIT: + switch (intent.getDataString()) { + case ContactsContract.Contacts.CONTENT_ITEM_TYPE: + return context.getString(com.android.internal.R.string.add_contact); + default: + return null; + } + case Intent.ACTION_VIEW: + switch (intent.getScheme()) { + case "geo": + return context.getString(com.android.internal.R.string.map); + case "http": // fall through + case "https": + return context.getString(com.android.internal.R.string.browse); + default: + return null; + } default: return null; } } - - @Nullable - public static int getLogType(String type) { - type = type.trim().toLowerCase(Locale.ENGLISH); - switch (type) { - case TextClassifier.TYPE_EMAIL: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_EMAIL; - case TextClassifier.TYPE_PHONE: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_PHONE; - case TextClassifier.TYPE_ADDRESS: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_ADDRESS; - case TextClassifier.TYPE_URL: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_URL; - default: - return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_OTHER; - } - } } } diff --git a/core/java/android/view/textclassifier/TextLinks.java b/core/java/android/view/textclassifier/TextLinks.java new file mode 100644 index 000000000000..0e039e35367e --- /dev/null +++ b/core/java/android/view/textclassifier/TextLinks.java @@ -0,0 +1,234 @@ +/* + * 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<>(entityScores); + } + + /** + * 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 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; + } + } + + /** + * 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 = + 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/core/java/android/view/textclassifier/TextSelection.java b/core/java/android/view/textclassifier/TextSelection.java index 11ebe8359b9c..25e9e7ecf0e7 100644 --- a/core/java/android/view/textclassifier/TextSelection.java +++ b/core/java/android/view/textclassifier/TextSelection.java @@ -19,11 +19,15 @@ 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.util.ArrayMap; import android.view.textclassifier.TextClassifier.EntityType; import com.android.internal.util.Preconditions; -import java.util.List; +import java.util.Locale; +import java.util.Map; /** * Information about where text selection should be. @@ -33,19 +37,15 @@ public final class TextSelection { private final int mStartIndex; private final int mEndIndex; @NonNull private final EntityConfidence<String> mEntityConfidence; - @NonNull private final List<String> mEntities; - @NonNull private final String mLogSource; - @NonNull private final String mVersionInfo; + @NonNull private final String mSignature; private TextSelection( - int startIndex, int endIndex, @NonNull EntityConfidence<String> entityConfidence, - @NonNull String logSource, @NonNull String versionInfo) { + int startIndex, int endIndex, @NonNull Map<String, Float> entityConfidence, + @NonNull String signature) { mStartIndex = startIndex; mEndIndex = endIndex; mEntityConfidence = new EntityConfidence<>(entityConfidence); - mEntities = mEntityConfidence.getEntities(); - mLogSource = logSource; - mVersionInfo = versionInfo; + mSignature = signature; } /** @@ -67,7 +67,7 @@ public final class TextSelection { */ @IntRange(from = 0) public int getEntityCount() { - return mEntities.size(); + return mEntityConfidence.getEntities().size(); } /** @@ -79,7 +79,7 @@ public final class TextSelection { */ @NonNull public @EntityType String getEntity(int index) { - return mEntities.get(index); + return mEntityConfidence.getEntities().get(index); } /** @@ -93,27 +93,21 @@ public final class TextSelection { } /** - * Returns a tag for the source classifier used to generate this result. - * @hide + * Returns the signature for this object. + * The TextClassifier that generates this object may use it as a way to internally identify + * this object. */ @NonNull - public String getSourceClassifier() { - return mLogSource; - } - - /** - * Returns information about the classifier model used to generate this TextSelection. - * @hide - */ - @NonNull - public String getVersionInfo() { - return mVersionInfo; + public String getSignature() { + return mSignature; } @Override public String toString() { - return String.format("TextSelection {%d, %d, %s}", - mStartIndex, mEndIndex, mEntityConfidence); + return String.format( + Locale.US, + "TextSelection {startIndex=%d, endIndex=%d, entities=%s, signature=%s}", + mStartIndex, mEndIndex, mEntityConfidence, mSignature); } /** @@ -123,10 +117,8 @@ public final class TextSelection { private final int mStartIndex; private final int mEndIndex; - @NonNull private final EntityConfidence<String> mEntityConfidence = - new EntityConfidence<>(); - @NonNull private String mLogSource = ""; - @NonNull private String mVersionInfo = ""; + @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>(); + @NonNull private String mSignature = ""; /** * Creates a builder used to build {@link TextSelection} objects. @@ -151,34 +143,78 @@ public final class TextSelection { public Builder setEntityType( @NonNull @EntityType String type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { - mEntityConfidence.setEntityType(type, confidenceScore); + mEntityConfidence.put(type, confidenceScore); return this; } /** - * Sets a tag for the source classifier used to generate this result. - * @hide + * Sets a signature for the TextSelection object. + * + * The TextClassifier that generates the TextSelection object may use it as a way to + * internally identify the TextSelection object. */ - Builder setLogSource(@NonNull String logSource) { - mLogSource = Preconditions.checkNotNull(logSource); + public Builder setSignature(@NonNull String signature) { + mSignature = Preconditions.checkNotNull(signature); return this; } /** - * Sets information about the classifier model used to generate this TextSelection. - * @hide + * Builds and returns {@link TextSelection} object. */ - Builder setVersionInfo(@NonNull String versionInfo) { - mVersionInfo = Preconditions.checkNotNull(versionInfo); + public TextSelection build() { + return new TextSelection( + mStartIndex, mEndIndex, mEntityConfidence, mSignature); + } + } + + /** + * Optional input parameters for generating TextSelection. + */ + 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; } /** - * Builds and returns {@link TextSelection} object. + * @return ordered list of locale preferences that can be used to disambiguate + * the provided text. */ - public TextSelection build() { - return new TextSelection( - mStartIndex, mEndIndex, mEntityConfidence, mLogSource, mVersionInfo); + @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/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java index fb870bd3b9bd..157b3d82163b 100644 --- a/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java +++ b/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java @@ -43,36 +43,50 @@ import java.util.UUID; public final class SmartSelectionEventTracker { private static final String LOG_TAG = "SmartSelectEventTracker"; - private static final boolean DEBUG_LOG_ENABLED = false; + private static final boolean DEBUG_LOG_ENABLED = true; 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,15 +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()) - .setTimestamp(now) .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); @@ -170,7 +196,7 @@ public final class SmartSelectionEventTracker { mSessionStartTime = 0; mLastEventTime = 0; mSmartSelectionTriggered = false; - mVersionTag = getVersionTag(null); + mModelName = getModelName(null); mSessionId = null; } @@ -252,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)); } /** @@ -370,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 = ""; @@ -485,7 +473,7 @@ public final class SmartSelectionEventTracker { final String entityType = classification.getEntityCount() > 0 ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = classification.getVersionInfo(); + final String versionTag = getVersionInfo(classification.getSignature()); return new SelectionEvent( start, end, EventType.SELECTION_MODIFIED, entityType, versionTag); } @@ -501,7 +489,7 @@ public final class SmartSelectionEventTracker { */ public static SelectionEvent selectionModified( int start, int end, @NonNull TextSelection selection) { - final boolean smartSelection = selection.getSourceClassifier() + final boolean smartSelection = getSourceClassifier(selection.getSignature()) .equals(TextClassifier.DEFAULT_LOG_TAG); final int eventType; if (smartSelection) { @@ -515,7 +503,7 @@ public final class SmartSelectionEventTracker { final String entityType = selection.getEntityCount() > 0 ? selection.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = selection.getVersionInfo(); + final String versionTag = getVersionInfo(selection.getSignature()); return new SelectionEvent(start, end, eventType, entityType, versionTag); } @@ -550,26 +538,25 @@ public final class SmartSelectionEventTracker { final String entityType = classification.getEntityCount() > 0 ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = classification.getVersionInfo(); + final String versionTag = getVersionInfo(classification.getSignature()); return new SelectionEvent(start, end, actionType, entityType, versionTag); } - private boolean isActionType() { - switch (mEventType) { - case ActionType.OVERTYPE: // fall through - case ActionType.COPY: // fall through - case ActionType.PASTE: // fall through - case ActionType.CUT: // fall through - case ActionType.SHARE: // fall through - case ActionType.SMART_SHARE: // fall through - case ActionType.DRAG: // fall through - case ActionType.ABANDON: // fall through - case ActionType.SELECT_ALL: // fall through - case ActionType.RESET: // fall through - return true; - default: - return false; + private static String getVersionInfo(String signature) { + final int start = signature.indexOf("|"); + final int end = signature.indexOf("|", start); + if (start >= 0 && end >= start) { + return signature.substring(start, end); + } + return ""; + } + + private static String getSourceClassifier(String signature) { + final int end = signature.indexOf("|"); + if (end >= 0) { + return signature.substring(0, end); } + return ""; } private boolean isTerminal() { diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index 45e6eb308788..fc76029a8bef 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -16,13 +16,14 @@ package android.webkit; +import android.annotation.Nullable; + import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Map; - /** * Manages the HTTP cache used by an application's {@link WebView} instances. * @deprecated Access to the HTTP cache will be removed in a future release. @@ -230,9 +231,10 @@ public final class CacheManager { * {@link CacheManager.CacheResult#getLocalPath CacheManager.CacheResult.getLocalPath()}. * * @return the base directory of the cache - * @deprecated This method no longer has any effect and always returns null. + * @deprecated This method no longer has any effect and always returns {@code null}. */ @Deprecated + @Nullable public static File getCacheFileBaseDir() { return null; } @@ -240,8 +242,8 @@ public final class CacheManager { /** * Gets whether the HTTP cache is disabled. * - * @return true if the HTTP cache is disabled - * @deprecated This method no longer has any effect and always returns false. + * @return {@code true} if the HTTP cache is disabled + * @deprecated This method no longer has any effect and always returns {@code false}. */ @Deprecated public static boolean cacheDisabled() { @@ -249,12 +251,12 @@ public final class CacheManager { } /** - * Starts a cache transaction. Returns true if this is the only running + * Starts a cache transaction. Returns {@code true} if this is the only running * transaction. Otherwise, this transaction is nested inside currently - * running transactions and false is returned. + * running transactions and {@code false} is returned. * - * @return true if this is the only running transaction - * @deprecated This method no longer has any effect and always returns false. + * @return {@code true} if this is the only running transaction + * @deprecated This method no longer has any effect and always returns {@code false}. */ @Deprecated public static boolean startCacheTransaction() { @@ -265,8 +267,8 @@ public final class CacheManager { * Ends the innermost cache transaction and returns whether this was the * only running transaction. * - * @return true if this was the only running transaction - * @deprecated This method no longer has any effect and always returns false. + * @return {@code true} if this was the only running transaction + * @deprecated This method no longer has any effect and always returns {@code false}. */ @Deprecated public static boolean endCacheTransaction() { @@ -274,7 +276,7 @@ public final class CacheManager { } /** - * Gets the cache entry for the specified URL, or null if none is found. + * Gets the cache entry for the specified URL, or {@code null} if none is found. * If a non-null value is provided for the HTTP headers map, and the cache * entry needs validation, appropriate headers will be added to the map. * The input stream of the CacheEntry object should be closed by the caller @@ -284,9 +286,10 @@ public final class CacheManager { * @param headers a map from HTTP header name to value, to be populated * for the returned cache entry * @return the cache entry for the specified URL - * @deprecated This method no longer has any effect and always returns null. + * @deprecated This method no longer has any effect and always returns {@code null}. */ @Deprecated + @Nullable public static CacheResult getCacheFile(String url, Map<String, String> headers) { return null; diff --git a/core/java/android/webkit/ClientCertRequest.java b/core/java/android/webkit/ClientCertRequest.java index 4a7f5fdf1f66..0fc47f1ed0ff 100644 --- a/core/java/android/webkit/ClientCertRequest.java +++ b/core/java/android/webkit/ClientCertRequest.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.Nullable; + import java.security.Principal; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -42,14 +44,16 @@ public abstract class ClientCertRequest { public ClientCertRequest() { } /** - * Returns the acceptable types of asymmetric keys (can be null). + * Returns the acceptable types of asymmetric keys. */ + @Nullable public abstract String[] getKeyTypes(); /** * Returns the acceptable certificate issuers for the certificate - * matching the private key (can be null). + * matching the private key. */ + @Nullable public abstract Principal[] getPrincipals(); /** diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java index 67289c28e7a1..ae6a2fd787d7 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.net.WebAddress; @@ -42,9 +43,9 @@ public abstract class CookieManager { /** * Sets whether the application's {@link WebView} instances should send and * accept cookies. - * By default this is set to true and the WebView accepts cookies. + * By default this is set to {@code true} and the WebView accepts cookies. * <p> - * When this is true + * When this is {@code true} * {@link CookieManager#setAcceptThirdPartyCookies setAcceptThirdPartyCookies} and * {@link CookieManager#setAcceptFileSchemeCookies setAcceptFileSchemeCookies} * can be used to control the policy for those specific types of cookie. @@ -58,7 +59,7 @@ public abstract class CookieManager { * Gets whether the application's {@link WebView} instances send and accept * cookies. * - * @return true if {@link WebView} instances send and accept cookies + * @return {@code true} if {@link WebView} instances send and accept cookies */ public abstract boolean acceptCookie(); @@ -82,7 +83,7 @@ public abstract class CookieManager { * Gets whether the {@link WebView} should allow third party cookies to be set. * * @param webview the {@link WebView} instance to get the cookie policy for - * @return true if the {@link WebView} accepts third party cookies + * @return {@code true} if the {@link WebView} accepts third party cookies */ public abstract boolean acceptThirdPartyCookies(WebView webview); @@ -116,7 +117,8 @@ public abstract class CookieManager { * HTTP response header * @param callback a callback to be executed when the cookie has been set */ - public abstract void setCookie(String url, String value, ValueCallback<Boolean> callback); + public abstract void setCookie(String url, String value, @Nullable ValueCallback<Boolean> + callback); /** * Gets the cookies for the given URL. @@ -175,7 +177,7 @@ public abstract class CookieManager { * method from a thread without a Looper. * @param callback a callback which is executed when the session cookies have been removed */ - public abstract void removeSessionCookies(ValueCallback<Boolean> callback); + public abstract void removeSessionCookies(@Nullable ValueCallback<Boolean> callback); /** * Removes all cookies. @@ -197,12 +199,12 @@ public abstract class CookieManager { * method from a thread without a Looper. * @param callback a callback which is executed when the cookies have been removed */ - public abstract void removeAllCookies(ValueCallback<Boolean> callback); + public abstract void removeAllCookies(@Nullable ValueCallback<Boolean> callback); /** * Gets whether there are stored cookies. * - * @return true if there are stored cookies + * @return {@code true} if there are stored cookies */ public abstract boolean hasCookies(); @@ -233,7 +235,7 @@ public abstract class CookieManager { * Gets whether the application's {@link WebView} instances send and accept * cookies for file scheme URLs. * - * @return true if {@link WebView} instances send and accept cookies for + * @return {@code true} if {@link WebView} instances send and accept cookies for * file scheme URLs */ // Static for backward compatibility. diff --git a/core/java/android/webkit/FindActionModeCallback.java b/core/java/android/webkit/FindActionModeCallback.java index f31389350519..e011d51c51fd 100644 --- a/core/java/android/webkit/FindActionModeCallback.java +++ b/core/java/android/webkit/FindActionModeCallback.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.NonNull; import android.annotation.SystemApi; import android.content.Context; import android.content.res.Resources; @@ -69,7 +70,7 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, mActionMode.finish(); } - /* + /** * Place text in the text field so it can be searched for. Need to press * the find next or find previous button to find all of the matches. */ @@ -87,10 +88,12 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, mMatchesFound = false; } - /* - * Set the WebView to search. Must be non null. + /** + * Set the WebView to search. + * + * @param webView an implementation of WebView */ - public void setWebView(WebView webView) { + public void setWebView(@NonNull WebView webView) { if (null == webView) { throw new AssertionError("WebView supplied to " + "FindActionModeCallback cannot be null"); @@ -107,10 +110,10 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, } } - /* + /** * Move the highlight to the next match. - * @param next If true, find the next match further down in the document. - * If false, find the previous match, up in the document. + * @param next If {@code true}, find the next match further down in the document. + * If {@code false}, find the previous match, up in the document. */ private void findNext(boolean next) { if (mWebView == null) { @@ -130,7 +133,7 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, updateMatchesString(); } - /* + /** * Highlight all the instances of the string from mEditText in mWebView. */ public void findAll() { @@ -169,7 +172,7 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, } } - /* + /** * Update the string which tells the user how many matches were found, and * which match is currently highlighted. */ diff --git a/core/java/android/webkit/HttpAuthHandler.java b/core/java/android/webkit/HttpAuthHandler.java index 45fc1f5204fd..5353bc6983ee 100644 --- a/core/java/android/webkit/HttpAuthHandler.java +++ b/core/java/android/webkit/HttpAuthHandler.java @@ -61,14 +61,4 @@ public class HttpAuthHandler extends Handler { */ public void proceed(String username, String password) { } - - /** - * Gets whether the prompt dialog should be suppressed. - * - * @return whether the prompt dialog should be suppressed - * @hide - */ - public boolean suppressDialog() { - return false; - } } diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index da8901a0e633..386169528e89 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -16,10 +16,13 @@ package android.webkit; +import android.annotation.Nullable; import android.text.TextUtils; -import java.util.regex.Pattern; + import libcore.net.MimeUtils; +import java.util.regex.Pattern; + /** * Two-way map that maps MIME-types to file extensions and vice versa. * @@ -34,7 +37,7 @@ public class MimeTypeMap { } /** - * Returns the file extension or an empty string iff there is no + * Returns the file extension or an empty string if there is no * extension. This method is a convenience method for obtaining the * extension of a url and has undefined results for other Strings. * @param url @@ -71,9 +74,9 @@ public class MimeTypeMap { } /** - * Return true if the given MIME type has an entry in the map. + * Return {@code true} if the given MIME type has an entry in the map. * @param mimeType A MIME type (i.e. text/plain) - * @return True iff there is a mimeType entry in the map. + * @return {@code true} if there is a mimeType entry in the map. */ public boolean hasMimeType(String mimeType) { return MimeUtils.hasMimeType(mimeType); @@ -82,8 +85,9 @@ public class MimeTypeMap { /** * Return the MIME type for the given extension. * @param extension A file extension without the leading '.' - * @return The MIME type for the given extension or null iff there is none. + * @return The MIME type for the given extension or {@code null} if there is none. */ + @Nullable public String getMimeTypeFromExtension(String extension) { return MimeUtils.guessMimeTypeFromExtension(extension); } @@ -94,9 +98,9 @@ public class MimeTypeMap { } /** - * Return true if the given extension has a registered MIME type. + * Return {@code true} if the given extension has a registered MIME type. * @param extension A file extension without the leading '.' - * @return True iff there is an extension entry in the map. + * @return {@code true} if there is an extension entry in the map. */ public boolean hasExtension(String extension) { return MimeUtils.hasExtension(extension); @@ -107,14 +111,15 @@ public class MimeTypeMap { * MIME types map to multiple extensions. This call will return the most * common extension for the given MIME type. * @param mimeType A MIME type (i.e. text/plain) - * @return The extension for the given MIME type or null iff there is none. + * @return The extension for the given MIME type or {@code null} if there is none. */ + @Nullable public String getExtensionFromMimeType(String mimeType) { return MimeUtils.guessExtensionFromMimeType(mimeType); } /** - * If the given MIME type is null, or one of the "generic" types (text/plain + * If the given MIME type is {@code null}, or one of the "generic" types (text/plain * or application/octet-stream) map it to a type that Android can deal with. * If the given type is not generic, return it unchanged. * @@ -123,7 +128,7 @@ public class MimeTypeMap { * @param contentDisposition Content-disposition header given by the server. * @return The MIME type that should be used for this data. */ - /* package */ String remapGenericMimeType(String mimeType, String url, + /* package */ String remapGenericMimeType(@Nullable String mimeType, String url, String contentDisposition) { // If we have one of "generic" MIME types, try to deduce // the right MIME type from the file extension (if any): diff --git a/core/java/android/webkit/Plugin.java b/core/java/android/webkit/Plugin.java index 072e02aacbc6..29a5edc3cd12 100644 --- a/core/java/android/webkit/Plugin.java +++ b/core/java/android/webkit/Plugin.java @@ -16,12 +16,12 @@ package android.webkit; -import com.android.internal.R; - import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import com.android.internal.R; + /** * Represents a plugin (Java equivalent of the PluginPackageAndroid * C++ class in libs/WebKitLib/WebKit/WebCore/plugins/android/) @@ -32,13 +32,13 @@ import android.content.DialogInterface; */ @Deprecated public class Plugin { - /* + /** * @hide * @deprecated This interface was intended to be used by Gears. Since Gears was * deprecated, so is this class. */ public interface PreferencesClickHandler { - /* + /** * @hide * @deprecated This interface was intended to be used by Gears. Since Gears was * deprecated, so is this class. diff --git a/core/java/android/webkit/RenderProcessGoneDetail.java b/core/java/android/webkit/RenderProcessGoneDetail.java index 1c793993909d..0843e26ea19c 100644 --- a/core/java/android/webkit/RenderProcessGoneDetail.java +++ b/core/java/android/webkit/RenderProcessGoneDetail.java @@ -28,7 +28,7 @@ public abstract class RenderProcessGoneDetail { * If the render process was killed, this is most likely caused by the * system being low on memory. * - * @return True if render process crashed, otherwise it was killed by + * @return {@code true} if render process crashed, otherwise it was killed by * system. **/ public abstract boolean didCrash(); diff --git a/core/java/android/webkit/SafeBrowsingResponse.java b/core/java/android/webkit/SafeBrowsingResponse.java index 0d0f1cce2dfc..1d3a617a6bec 100644 --- a/core/java/android/webkit/SafeBrowsingResponse.java +++ b/core/java/android/webkit/SafeBrowsingResponse.java @@ -25,28 +25,27 @@ package android.webkit; * <p> * If reporting is enabled, all reports will be sent according to the privacy policy referenced by * {@link android.webkit.WebView#getSafeBrowsingPrivacyPolicyUrl()}. - * </p> */ public abstract class SafeBrowsingResponse { /** * Display the default interstitial. * - * @param allowReporting True if the interstitial should show a reporting checkbox. + * @param allowReporting {@code true} if the interstitial should show a reporting checkbox. */ public abstract void showInterstitial(boolean allowReporting); /** * Act as if the user clicked "visit this unsafe site." * - * @param report True to enable Safe Browsing reporting. + * @param report {@code true} to enable Safe Browsing reporting. */ public abstract void proceed(boolean report); /** * Act as if the user clicked "back to safety." * - * @param report True to enable Safe Browsing reporting. + * @param report {@code true} to enable Safe Browsing reporting. */ public abstract void backToSafety(boolean report); } diff --git a/core/java/android/webkit/ServiceWorkerClient.java b/core/java/android/webkit/ServiceWorkerClient.java index 23340550def3..9124c8554a26 100644 --- a/core/java/android/webkit/ServiceWorkerClient.java +++ b/core/java/android/webkit/ServiceWorkerClient.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.Nullable; + /** * Base class for clients to capture Service Worker related callbacks, * see {@link ServiceWorkerController} for usage example. @@ -24,19 +26,20 @@ public class ServiceWorkerClient { /** * Notify the host application of a resource request and allow the - * application to return the data. If the return value is null, the + * 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 - * response information or null if the WebView should load the + * response information or {@code null} if the WebView should load the * resource itself. * @see WebViewClient#shouldInterceptRequest(WebView, WebResourceRequest) */ + @Nullable public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { return null; } diff --git a/core/java/android/webkit/ServiceWorkerController.java b/core/java/android/webkit/ServiceWorkerController.java index 571d45e22d3b..3517c74b680e 100644 --- a/core/java/android/webkit/ServiceWorkerController.java +++ b/core/java/android/webkit/ServiceWorkerController.java @@ -33,7 +33,7 @@ import android.annotation.Nullable; * return null; * } * }); - * </pre></p> + * </pre> */ public abstract class ServiceWorkerController { diff --git a/core/java/android/webkit/ServiceWorkerWebSettings.java b/core/java/android/webkit/ServiceWorkerWebSettings.java index 92e9fbe29b55..25058da4cd92 100644 --- a/core/java/android/webkit/ServiceWorkerWebSettings.java +++ b/core/java/android/webkit/ServiceWorkerWebSettings.java @@ -76,14 +76,15 @@ public abstract class ServiceWorkerWebSettings { * Sets whether Service Workers should not load resources from the network, * see {@link WebSettings#setBlockNetworkLoads}. * - * @param flag true means block network loads by the Service Workers + * @param flag {@code true} means block network loads by the Service Workers */ public abstract void setBlockNetworkLoads(boolean flag); /** * Gets whether Service Workers are prohibited from loading any resources from the network. * - * @return true if the Service Workers are not allowed to load any resources from the network + * @return {@code true} if the Service Workers are not allowed to load any resources from the + * network * @see #setBlockNetworkLoads */ public abstract boolean getBlockNetworkLoads(); diff --git a/core/java/android/webkit/TokenBindingService.java b/core/java/android/webkit/TokenBindingService.java index f7caac7d5254..b37e1b8962c5 100644 --- a/core/java/android/webkit/TokenBindingService.java +++ b/core/java/android/webkit/TokenBindingService.java @@ -16,11 +16,12 @@ package android.webkit; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.net.Uri; import java.security.KeyPair; -import java.security.spec.AlgorithmParameterSpec; /** * Enables the token binding procotol, and provides access to the keys. See @@ -82,34 +83,33 @@ public abstract class TokenBindingService { * If no key pair exists, WebView chooses an algorithm from the list, in * the order given, to generate a key. * - * The user can pass a null if any algorithm is acceptable. + * The user can pass {@code null} if any algorithm is acceptable. * * @param origin The origin for the server. - * @param algorithm The list of algorithms. Can be null. An - * IllegalArgumentException is thrown if array is empty. + * @param algorithm The list of algorithms. An IllegalArgumentException is thrown if array is + * empty. * @param callback The callback that will be called when key is available. - * Cannot be null. */ public abstract void getKey(Uri origin, - String[] algorithm, - ValueCallback<TokenBindingKey> callback); + @Nullable String[] algorithm, + @NonNull ValueCallback<TokenBindingKey> callback); /** * Deletes specified key (for use when associated cookie is cleared). * * @param origin The origin of the server. * @param callback The callback that will be called when key is deleted. The * callback parameter (Boolean) will indicate if operation is - * successful or if failed. The callback can be null. + * successful or if failed. */ public abstract void deleteKey(Uri origin, - ValueCallback<Boolean> callback); + @Nullable ValueCallback<Boolean> callback); /** * Deletes all the keys (for use when cookies are cleared). * * @param callback The callback that will be called when keys are deleted. * The callback parameter (Boolean) will indicate if operation is - * successful or if failed. The callback can be null. + * successful or if failed. */ - public abstract void deleteAllKeys(ValueCallback<Boolean> callback); + public abstract void deleteAllKeys(@Nullable ValueCallback<Boolean> callback); } diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index f5233b611dce..84c000a379df 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -16,16 +16,17 @@ package android.webkit; +import android.annotation.Nullable; +import android.net.ParseException; +import android.net.Uri; +import android.net.WebAddress; +import android.util.Log; + import java.io.UnsupportedEncodingException; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -import android.net.Uri; -import android.net.ParseException; -import android.net.WebAddress; -import android.util.Log; - public final class URLUtil { private static final String LOGTAG = "webkit"; @@ -136,7 +137,7 @@ public final class URLUtil { } /** - * @return True iff the url is correctly URL encoded + * @return {@code true} if the url is correctly URL encoded */ static boolean verifyURLEncoding(String url) { int count = url.length(); @@ -170,14 +171,14 @@ public final class URLUtil { } /** - * @return True iff the url is an asset file. + * @return {@code true} if the url is an asset file. */ public static boolean isAssetUrl(String url) { return (null != url) && url.startsWith(ASSET_BASE); } /** - * @return True iff the url is a resource file. + * @return {@code true} if the url is a resource file. * @hide */ public static boolean isResourceUrl(String url) { @@ -185,7 +186,7 @@ public final class URLUtil { } /** - * @return True iff the url is a proxy url to allow cookieless network + * @return {@code true} if the url is a proxy url to allow cookieless network * requests from a file url. * @deprecated Cookieless proxy is no longer supported. */ @@ -195,7 +196,7 @@ public final class URLUtil { } /** - * @return True iff the url is a local file. + * @return {@code true} if the url is a local file. */ public static boolean isFileUrl(String url) { return (null != url) && (url.startsWith(FILE_BASE) && @@ -204,28 +205,28 @@ public final class URLUtil { } /** - * @return True iff the url is an about: url. + * @return {@code true} if the url is an about: url. */ public static boolean isAboutUrl(String url) { return (null != url) && url.startsWith("about:"); } /** - * @return True iff the url is a data: url. + * @return {@code true} if the url is a data: url. */ public static boolean isDataUrl(String url) { return (null != url) && url.startsWith("data:"); } /** - * @return True iff the url is a javascript: url. + * @return {@code true} if the url is a javascript: url. */ public static boolean isJavaScriptUrl(String url) { return (null != url) && url.startsWith("javascript:"); } /** - * @return True iff the url is an http: url. + * @return {@code true} if the url is an http: url. */ public static boolean isHttpUrl(String url) { return (null != url) && @@ -234,7 +235,7 @@ public final class URLUtil { } /** - * @return True iff the url is an https: url. + * @return {@code true} if the url is an https: url. */ public static boolean isHttpsUrl(String url) { return (null != url) && @@ -243,7 +244,7 @@ public final class URLUtil { } /** - * @return True iff the url is a network url. + * @return {@code true} if the url is a network url. */ public static boolean isNetworkUrl(String url) { if (url == null || url.length() == 0) { @@ -253,14 +254,14 @@ public final class URLUtil { } /** - * @return True iff the url is a content: url. + * @return {@code true} if the url is a content: url. */ public static boolean isContentUrl(String url) { return (null != url) && url.startsWith(CONTENT_BASE); } /** - * @return True iff the url is valid. + * @return {@code true} if the url is valid. */ public static boolean isValidUrl(String url) { if (url == null || url.length() == 0) { @@ -293,15 +294,15 @@ public final class URLUtil { * the URL and contentDisposition. File extension, if not defined, * is added based on the mimetype * @param url Url to the content - * @param contentDisposition Content-Disposition HTTP header or null - * @param mimeType Mime-type of the content or null + * @param contentDisposition Content-Disposition HTTP header or {@code null} + * @param mimeType Mime-type of the content or {@code null} * * @return suggested filename */ public static final String guessFileName( String url, - String contentDisposition, - String mimeType) { + @Nullable String contentDisposition, + @Nullable String mimeType) { String filename = null; String extension = null; @@ -388,7 +389,7 @@ public final class URLUtil { Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$", Pattern.CASE_INSENSITIVE); - /* + /** * Parse the Content-Disposition HTTP Header. The format of the header * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html * This header provides a filename for content that is going to be diff --git a/core/java/android/webkit/UrlInterceptHandler.java b/core/java/android/webkit/UrlInterceptHandler.java index 59fc0cba1fd3..0a6e51f7f3bd 100644 --- a/core/java/android/webkit/UrlInterceptHandler.java +++ b/core/java/android/webkit/UrlInterceptHandler.java @@ -16,8 +16,10 @@ package android.webkit; +import android.annotation.Nullable; import android.webkit.CacheManager.CacheResult; import android.webkit.PluginData; + import java.util.Map; /** @@ -30,31 +32,33 @@ public interface UrlInterceptHandler { /** * Given an URL, returns the CacheResult which contains the - * surrogate response for the request, or null if the handler is + * surrogate response for the request, or {@code null} if the handler is * not interested. * * @param url URL string. - * @param headers The headers associated with the request. May be null. + * @param headers The headers associated with the request. * @return The CacheResult containing the surrogate response. * * @hide * @deprecated Do not use, this interface is deprecated. */ @Deprecated - public CacheResult service(String url, Map<String, String> headers); + @Nullable + CacheResult service(String url, @Nullable Map<String, String> headers); /** * Given an URL, returns the PluginData which contains the - * surrogate response for the request, or null if the handler is + * surrogate response for the request, or {@code null} if the handler is * not interested. * * @param url URL string. - * @param headers The headers associated with the request. May be null. + * @param headers The headers associated with the request. * @return The PluginData containing the surrogate response. * * @hide * @deprecated Do not use, this interface is deprecated. */ @Deprecated - public PluginData getPluginData(String url, Map<String, String> headers); + @Nullable + PluginData getPluginData(String url, @Nullable Map<String, String> headers); } diff --git a/core/java/android/webkit/UrlInterceptRegistry.java b/core/java/android/webkit/UrlInterceptRegistry.java index bdf6747aa5a4..700d6d9332d6 100644 --- a/core/java/android/webkit/UrlInterceptRegistry.java +++ b/core/java/android/webkit/UrlInterceptRegistry.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.webkit.CacheManager.CacheResult; import android.webkit.PluginData; import android.webkit.UrlInterceptHandler; @@ -47,7 +48,7 @@ public final class UrlInterceptRegistry { /** * set the flag to control whether url intercept is enabled or disabled * - * @param disabled true to disable the cache + * @param disabled {@code true} to disable the cache * * @hide * @deprecated This class was intended to be used by Gears. Since Gears was @@ -77,7 +78,7 @@ public final class UrlInterceptRegistry { * before any that were previously registered. * * @param handler The new UrlInterceptHandler object - * @return true if the handler was not previously registered. + * @return {@code true} if the handler was not previously registered. * * @hide * @deprecated This class was intended to be used by Gears. Since Gears was @@ -98,7 +99,7 @@ public final class UrlInterceptRegistry { * Unregister a previously registered UrlInterceptHandler. * * @param handler A previously registered UrlInterceptHandler. - * @return true if the handler was found and removed from the list. + * @return {@code true} if the handler was found and removed from the list. * * @hide * @deprecated This class was intended to be used by Gears. Since Gears was @@ -112,7 +113,7 @@ public final class UrlInterceptRegistry { /** * Given an url, returns the CacheResult of the first - * UrlInterceptHandler interested, or null if none are. + * UrlInterceptHandler interested, or {@code null} if none are. * * @return A CacheResult containing surrogate content. * @@ -121,6 +122,7 @@ public final class UrlInterceptRegistry { * deprecated, so is this class. */ @Deprecated + @Nullable public static synchronized CacheResult getSurrogate( String url, Map<String, String> headers) { if (urlInterceptDisabled()) { @@ -139,7 +141,7 @@ public final class UrlInterceptRegistry { /** * Given an url, returns the PluginData of the first - * UrlInterceptHandler interested, or null if none are or if + * UrlInterceptHandler interested, or {@code null} if none are or if * intercepts are disabled. * * @return A PluginData instance containing surrogate content. @@ -149,6 +151,7 @@ public final class UrlInterceptRegistry { * deprecated, so is this class. */ @Deprecated + @Nullable public static synchronized PluginData getPluginData( String url, Map<String, String> headers) { if (urlInterceptDisabled()) { diff --git a/core/java/android/webkit/UserPackage.java b/core/java/android/webkit/UserPackage.java index 9da64559d0fe..63519a655113 100644 --- a/core/java/android/webkit/UserPackage.java +++ b/core/java/android/webkit/UserPackage.java @@ -34,6 +34,8 @@ public class UserPackage { private final UserInfo mUserInfo; private final PackageInfo mPackageInfo; + public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.O_MR1; + public UserPackage(UserInfo user, PackageInfo packageInfo) { this.mUserInfo = user; this.mPackageInfo = packageInfo; @@ -69,7 +71,7 @@ public class UserPackage { } /** - * Return true if the package is installed and not hidden + * Return {@code true} if the package is installed and not hidden */ public boolean isInstalledPackage() { if (mPackageInfo == null) return false; @@ -83,7 +85,7 @@ public class UserPackage { * supported by the current framework version. */ public static boolean hasCorrectTargetSdkVersion(PackageInfo packageInfo) { - return packageInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O_MR1; + return packageInfo.applicationInfo.targetSdkVersion >= MINIMUM_SUPPORTED_SDK; } public UserInfo getUserInfo() { diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java index 6f763dcfc072..0c34e3c16ac6 100644 --- a/core/java/android/webkit/WebBackForwardList.java +++ b/core/java/android/webkit/WebBackForwardList.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.Nullable; + import java.io.Serializable; /** @@ -25,10 +27,11 @@ import java.io.Serializable; */ public abstract class WebBackForwardList implements Cloneable, Serializable { /** - * Return the current history item. This method returns null if the list is + * Return the current history item. This method returns {@code null} if the list is * empty. * @return The current history item. */ + @Nullable public abstract WebHistoryItem getCurrentItem(); /** diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 70a49fb45e2c..4aa1c4a6daff 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -52,7 +53,7 @@ public class WebChromeClient { * Notify the host application of the url for an apple-touch-icon. * @param view The WebView that initiated the callback. * @param url The icon url. - * @param precomposed True if the url is for a precomposed touch icon. + * @param precomposed {@code true} if the url is for a precomposed touch icon. */ public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {} @@ -106,17 +107,17 @@ public class WebChromeClient { /** * Request the host application to create a new window. If the host - * application chooses to honor this request, it should return true from + * application chooses to honor this request, it should return {@code true} from * this method, create a new WebView to host the window, insert it into the * View system and send the supplied resultMsg message to its target with * the new WebView as an argument. If the host application chooses not to - * honor the request, it should return false from this method. The default - * implementation of this method does nothing and hence returns false. + * honor the request, it should return {@code false} from this method. The default + * implementation of this method does nothing and hence returns {@code false}. * @param view The WebView from which the request for a new window * originated. - * @param isDialog True if the new window should be a dialog, rather than + * @param isDialog {@code true} if the new window should be a dialog, rather than * a full-size window. - * @param isUserGesture True if the request was initiated by a user gesture, + * @param isUserGesture {@code true} if the request was initiated by a user gesture, * such as the user clicking a link. * @param resultMsg The message to send when once a new WebView has been * created. resultMsg.obj is a @@ -124,10 +125,10 @@ public class WebChromeClient { * used to transport the new WebView, by calling * {@link WebView.WebViewTransport#setWebView(WebView) * WebView.WebViewTransport.setWebView(WebView)}. - * @return This method should return true if the host application will + * @return This method should return {@code true} if the host application will * create a new window, in which case resultMsg should be sent to - * its target. Otherwise, this method should return false. Returning - * false from this method but also sending resultMsg will result in + * its target. Otherwise, this method should return {@code false}. Returning + * {@code false} from this method but also sending resultMsg will result in * undefined behavior. */ public boolean onCreateWindow(WebView view, boolean isDialog, @@ -154,8 +155,8 @@ public class WebChromeClient { /** * Tell the client to display a javascript alert dialog. If the client - * returns true, WebView will assume that the client will handle the - * dialog. If the client returns false, it will continue execution. + * returns {@code true}, WebView will assume that the client will handle the + * dialog. If the client returns {@code false}, it will continue execution. * @param view The WebView that initiated the callback. * @param url The url of the page requesting the dialog. * @param message Message to be displayed in the window. @@ -169,10 +170,10 @@ public class WebChromeClient { /** * Tell the client to display a confirm dialog to the user. If the client - * returns true, WebView will assume that the client will handle the + * returns {@code true}, WebView will assume that the client will handle the * confirm dialog and call the appropriate JsResult method. If the - * client returns false, a default value of false will be returned to - * javascript. The default behavior is to return false. + * client returns false, a default value of {@code false} will be returned to + * javascript. The default behavior is to return {@code false}. * @param view The WebView that initiated the callback. * @param url The url of the page requesting the dialog. * @param message Message to be displayed in the window. @@ -187,10 +188,10 @@ public class WebChromeClient { /** * Tell the client to display a prompt dialog to the user. If the client - * returns true, WebView will assume that the client will handle the + * returns {@code true}, WebView will assume that the client will handle the * prompt dialog and call the appropriate JsPromptResult method. If the - * client returns false, a default value of false will be returned to to - * javascript. The default behavior is to return false. + * client returns false, a default value of {@code false} will be returned to to + * javascript. The default behavior is to return {@code false}. * @param view The WebView that initiated the callback. * @param url The url of the page requesting the dialog. * @param message Message to be displayed in the window. @@ -207,12 +208,12 @@ public class WebChromeClient { /** * Tell the client to display a dialog to confirm navigation away from the * current page. This is the result of the onbeforeunload javascript event. - * If the client returns true, WebView will assume that the client will + * If the client returns {@code true}, WebView will assume that the client will * handle the confirm dialog and call the appropriate JsResult method. If - * the client returns false, a default value of true will be returned to + * the client returns {@code false}, a default value of {@code true} will be returned to * javascript to accept navigation away from the current page. The default - * behavior is to return false. Setting the JsResult to true will navigate - * away from the current page, false will cancel the navigation. + * behavior is to return {@code false}. Setting the JsResult to {@code true} will navigate + * away from the current page, {@code false} will cancel the navigation. * @param view The WebView that initiated the callback. * @param url The url of the page requesting the dialog. * @param message Message to be displayed in the window. @@ -289,7 +290,7 @@ public class WebChromeClient { * (API level > {@link android.os.Build.VERSION_CODES#M}) * this method is only called for requests originating from secure * origins such as https. On non-secure origins geolocation requests - * are automatically denied.</p> + * are automatically denied. * * @param origin The origin of the web content attempting to use the * Geolocation API. @@ -332,8 +333,8 @@ public class WebChromeClient { /** * Tell the client that a JavaScript execution timeout has occured. And the * client may decide whether or not to interrupt the execution. If the - * client returns true, the JavaScript will be interrupted. If the client - * returns false, the execution will continue. Note that in the case of + * client returns {@code true}, the JavaScript will be interrupted. If the client + * returns {@code false}, the execution will continue. Note that in the case of * continuing execution, the timeout counter will be reset, and the callback * will continue to occur if the script does not finish at the next check * point. @@ -365,7 +366,7 @@ public class WebChromeClient { * Report a JavaScript console message to the host application. The ChromeClient * should override this to process the log message as they see fit. * @param consoleMessage Object containing details of the console message. - * @return true if the message is handled by the client. + * @return {@code true} if the message is handled by the client. */ public boolean onConsoleMessage(ConsoleMessage consoleMessage) { // Call the old version of this function for backwards compatability. @@ -380,9 +381,10 @@ public class WebChromeClient { * HTML. If the attribute is absent, then a default poster will be used. This * method allows the ChromeClient to provide that default image. * - * @return Bitmap The image to use as a default poster, or null if no such image is + * @return Bitmap The image to use as a default poster, or {@code null} if no such image is * available. */ + @Nullable public Bitmap getDefaultVideoPoster() { return null; } @@ -394,6 +396,7 @@ public class WebChromeClient { * * @return View The View to be displayed whilst the video is loading. */ + @Nullable public View getVideoLoadingProgressView() { return null; } @@ -409,15 +412,16 @@ public class WebChromeClient { * This is called to handle HTML forms with 'file' input type, in response to the * user pressing the "Select File" button. * To cancel the request, call <code>filePathCallback.onReceiveValue(null)</code> and - * return true. + * return {@code true}. * * @param webView The WebView instance that is initiating the request. * @param filePathCallback Invoke this callback to supply the list of paths to files to upload, - * or NULL to cancel. Must only be called if the - * <code>showFileChooser</code> implementations returns true. + * or {@code null} to cancel. Must only be called if the + * {@link #onShowFileChooser} implementation returns {@code true}. * @param fileChooserParams Describes the mode of file chooser to be opened, and options to be * used with it. - * @return true if filePathCallback will be invoked, false to use default handling. + * @return {@code true} if filePathCallback will be invoked, {@code false} to use default + * handling. * * @see FileChooserParams */ @@ -448,9 +452,10 @@ public class WebChromeClient { * * @param resultCode the integer result code returned by the file picker activity. * @param data the intent returned by the file picker activity. - * @return the Uris of selected file(s) or null if the resultCode indicates + * @return the Uris of selected file(s) or {@code null} if the resultCode indicates * activity canceled or any other error. */ + @Nullable public static Uri[] parseResult(int resultCode, Intent data) { return WebViewFactory.getProvider().getStatics().parseFileChooserResult(resultCode, data); } @@ -469,21 +474,23 @@ public class WebChromeClient { /** * Returns preference for a live media captured value (e.g. Camera, Microphone). - * True indicates capture is enabled, false disabled. + * True indicates capture is enabled, {@code false} disabled. * * Use <code>getAcceptTypes</code> to determine suitable capture devices. */ public abstract boolean isCaptureEnabled(); /** - * Returns the title to use for this file selector, or null. If null a default - * title should be used. + * Returns the title to use for this file selector. If {@code null} a default title should + * be used. */ + @Nullable public abstract CharSequence getTitle(); /** - * The file name of a default selection if specified, or null. + * The file name of a default selection if specified, or {@code null}. */ + @Nullable public abstract String getFilenameHint(); /** @@ -517,7 +524,7 @@ public class WebChromeClient { * @param capture The value of the 'capture' attribute of the input tag * associated with this file picker. * - * @deprecated Use {@link #showFileChooser} instead. + * @deprecated Use {@link #onShowFileChooser} instead. * @hide This method was not published in any SDK version. */ @SystemApi @@ -525,15 +532,4 @@ public class WebChromeClient { public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) { uploadFile.onReceiveValue(null); } - - /** - * Tell the client that the page being viewed has an autofillable - * form and the user would like to set a profile up. - * @param msg A Message to send once the user has successfully - * set up a profile and to inform the WebTextView it should - * now autofill using that new profile. - * @hide - */ - public void setupAutoFill(Message msg) { } - } diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java index 569fccd565cc..74db039e015d 100644 --- a/core/java/android/webkit/WebHistoryItem.java +++ b/core/java/android/webkit/WebHistoryItem.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.graphics.Bitmap; @@ -65,11 +66,12 @@ public abstract class WebHistoryItem implements Cloneable { public abstract String getTitle(); /** - * Return the favicon of this history item or null if no favicon was found. - * @return A Bitmap containing the favicon for this history item or null. + * Return the favicon of this history item or {@code null} if no favicon was found. + * @return A Bitmap containing the favicon for this history item or {@code null}. * Note: The VM ensures 32-bit atomic read/write operations so we don't have * to synchronize this method. */ + @Nullable public abstract Bitmap getFavicon(); /** diff --git a/core/java/android/webkit/WebMessage.java b/core/java/android/webkit/WebMessage.java index 7683a40d114a..bfc00e7a0145 100644 --- a/core/java/android/webkit/WebMessage.java +++ b/core/java/android/webkit/WebMessage.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.Nullable; + /** * The Java representation of the HTML5 PostMessage event. See * https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces @@ -53,9 +55,10 @@ public class WebMessage { } /** - * Returns the ports that are sent with the message, or null if no port + * Returns the ports that are sent with the message, or {@code null} if no port * is sent. */ + @Nullable public WebMessagePort[] getPorts() { return mPorts; } diff --git a/core/java/android/webkit/WebMessagePort.java b/core/java/android/webkit/WebMessagePort.java index 54dd502ca7de..acd7af955b75 100644 --- a/core/java/android/webkit/WebMessagePort.java +++ b/core/java/android/webkit/WebMessagePort.java @@ -22,30 +22,30 @@ import android.os.Handler; /** * <p>The Java representation of the * <a href="https://html.spec.whatwg.org/multipage/comms.html#messageport"> - * HTML5 message ports.</a> </p> + * HTML5 message ports.</a> * * <p>A Message port represents one endpoint of a Message Channel. In Android * webview, there is no separate Message Channel object. When a message channel * is created, both ports are tangled to each other and started, and then * returned in a MessagePort array, see {@link WebView#createWebMessageChannel} - * for creating a message channel. </p> + * for creating a message channel. * * <p>When a message port is first created or received via transfer, it does not * have a WebMessageCallback to receive web messages. The messages are queued until - * a WebMessageCallback is set. </p> + * a WebMessageCallback is set. * * <p>A message port should be closed when it is not used by the embedder application * anymore. A closed port cannot be transferred or cannot be reopened to send - * messages. Close can be called multiple times. </p> + * messages. Close can be called multiple times. * * <p>When a port is transferred to JS, it cannot be used to send or receive messages * at the Java side anymore. Different from HTML5 Spec, a port cannot be transferred * if one of these has ever happened: i. a message callback was set, ii. a message was * posted on it. A transferred port cannot be closed by the application, since - * the ownership is also transferred. </p> + * the ownership is also transferred. * * <p>It is possible to transfer both ports of a channel to JS, for example for - * communication between subframes.</p> + * communication between subframes. */ public abstract class WebMessagePort { diff --git a/core/java/android/webkit/WebResourceRequest.java b/core/java/android/webkit/WebResourceRequest.java index ab935050eb73..964b6f8e259d 100644 --- a/core/java/android/webkit/WebResourceRequest.java +++ b/core/java/android/webkit/WebResourceRequest.java @@ -34,7 +34,7 @@ public interface WebResourceRequest { /** * Gets whether the request was made for the main frame. * - * @return whether the request was made for the main frame. Will be false for iframes, + * @return whether the request was made for the main frame. Will be {@code false} for iframes, * for example. */ boolean isForMainFrame(); @@ -48,8 +48,9 @@ public interface WebResourceRequest { /** * Gets whether a gesture (such as a click) was associated with the request. - * For security reasons in certain situations this method may return false even though the - * sequence of events which caused the request to be created was initiated by a user gesture. + * For security reasons in certain situations this method may return {@code false} even though + * the sequence of events which caused the request to be created was initiated by a user + * gesture. * * @return whether a gesture was associated with the request. */ diff --git a/core/java/android/webkit/WebResourceResponse.java b/core/java/android/webkit/WebResourceResponse.java index 80c43c147dbe..7bc7b07d2fc1 100644 --- a/core/java/android/webkit/WebResourceResponse.java +++ b/core/java/android/webkit/WebResourceResponse.java @@ -16,12 +16,13 @@ package android.webkit; +import android.annotation.NonNull; +import android.annotation.SystemApi; + import java.io.InputStream; import java.io.StringBufferInputStream; import java.util.Map; -import android.annotation.SystemApi; - /** * Encapsulates a resource response. Applications can return an instance of this * class from {@link WebViewClient#shouldInterceptRequest} to provide a custom @@ -63,15 +64,15 @@ public class WebResourceResponse { * @param encoding the resource response's encoding * @param statusCode the status code needs to be in the ranges [100, 299], [400, 599]. * Causing a redirect by specifying a 3xx code is not supported. - * @param reasonPhrase the phrase describing the status code, for example "OK". Must be non-null - * and not empty. + * @param reasonPhrase the phrase describing the status code, for example "OK". Must be + * non-empty. * @param responseHeaders the resource response's headers represented as a mapping of header * name -> header value. * @param data the input stream that provides the resource response's data. Must not be a * StringBufferInputStream. */ public WebResourceResponse(String mimeType, String encoding, int statusCode, - String reasonPhrase, Map<String, String> responseHeaders, InputStream data) { + @NonNull String reasonPhrase, Map<String, String> responseHeaders, InputStream data) { this(mimeType, encoding, data); setStatusCodeAndReasonPhrase(statusCode, reasonPhrase); setResponseHeaders(responseHeaders); @@ -121,10 +122,10 @@ public class WebResourceResponse { * * @param statusCode the status code needs to be in the ranges [100, 299], [400, 599]. * Causing a redirect by specifying a 3xx code is not supported. - * @param reasonPhrase the phrase describing the status code, for example "OK". Must be non-null - * and not empty. + * @param reasonPhrase the phrase describing the status code, for example "OK". Must be + * non-empty. */ - public void setStatusCodeAndReasonPhrase(int statusCode, String reasonPhrase) { + public void setStatusCodeAndReasonPhrase(int statusCode, @NonNull String reasonPhrase) { checkImmutable(); if (statusCode < 100) throw new IllegalArgumentException("statusCode can't be less than 100."); diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 82cff7c13e47..e49373923ec9 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -17,6 +17,7 @@ package android.webkit; import android.annotation.IntDef; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.content.Context; @@ -28,10 +29,10 @@ import java.lang.annotation.Target; /** * Manages settings state for a WebView. When a WebView is first created, it * obtains a set of default settings. These default settings will be returned - * from any getter call. A WebSettings object obtained from - * WebView.getSettings() is tied to the life of the WebView. If a WebView has - * been destroyed, any method call on WebSettings will throw an - * IllegalStateException. + * from any getter call. A {@code WebSettings} object obtained from + * {@link WebView#getSettings()} is tied to the life of the WebView. If a WebView has + * been destroyed, any method call on {@code WebSettings} will throw an + * {@link IllegalStateException}. */ // This is an abstract base class: concrete WebViewProviders must // create a class derived from this, and return an instance of it in the @@ -40,13 +41,13 @@ public abstract class WebSettings { /** * Enum for controlling the layout of html. * <ul> - * <li>NORMAL means no rendering changes. This is the recommended choice for maximum + * <li>{@code NORMAL} means no rendering changes. This is the recommended choice for maximum * compatibility across different platforms and Android versions.</li> - * <li>SINGLE_COLUMN moves all content into one column that is the width of the + * <li>{@code SINGLE_COLUMN} moves all content into one column that is the width of the * view.</li> - * <li>NARROW_COLUMNS makes all columns no wider than the screen if possible. Only use + * <li>{@code NARROW_COLUMNS} makes all columns no wider than the screen if possible. Only use * this for API levels prior to {@link android.os.Build.VERSION_CODES#KITKAT}.</li> - * <li>TEXT_AUTOSIZING boosts font size of paragraphs based on heuristics to make + * <li>{@code TEXT_AUTOSIZING} boosts font size of paragraphs based on heuristics to make * the text readable when viewing a wide-viewport layout in the overview mode. * It is recommended to enable zoom support {@link #setSupportZoom} when * using this mode. Supported from API level @@ -97,9 +98,9 @@ public abstract class WebSettings { /** * Enum for specifying the WebView's desired density. * <ul> - * <li>FAR makes 100% looking like in 240dpi</li> - * <li>MEDIUM makes 100% looking like in 160dpi</li> - * <li>CLOSE makes 100% looking like in 120dpi</li> + * <li>{@code FAR} makes 100% looking like in 240dpi</li> + * <li>{@code MEDIUM} makes 100% looking like in 160dpi</li> + * <li>{@code CLOSE} makes 100% looking like in 120dpi</li> * </ul> */ public enum ZoomDensity { @@ -217,7 +218,7 @@ public abstract class WebSettings { /** * Enables dumping the pages navigation cache to a text file. The default - * is false. + * is {@code false}. * * @deprecated This method is now obsolete. * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} @@ -243,7 +244,7 @@ public abstract class WebSettings { * controls and gestures. The particular zoom mechanisms that should be used * can be set with {@link #setBuiltInZoomControls}. This setting does not * affect zooming performed using the {@link WebView#zoomIn()} and - * {@link WebView#zoomOut()} methods. The default is true. + * {@link WebView#zoomOut()} methods. The default is {@code true}. * * @param support whether the WebView should support zoom */ @@ -252,14 +253,14 @@ public abstract class WebSettings { /** * Gets whether the WebView supports zoom. * - * @return true if the WebView supports zoom + * @return {@code true} if the WebView supports zoom * @see #setSupportZoom */ public abstract boolean supportZoom(); /** * Sets whether the WebView requires a user gesture to play media. - * The default is true. + * The default is {@code true}. * * @param require whether the WebView requires a user gesture to play media */ @@ -268,7 +269,7 @@ public abstract class WebSettings { /** * Gets whether the WebView requires a user gesture to play media. * - * @return true if the WebView requires a user gesture to play media + * @return {@code true} if the WebView requires a user gesture to play media * @see #setMediaPlaybackRequiresUserGesture */ public abstract boolean getMediaPlaybackRequiresUserGesture(); @@ -278,7 +279,7 @@ public abstract class WebSettings { * built-in zoom mechanisms comprise on-screen zoom controls, which are * displayed over the WebView's content, and the use of a pinch gesture to * control zooming. Whether or not these on-screen controls are displayed - * can be set with {@link #setDisplayZoomControls}. The default is false. + * can be set with {@link #setDisplayZoomControls}. The default is {@code false}. * <p> * The built-in mechanisms are the only currently supported zoom * mechanisms, so it is recommended that this setting is always enabled. @@ -293,7 +294,7 @@ public abstract class WebSettings { /** * Gets whether the zoom mechanisms built into WebView are being used. * - * @return true if the zoom mechanisms built into WebView are being used + * @return {@code true} if the zoom mechanisms built into WebView are being used * @see #setBuiltInZoomControls */ public abstract boolean getBuiltInZoomControls(); @@ -301,7 +302,7 @@ public abstract class WebSettings { /** * Sets whether the WebView should display on-screen zoom controls when * using the built-in zoom mechanisms. See {@link #setBuiltInZoomControls}. - * The default is true. + * The default is {@code true}. * * @param enabled whether the WebView should display on-screen zoom controls */ @@ -311,7 +312,7 @@ public abstract class WebSettings { * Gets whether the WebView displays on-screen zoom controls when using * the built-in zoom mechanisms. * - * @return true if the WebView displays on-screen zoom controls when using + * @return {@code true} if the WebView displays on-screen zoom controls when using * the built-in zoom mechanisms * @see #setDisplayZoomControls */ @@ -351,7 +352,7 @@ public abstract class WebSettings { * zooms out the content to fit on screen by width. This setting is * taken into account when the content width is greater than the width * of the WebView control, for example, when {@link #getUseWideViewPort} - * is enabled. The default is false. + * is enabled. The default is {@code false}. */ public abstract void setLoadWithOverviewMode(boolean overview); @@ -366,9 +367,9 @@ public abstract class WebSettings { /** * Sets whether the WebView will enable smooth transition while panning or * zooming or while the window hosting the WebView does not have focus. - * If it is true, WebView will choose a solution to maximize the performance. + * If it is {@code true}, WebView will choose a solution to maximize the performance. * e.g. the WebView's content may not be updated during the transition. - * If it is false, WebView will keep its fidelity. The default value is false. + * If it is false, WebView will keep its fidelity. The default value is {@code false}. * * @deprecated This method is now obsolete, and will become a no-op in future. */ @@ -388,8 +389,8 @@ public abstract class WebSettings { /** * Sets whether the WebView uses its background for over scroll background. - * If true, it will use the WebView's background. If false, it will use an - * internal pattern. Default is true. + * If {@code true}, it will use the WebView's background. If {@code false}, it will use an + * internal pattern. Default is {@code true}. * * @deprecated This method is now obsolete. * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} @@ -433,7 +434,7 @@ public abstract class WebSettings { public abstract boolean getSaveFormData(); /** - * Sets whether the WebView should save passwords. The default is true. + * Sets whether the WebView should save passwords. The default is {@code true}. * @deprecated Saving passwords in WebView will not be supported in future versions. */ @Deprecated @@ -627,9 +628,9 @@ public abstract class WebSettings { /** * Sets whether the WebView should enable support for the "viewport" * HTML meta tag or should use a wide viewport. - * When the value of the setting is false, the layout width is always set to the + * When the value of the setting is {@code false}, the layout width is always set to the * width of the WebView control in device-independent (CSS) pixels. - * When the value is true and the page contains the viewport meta tag, the value + * When the value is {@code true} and the page contains the viewport meta tag, the value * of the width specified in the tag is used. If the page does not contain the tag or * does not provide a width, then a wide viewport will be used. * @@ -641,7 +642,7 @@ public abstract class WebSettings { * Gets whether the WebView supports the "viewport" * HTML meta tag or will use a wide viewport. * - * @return true if the WebView supports the viewport meta tag + * @return {@code true} if the WebView supports the viewport meta tag * @see #setUseWideViewPort */ public abstract boolean getUseWideViewPort(); @@ -649,22 +650,22 @@ public abstract class WebSettings { /** * Sets whether the WebView whether supports multiple windows. If set to * true, {@link WebChromeClient#onCreateWindow} must be implemented by the - * host application. The default is false. + * host application. The default is {@code false}. * - * @param support whether to suport multiple windows + * @param support whether to support multiple windows */ public abstract void setSupportMultipleWindows(boolean support); /** * Gets whether the WebView supports multiple windows. * - * @return true if the WebView supports multiple windows + * @return {@code true} if the WebView supports multiple windows * @see #setSupportMultipleWindows */ public abstract boolean supportMultipleWindows(); /** - * Sets the underlying layout algorithm. This will cause a relayout of the + * Sets the underlying layout algorithm. This will cause a re-layout of the * WebView. The default is {@link LayoutAlgorithm#NARROW_COLUMNS}. * * @param l the layout algorithm to use, as a {@link LayoutAlgorithm} value @@ -838,9 +839,9 @@ public abstract class WebSettings { * controls loading of all images, including those embedded using the data * URI scheme. Use {@link #setBlockNetworkImage} to control loading only * of images specified using network URI schemes. Note that if the value of this - * setting is changed from false to true, all images resources referenced + * setting is changed from {@code false} to {@code true}, all images resources referenced * by content currently displayed by the WebView are loaded automatically. - * The default is true. + * The default is {@code true}. * * @param flag whether the WebView should load image resources */ @@ -850,7 +851,7 @@ public abstract class WebSettings { * Gets whether the WebView loads image resources. This includes * images embedded using the data URI scheme. * - * @return true if the WebView loads image resources + * @return {@code true} if the WebView loads image resources * @see #setLoadsImagesAutomatically */ public abstract boolean getLoadsImagesAutomatically(); @@ -859,12 +860,12 @@ public abstract class WebSettings { * Sets whether the WebView should not load image resources from the * network (resources accessed via http and https URI schemes). Note * that this method has no effect unless - * {@link #getLoadsImagesAutomatically} returns true. Also note that + * {@link #getLoadsImagesAutomatically} returns {@code true}. Also note that * disabling all network loads using {@link #setBlockNetworkLoads} * will also prevent network images from loading, even if this flag is set - * to false. When the value of this setting is changed from true to false, + * to false. When the value of this setting is changed from {@code true} to {@code false}, * network images resources referenced by content currently displayed by - * the WebView are fetched automatically. The default is false. + * the WebView are fetched automatically. The default is {@code false}. * * @param flag whether the WebView should not load image resources from the * network @@ -875,7 +876,7 @@ public abstract class WebSettings { /** * Gets whether the WebView does not load image resources from the network. * - * @return true if the WebView does not load image resources from the network + * @return {@code true} if the WebView does not load image resources from the network * @see #setBlockNetworkImage */ public abstract boolean getBlockNetworkImage(); @@ -884,17 +885,17 @@ public abstract class WebSettings { * Sets whether the WebView should not load resources from the network. * Use {@link #setBlockNetworkImage} to only avoid loading * image resources. Note that if the value of this setting is - * changed from true to false, network resources referenced by content + * changed from {@code true} to {@code false}, network resources referenced by content * currently displayed by the WebView are not fetched until * {@link android.webkit.WebView#reload} is called. * If the application does not have the * {@link android.Manifest.permission#INTERNET} permission, attempts to set - * a value of false will cause a {@link java.lang.SecurityException} - * to be thrown. The default value is false if the application has the + * a value of {@code false} will cause a {@link java.lang.SecurityException} + * to be thrown. The default value is {@code false} if the application has the * {@link android.Manifest.permission#INTERNET} permission, otherwise it is - * true. + * {@code true}. * - * @param flag true means block network loads by the WebView + * @param flag {@code true} means block network loads by the WebView * @see android.webkit.WebView#reload */ public abstract void setBlockNetworkLoads(boolean flag); @@ -902,16 +903,16 @@ public abstract class WebSettings { /** * Gets whether the WebView does not load any resources from the network. * - * @return true if the WebView does not load any resources from the network + * @return {@code true} if the WebView does not load any resources from the network * @see #setBlockNetworkLoads */ public abstract boolean getBlockNetworkLoads(); /** * Tells the WebView to enable JavaScript execution. - * <b>The default is false.</b> + * <b>The default is {@code false}.</b> * - * @param flag true if the WebView should execute JavaScript + * @param flag {@code true} if the WebView should execute JavaScript */ public abstract void setJavaScriptEnabled(boolean flag); @@ -927,9 +928,9 @@ public abstract class WebSettings { * on {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} and earlier * devices, you should explicitly set this value to {@code false}. * <p> - * The default value is true for API level + * The default value is {@code true} for API level * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH_MR1} and below, - * and false for API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} + * and {@code false} for API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} * and above. * * @param flag whether JavaScript running in the context of a file scheme @@ -942,16 +943,16 @@ public abstract class WebSettings { * should be allowed to access content from other file scheme URLs. To * enable the most restrictive, and therefore secure policy, this setting * should be disabled. Note that the value of this setting is ignored if - * the value of {@link #getAllowUniversalAccessFromFileURLs} is true. + * the value of {@link #getAllowUniversalAccessFromFileURLs} is {@code true}. * Note too, that this setting affects only JavaScript access to file scheme * resources. Other access to such resources, for example, from image HTML * elements, is unaffected. To prevent possible violation of same domain policy * on {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} and earlier * devices, you should explicitly set this value to {@code false}. * <p> - * The default value is true for API level + * The default value is {@code true} for API level * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH_MR1} and below, - * and false for API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} + * and {@code false} for API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} * and above. * * @param flag whether JavaScript running in the context of a file scheme @@ -961,9 +962,9 @@ public abstract class WebSettings { public abstract void setAllowFileAccessFromFileURLs(boolean flag); /** - * Sets whether the WebView should enable plugins. The default is false. + * Sets whether the WebView should enable plugins. The default is {@code false}. * - * @param flag true if plugins should be enabled + * @param flag {@code true} if plugins should be enabled * @deprecated This method has been deprecated in favor of * {@link #setPluginState} * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} @@ -1028,11 +1029,11 @@ public abstract class WebSettings { /** * Sets whether the Application Caches API should be enabled. The default - * is false. Note that in order for the Application Caches API to be + * is {@code false}. Note that in order for the Application Caches API to be * enabled, a valid database path must also be supplied to * {@link #setAppCachePath}. * - * @param flag true if the WebView should enable Application Caches + * @param flag {@code true} if the WebView should enable Application Caches */ public abstract void setAppCacheEnabled(boolean flag); @@ -1072,21 +1073,21 @@ public abstract class WebSettings { * page load within a given process, as the WebView implementation may ignore * changes to this setting after that point. * - * @param flag true if the WebView should use the database storage API + * @param flag {@code true} if the WebView should use the database storage API */ public abstract void setDatabaseEnabled(boolean flag); /** - * Sets whether the DOM storage API is enabled. The default value is false. + * Sets whether the DOM storage API is enabled. The default value is {@code false}. * - * @param flag true if the WebView should use the DOM storage API + * @param flag {@code true} if the WebView should use the DOM storage API */ public abstract void setDomStorageEnabled(boolean flag); /** * Gets whether the DOM Storage APIs are enabled. * - * @return true if the DOM Storage APIs are enabled + * @return {@code true} if the DOM Storage APIs are enabled * @see #setDomStorageEnabled */ public abstract boolean getDomStorageEnabled(); @@ -1104,13 +1105,13 @@ public abstract class WebSettings { /** * Gets whether the database storage API is enabled. * - * @return true if the database storage API is enabled + * @return {@code true} if the database storage API is enabled * @see #setDatabaseEnabled */ public abstract boolean getDatabaseEnabled(); /** - * Sets whether Geolocation is enabled. The default is true. + * Sets whether Geolocation is enabled. The default is {@code true}. * <p> * Please note that in order for the Geolocation API to be usable * by a page in the WebView, the following requirements must be met: @@ -1132,7 +1133,7 @@ public abstract class WebSettings { /** * Gets whether JavaScript is enabled. * - * @return true if JavaScript is enabled + * @return {@code true} if JavaScript is enabled * @see #setJavaScriptEnabled */ public abstract boolean getJavaScriptEnabled(); @@ -1161,7 +1162,7 @@ public abstract class WebSettings { /** * Gets whether plugins are enabled. * - * @return true if plugins are enabled + * @return {@code true} if plugins are enabled * @see #setPluginsEnabled * @deprecated This method has been replaced by {@link #getPluginState} * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} @@ -1197,17 +1198,17 @@ public abstract class WebSettings { /** * Tells JavaScript to open windows automatically. This applies to the - * JavaScript function window.open(). The default is false. + * JavaScript function {@code window.open()}. The default is {@code false}. * - * @param flag true if JavaScript can open windows automatically + * @param flag {@code true} if JavaScript can open windows automatically */ public abstract void setJavaScriptCanOpenWindowsAutomatically(boolean flag); /** * Gets whether JavaScript can open windows automatically. * - * @return true if JavaScript can open windows automatically during - * window.open() + * @return {@code true} if JavaScript can open windows automatically during + * {@code window.open()} * @see #setJavaScriptCanOpenWindowsAutomatically */ public abstract boolean getJavaScriptCanOpenWindowsAutomatically(); @@ -1229,7 +1230,7 @@ public abstract class WebSettings { public abstract String getDefaultTextEncodingName(); /** - * Sets the WebView's user-agent string. If the string is null or empty, + * Sets the WebView's user-agent string. If the string is {@code null} or empty, * the system default value will be used. * * Note that starting from {@link android.os.Build.VERSION_CODES#KITKAT} Android @@ -1238,7 +1239,7 @@ public abstract class WebSettings { * * @param ua new user-agent string */ - public abstract void setUserAgentString(String ua); + public abstract void setUserAgentString(@Nullable String ua); /** * Gets the WebView's user-agent string. @@ -1262,7 +1263,7 @@ public abstract class WebSettings { /** * Tells the WebView whether it needs to set a node to have focus when * {@link WebView#requestFocus(int, android.graphics.Rect)} is called. The - * default value is true. + * default value is {@code true}. * * @param flag whether the WebView needs to set a node */ @@ -1355,7 +1356,7 @@ public abstract class WebSettings { /** * Gets whether a video overlay will be used for embedded encrypted video. * - * @return true if WebView uses a video overlay for embedded encrypted video. + * @return {@code true} if WebView uses a video overlay for embedded encrypted video. * @see #setVideoOverlayForEmbeddedEncryptedVideoEnabled * @hide */ @@ -1380,7 +1381,7 @@ public abstract class WebSettings { /** * Gets whether this WebView should raster tiles when it is * offscreen but attached to a window. - * @return true if this WebView will raster tiles when it is + * @return {@code true} if this WebView will raster tiles when it is * offscreen but attached to a window. */ public abstract boolean getOffscreenPreRaster(); @@ -1394,11 +1395,9 @@ public abstract class WebSettings { * Safe browsing is disabled by default. The recommended way to enable Safe browsing is using a * manifest tag to change the default value to enabled for all WebViews (read <a * href="{@docRoot}reference/android/webkit/WebView.html">general Safe Browsing info</a>). - * </p> * * <p> * This API overrides the manifest tag value for this WebView. - * </p> * * @param enabled Whether Safe browsing is enabled. */ @@ -1408,7 +1407,7 @@ public abstract class WebSettings { * Gets whether Safe browsing is enabled. * See {@link #setSafeBrowsingEnabled}. * - * @return true if Safe browsing is enabled and false otherwise. + * @return {@code true} if Safe browsing is enabled and {@code false} otherwise. */ public abstract boolean getSafeBrowsingEnabled(); diff --git a/core/java/android/webkit/WebSyncManager.java b/core/java/android/webkit/WebSyncManager.java index 801be12879ce..03b94e711c22 100644 --- a/core/java/android/webkit/WebSyncManager.java +++ b/core/java/android/webkit/WebSyncManager.java @@ -18,7 +18,7 @@ package android.webkit; import android.content.Context; -/* +/** * @deprecated The WebSyncManager no longer does anything. */ @Deprecated diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 637b60e2dcb4..6f9925480a22 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -75,18 +75,22 @@ import java.util.Map; * can roll your own web browser or simply display some online content within your Activity. * It uses the WebKit rendering engine to display * web pages and includes methods to navigate forward and backward - * through a history, zoom in and out, perform text searches and more.</p> + * through a history, zoom in and out, perform text searches and more. + * * <p>Note that, in order for your Activity to access the Internet and load web pages * in a WebView, you must add the {@code INTERNET} permissions to your - * Android Manifest file:</p> - * <pre><uses-permission android:name="android.permission.INTERNET" /></pre> + * Android Manifest file: + * + * <pre> + * {@code <uses-permission android:name="android.permission.INTERNET" />} + * </pre> * * <p>This must be a child of the <a * href="{@docRoot}guide/topics/manifest/manifest-element.html">{@code <manifest>}</a> - * element.</p> + * element. * * <p>For more information, read - * <a href="{@docRoot}guide/webapps/webview.html">Building Web Apps in WebView</a>.</p> + * <a href="{@docRoot}guide/webapps/webview.html">Building Web Apps in WebView</a>. * * <h3>Basic usage</h3> * @@ -103,17 +107,19 @@ import java.util.Map; * Intent intent = new Intent(Intent.ACTION_VIEW, uri); * startActivity(intent); * </pre> - * <p>See {@link android.content.Intent} for more information.</p> + * <p>See {@link android.content.Intent} for more information. * * <p>To provide a WebView in your own Activity, include a {@code <WebView>} in your layout, * or set the entire Activity window as a WebView during {@link - * android.app.Activity#onCreate(Bundle) onCreate()}:</p> + * android.app.Activity#onCreate(Bundle) onCreate()}: + * * <pre class="prettyprint"> * WebView webview = new WebView(this); * setContentView(webview); * </pre> * - * <p>Then load the desired web page:</p> + * <p>Then load the desired web page: + * * <pre> * // Simplest usage: note that an exception will NOT be thrown * // if there is an error loading this page (see below). @@ -128,7 +134,7 @@ import java.util.Map; * </pre> * * <p>A WebView has several customization points where you can add your - * own behavior. These are:</p> + * own behavior. These are: * * <ul> * <li>Creating and setting a {@link android.webkit.WebChromeClient} subclass. @@ -153,7 +159,7 @@ import java.util.Map; * </ul> * * <p>Here's a more complicated example, showing error handling, - * settings, and progress notification:</p> + * settings, and progress notification: * * <pre class="prettyprint"> * // Let's display the progress in the activity title bar, like the @@ -183,23 +189,23 @@ import java.util.Map; * * <p>To enable the built-in zoom, set * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} - * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}).</p> - * <p>NOTE: Using zoom if either the height or width is set to + * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}). + * + * <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.</p> + * and should be avoided. * * <h3>Cookie and window management</h3> * * <p>For obvious security reasons, your application has its own * cache, cookie store etc.—it does not share the Browser * application's data. - * </p> * * <p>By default, requests by the HTML to open new windows are - * ignored. This is true whether they be opened by JavaScript or by + * ignored. This is {@code true} whether they be opened by JavaScript or by * the target attribute on a link. You can customize your * {@link WebChromeClient} to provide your own behavior for opening multiple windows, - * and render them in whatever manner you want.</p> + * and render them in whatever manner you want. * * <p>The standard behavior for an Activity is to be destroyed and * recreated when the device orientation or any other configuration changes. This will cause @@ -208,7 +214,7 @@ import java.util.Map; * changes, and then just leave the WebView alone. It'll automatically * re-orient itself as appropriate. Read <a * href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a> for - * more information about how to handle configuration changes during runtime.</p> + * more information about how to handle configuration changes during runtime. * * * <h3>Building web pages to support different screen densities</h3> @@ -220,15 +226,15 @@ import java.util.Map; * height and width are defined in terms of screen pixels will appear larger on the lower density * screen and smaller on the higher density screen. * For simplicity, Android collapses all actual screen densities into three generalized densities: - * high, medium, and low.</p> + * high, medium, and low. * <p>By default, WebView scales a web page so that it is drawn at a size that matches the default * appearance on a medium density screen. So, it applies 1.5x scaling on a high density screen * (because its pixels are smaller) and 0.75x scaling on a low density screen (because its pixels * are bigger). * Starting with API level {@link android.os.Build.VERSION_CODES#ECLAIR}, WebView supports DOM, CSS, * and meta tag features to help you (as a web developer) target screens with different screen - * densities.</p> - * <p>Here's a summary of the features you can use to handle different screen densities:</p> + * densities. + * <p>Here's a summary of the features you can use to handle different screen densities: * <ul> * <li>The {@code window.devicePixelRatio} DOM property. The value of this property specifies the * default scaling factor used for the current device. For example, if the value of {@code @@ -244,7 +250,7 @@ import java.util.Map; * <pre> * <link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio:1.5)" href="hdpi.css" /></pre> * <p>The {@code hdpi.css} stylesheet is only used for devices with a screen pixel ration of 1.5, - * which is the high density pixel ratio.</p> + * which is the high density pixel ratio. * </li> * </ul> * @@ -252,7 +258,6 @@ import java.util.Map; * * <p>In order to support inline HTML5 video in your application you need to have hardware * acceleration turned on. - * </p> * * <h3>Full screen support</h3> * @@ -263,7 +268,6 @@ import java.util.Map; * missing then the web contents will not be allowed to enter full screen. Optionally you can implement * {@link WebChromeClient#getVideoLoadingProgressView()} to customize the View displayed whilst a video * is loading. - * </p> * * <h3>HTML5 Geolocation API support</h3> * @@ -273,7 +277,6 @@ import java.util.Map; * origins are automatically denied without invoking the corresponding * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback)} * method. - * </p> * * <h3>Layout size</h3> * <p> @@ -284,7 +287,6 @@ import java.util.Map; * for the height none of the WebView's parents should use a * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} layout height since that could result in * incorrect sizing of the views. - * </p> * * <p>Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} * enables the following behaviors: @@ -294,13 +296,11 @@ import java.util.Map; * <li>For applications targeting {@link android.os.Build.VERSION_CODES#KITKAT} and earlier SDKs the * HTML viewport meta tag will be ignored in order to preserve backwards compatibility. </li> * </ul> - * </p> * * <p> * Using a layout width of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} is not * supported. If such a width is used the WebView will attempt to use the width of the parent * instead. - * </p> * * <h3>Metrics</h3> * @@ -308,32 +308,38 @@ 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> * <p> * Data will only be uploaded for a given app if the user has consented AND the app has not opted * out. - * </p> * * <h3>Safe Browsing</h3> * * <p> * 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> * <p> - * The recommended way for apps to enable the feature is putting the following tag in the manifest: - * </p> + * 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> - * </p> * */ // Implementation notes. @@ -405,7 +411,7 @@ public class WebView extends AbsoluteLayout * may be notified multiple times while the * operation is underway, and the numberOfMatches * value should not be considered final unless - * isDoneCounting is true. + * isDoneCounting is {@code true}. */ public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting); @@ -439,11 +445,11 @@ public class WebView extends AbsoluteLayout * @param view the WebView that owns the picture * @param picture the new picture. Applications targeting * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or above - * will always receive a null Picture. + * will always receive a {@code null} Picture. * @deprecated Deprecated due to internal changes. */ @Deprecated - public void onNewPicture(WebView view, Picture picture); + void onNewPicture(WebView view, @Nullable Picture picture); } public static class HitTestResult { @@ -529,20 +535,25 @@ public class WebView extends AbsoluteLayout /** * Gets additional type-dependant information about the result. See - * {@link WebView#getHitTestResult()} for details. May either be null + * {@link WebView#getHitTestResult()} for details. May either be {@code null} * or contain extra information about this result. * * @return additional type-dependant information about the result */ + @Nullable public String getExtra() { return mExtra; } } /** - * 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); @@ -551,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) { @@ -561,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 @@ -574,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 @@ -591,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 @@ -616,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 @@ -664,7 +675,7 @@ public class WebView extends AbsoluteLayout * Specifies whether the horizontal scrollbar has overlay style. * * @deprecated This method has no effect. - * @param overlay true if horizontal scrollbar should have overlay style + * @param overlay {@code true} if horizontal scrollbar should have overlay style */ @Deprecated public void setHorizontalScrollbarOverlay(boolean overlay) { @@ -674,7 +685,7 @@ public class WebView extends AbsoluteLayout * Specifies whether the vertical scrollbar has overlay style. * * @deprecated This method has no effect. - * @param overlay true if vertical scrollbar should have overlay style + * @param overlay {@code true} if vertical scrollbar should have overlay style */ @Deprecated public void setVerticalScrollbarOverlay(boolean overlay) { @@ -684,7 +695,7 @@ public class WebView extends AbsoluteLayout * Gets whether horizontal scrollbar has overlay style. * * @deprecated This method is now obsolete. - * @return true + * @return {@code true} */ @Deprecated public boolean overlayHorizontalScrollbar() { @@ -696,7 +707,7 @@ public class WebView extends AbsoluteLayout * Gets whether vertical scrollbar has overlay style. * * @deprecated This method is now obsolete. - * @return false + * @return {@code false} */ @Deprecated public boolean overlayVerticalScrollbar() { @@ -717,11 +728,12 @@ public class WebView extends AbsoluteLayout } /** - * Gets the SSL certificate for the main top-level page or null if there is + * Gets the SSL certificate for the main top-level page or {@code null} if there is * no certificate (the site is not secure). * * @return the SSL certificate for the main top-level page */ + @Nullable public SslCertificate getCertificate() { checkThread(); return mProvider.getCertificate(); @@ -785,11 +797,12 @@ public class WebView extends AbsoluteLayout * @param host the host to which the credentials apply * @param realm the realm to which the credentials apply * @return the credentials as a String array, if found. The first element - * is the username and the second element is the password. Null if + * is the username and the second element is the password. {@code null} if * no credentials are found. * @deprecated Use {@link WebViewDatabase#getHttpAuthUsernamePassword} instead */ @Deprecated + @Nullable public String[] getHttpAuthUsernamePassword(String host, String realm) { checkThread(); return mProvider.getHttpAuthUsernamePassword(host, realm); @@ -858,9 +871,10 @@ public class WebView extends AbsoluteLayout * called. * * @param outState the Bundle to store this WebView's state - * @return the same copy of the back/forward list used to save the state. If - * saveState fails, the returned list will be null. + * @return the same copy of the back/forward list used to save the state, {@code null} if the + * method fails. */ + @Nullable public WebBackForwardList saveState(Bundle outState) { checkThread(); return mProvider.saveState(outState); @@ -872,7 +886,7 @@ public class WebView extends AbsoluteLayout * @param b a Bundle to store the display data * @param dest the file to store the serialized picture data. Will be * overwritten with this WebView's picture data. - * @return true if the picture was successfully saved + * @return {@code true} if the picture was successfully saved * @deprecated This method is now obsolete. * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} */ @@ -889,7 +903,7 @@ public class WebView extends AbsoluteLayout * * @param b a Bundle containing the saved display data * @param src the file where the picture data was stored - * @return true if the picture was successfully restored + * @return {@code true} if the picture was successfully restored * @deprecated This method is now obsolete. * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} */ @@ -909,8 +923,9 @@ public class WebView extends AbsoluteLayout * display data for this WebView. * * @param inState the incoming Bundle of state - * @return the restored back/forward list or null if restoreState failed + * @return the restored back/forward list or {@code null} if restoreState failed */ + @Nullable public WebBackForwardList restoreState(Bundle inState) { checkThread(); return mProvider.restoreState(inState); @@ -977,7 +992,7 @@ public class WebView extends AbsoluteLayout * The encoding parameter specifies whether the data is base64 or URL * encoded. If the data is base64 encoded, the value of the encoding * parameter must be 'base64'. For all other values of the parameter, - * including null, it is assumed that the data uses ASCII encoding for + * including {@code null}, it is assumed that the data uses ASCII encoding for * octets inside the range of safe URL characters and use the standard %xx * hex encoding of URLs for octets outside that range. For example, '#', * '%', '\', '?' should be replaced by %23, %25, %27, %3f respectively. @@ -990,10 +1005,11 @@ public class WebView extends AbsoluteLayout * always overrides that specified in the HTML or XML document itself. * * @param data a String of data in the given encoding - * @param mimeType the MIME type of the data, e.g. 'text/html' + * @param mimeType the MIMEType of the data, e.g. 'text/html'. If {@code null}, + * defaults to 'text/html'. * @param encoding the encoding of the data */ - public void loadData(String data, String mimeType, String encoding) { + public void loadData(String data, @Nullable String mimeType, @Nullable String encoding) { checkThread(); mProvider.loadData(data, mimeType, encoding); } @@ -1018,17 +1034,17 @@ public class WebView extends AbsoluteLayout * Note that the baseUrl is sent in the 'Referer' HTTP header when * requesting subresources (images, etc.) of the page loaded using this method. * - * @param baseUrl the URL to use as the page's base URL. If null defaults to + * @param baseUrl the URL to use as the page's base URL. If {@code null} defaults to * 'about:blank'. * @param data a String of data in the given encoding - * @param mimeType the MIMEType of the data, e.g. 'text/html'. If null, + * @param mimeType the MIMEType of the data, e.g. 'text/html'. If {@code null}, * defaults to 'text/html'. * @param encoding the encoding of the data - * @param historyUrl the URL to use as the history entry. If null defaults + * @param historyUrl the URL to use as the history entry. If {@code null} defaults * to 'about:blank'. If non-null, this must be a valid URL. */ - public void loadDataWithBaseURL(String baseUrl, String data, - String mimeType, String encoding, String historyUrl) { + public void loadDataWithBaseURL(@Nullable String baseUrl, String data, + @Nullable String mimeType, @Nullable String encoding, @Nullable String historyUrl) { checkThread(); mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } @@ -1048,9 +1064,9 @@ public class WebView extends AbsoluteLayout * @param script the JavaScript to execute. * @param resultCallback A callback to be invoked when the script execution * completes with the result of the execution (if any). - * May be null if no notification of the result is required. + * May be {@code null} if no notification of the result is required. */ - public void evaluateJavascript(String script, ValueCallback<String> resultCallback) { + public void evaluateJavascript(String script, @Nullable ValueCallback<String> resultCallback) { checkThread(); mProvider.evaluateJavaScript(script, resultCallback); } @@ -1069,15 +1085,16 @@ public class WebView extends AbsoluteLayout * Saves the current view as a web archive. * * @param basename the filename where the archive should be placed - * @param autoname if false, takes basename to be a file. If true, basename + * @param autoname if {@code false}, takes basename to be a file. If {@code true}, basename * is assumed to be a directory in which a filename will be * chosen according to the URL of the current page. * @param callback called after the web archive has been saved. The * parameter for onReceiveValue will either be the filename - * under which the file was saved, or null if saving the + * under which the file was saved, or {@code null} if saving the * file failed. */ - public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback) { + public void saveWebArchive(String basename, boolean autoname, @Nullable ValueCallback<String> + callback) { checkThread(); mProvider.saveWebArchive(basename, autoname, callback); } @@ -1101,7 +1118,7 @@ public class WebView extends AbsoluteLayout /** * Gets whether this WebView has a back history item. * - * @return true iff this WebView has a back history item + * @return {@code true} if this WebView has a back history item */ public boolean canGoBack() { checkThread(); @@ -1119,7 +1136,7 @@ public class WebView extends AbsoluteLayout /** * Gets whether this WebView has a forward history item. * - * @return true iff this WebView has a forward history item + * @return {@code true} if this WebView has a forward history item */ public boolean canGoForward() { checkThread(); @@ -1170,8 +1187,8 @@ public class WebView extends AbsoluteLayout /** * Scrolls the contents of this WebView up by half the view size. * - * @param top true to jump to the top of the page - * @return true if the page was scrolled + * @param top {@code true} to jump to the top of the page + * @return {@code true} if the page was scrolled */ public boolean pageUp(boolean top) { checkThread(); @@ -1181,8 +1198,8 @@ public class WebView extends AbsoluteLayout /** * Scrolls the contents of this WebView down by half the page size. * - * @param bottom true to jump to bottom of page - * @return true if the page was scrolled + * @param bottom {@code true} to jump to bottom of page + * @return {@code true} if the page was scrolled */ public boolean pageDown(boolean bottom) { checkThread(); @@ -1197,11 +1214,11 @@ public class WebView extends AbsoluteLayout * immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents of * the DOM at the current time are ready to be drawn the next time the {@link WebView} - * draws.</p> + * draws. * * <p>The next draw after the callback completes is guaranteed to reflect all the updates to the * DOM up to the point at which the {@link VisualStateCallback} was posted, but it may also - * contain updates applied after the callback was posted.</p> + * contain updates applied after the callback was posted. * * <p>The state of the DOM covered by this API includes the following: * <ul> @@ -1214,7 +1231,7 @@ public class WebView extends AbsoluteLayout * It does not include the state of: * <ul> * <li>the video tag</li> - * </ul></p> + * </ul> * * <p>To guarantee that the {@link WebView} will successfully render the first frame * after the {@link VisualStateCallback#onComplete} method has been called a set of conditions @@ -1230,11 +1247,11 @@ public class WebView extends AbsoluteLayout * {@link AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be set to fixed * values and must be made {@link View#VISIBLE VISIBLE} from the * {@link VisualStateCallback#onComplete} method.</li> - * </ul></p> + * </ul> * * <p>When using this API it is also recommended to enable pre-rasterization if the {@link * WebView} is off screen to avoid flickering. See {@link WebSettings#setOffscreenPreRaster} for - * more details and do consider its caveats.</p> + * more details and do consider its caveats. * * @param requestId An id that will be returned in the callback to allow callers to match * requests with callbacks. @@ -1339,7 +1356,7 @@ public class WebView extends AbsoluteLayout * If the content fits into the WebView control by width, then * the zoom is set to 100%. For wide content, the behavior * depends on the state of {@link WebSettings#getLoadWithOverviewMode()}. - * If its value is true, the content will be zoomed out to be fit + * If its value is {@code true}, the content will be zoomed out to be fit * by width into the WebView control, otherwise not. * * If initial scale is greater than 0, WebView starts with this value @@ -1389,7 +1406,7 @@ public class WebView extends AbsoluteLayout /** * Requests the anchor or image element URL at the last tapped point. - * If hrefMsg is null, this method returns immediately and does not + * If hrefMsg is {@code null}, this method returns immediately and does not * dispatch hrefMsg to its target. If the tapped point hits an image, * an anchor, or an image in an anchor, the message associates * strings in named keys in its data. The value paired with the key @@ -1400,7 +1417,7 @@ public class WebView extends AbsoluteLayout * returns the anchor's href attribute. "title" returns the * anchor's text. "src" returns the image's src attribute. */ - public void requestFocusNodeHref(Message hrefMsg) { + public void requestFocusNodeHref(@Nullable Message hrefMsg) { checkThread(); mProvider.requestFocusNodeHref(hrefMsg); } @@ -1410,7 +1427,7 @@ public class WebView extends AbsoluteLayout * to its target with a String representing the URL as its object. * * @param msg the message to be dispatched with the result of the request - * as the data member with "url" as key. The result can be null. + * as the data member with "url" as key. The result can be {@code null}. */ public void requestImageRef(Message msg) { checkThread(); @@ -1553,7 +1570,7 @@ public class WebView extends AbsoluteLayout /** * Gets whether this WebView is paused, meaning onPause() was called. - * Calling onResume() sets the paused state back to false. + * Calling onResume() sets the paused state back to {@code false}. * * @hide */ @@ -1577,7 +1594,7 @@ public class WebView extends AbsoluteLayout * Clears the resource cache. Note that the cache is per-application, so * this will clear the cache for all WebViews used. * - * @param includeDiskFiles if false, only the RAM cache is cleared + * @param includeDiskFiles if {@code false}, only the RAM cache is cleared */ public void clearCache(boolean includeDiskFiles) { checkThread(); @@ -1620,10 +1637,9 @@ public class WebView extends AbsoluteLayout * shared by all the WebViews that are created by the embedder application. * * @param onCleared A runnable to be invoked when client certs are cleared. - * The embedder can pass null if not interested in the - * callback. The runnable will be called in UI thread. + * The runnable will be called in UI thread. */ - public static void clearClientCertPreferences(Runnable onCleared) { + public static void clearClientCertPreferences(@Nullable Runnable onCleared) { getFactory().getStatics().clearClientCertPreferences(onCleared); } @@ -1645,7 +1661,8 @@ public class WebView extends AbsoluteLayout * @param callback will be called on the UI thread with {@code true} if initialization is * successful, {@code false} otherwise. */ - public static void startSafeBrowsing(Context context, ValueCallback<Boolean> callback) { + public static void startSafeBrowsing(Context context, + @Nullable ValueCallback<Boolean> callback) { getFactory().getStatics().initSafeBrowsing(context, callback); } @@ -1665,9 +1682,9 @@ public class WebView extends AbsoluteLayout * All other rules, including wildcards, are invalid. * * @param urls the list of URLs - * @param callback will be called with true if URLs are successfully added to the whitelist. - * It will be called with false if any URLs are malformed. The callback will be run on - * the UI thread + * @param callback will be called with {@code true} if URLs are successfully added to the + * whitelist. It will be called with {@code false} if any URLs are malformed. The callback will + * be run on the UI thread */ public static void setSafeBrowsingWhitelist(@NonNull List<String> urls, @Nullable ValueCallback<Boolean> callback) { @@ -1761,15 +1778,15 @@ public class WebView extends AbsoluteLayout * @param text if non-null, will be the initial text to search for. * Otherwise, the last String searched for in this WebView will * be used to start. - * @param showIme if true, show the IME, assuming the user will begin typing. - * If false and text is non-null, perform a find all. - * @return true if the find dialog is shown, false otherwise + * @param showIme if {@code true}, show the IME, assuming the user will begin typing. + * If {@code false} and text is non-null, perform a find all. + * @return {@code true} if the find dialog is shown, {@code false} otherwise * @deprecated This method does not work reliably on all Android versions; * implementing a custom find dialog using WebView.findAllAsync() * provides a more robust solution. */ @Deprecated - public boolean showFindDialog(String text, boolean showIme) { + public boolean showFindDialog(@Nullable String text, boolean showIme) { checkThread(); return mProvider.showFindDialog(text, showIme); } @@ -1794,8 +1811,9 @@ public class WebView extends AbsoluteLayout * five digits. * * @param addr the string to search for addresses - * @return the address, or if no address is found, null + * @return the address, or if no address is found, {@code null} */ + @Nullable public static String findAddress(String addr) { // TODO: Rewrite this in Java so it is not needed to start up chromium // Could also be deprecated @@ -1893,9 +1911,10 @@ public class WebView extends AbsoluteLayout /** * Gets the chrome handler. * - * @return the WebChromeClient, or null if not yet set + * @return the WebChromeClient, or {@code null} if not yet set * @see #setWebChromeClient */ + @Nullable public WebChromeClient getWebChromeClient() { checkThread(); return mProvider.getWebChromeClient(); @@ -1963,7 +1982,7 @@ public class WebView extends AbsoluteLayout * </ul> * * @param object the Java object to inject into this WebView's JavaScript - * context. Null values are ignored. + * context. {@code null} values are ignored. * @param name the name used to expose the object in JavaScript */ public void addJavascriptInterface(Object object, String name) { @@ -1978,7 +1997,7 @@ public class WebView extends AbsoluteLayout * * @param name the name used to expose the object in JavaScript */ - public void removeJavascriptInterface(String name) { + public void removeJavascriptInterface(@NonNull String name) { checkThread(); mProvider.removeJavascriptInterface(name); } @@ -1990,7 +2009,7 @@ public class WebView extends AbsoluteLayout * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here * </a> * - * <p>The returned message channels are entangled and already in started state.</p> + * <p>The returned message channels are entangled and already in started state. * * @return the two message ports that form the message channel. */ @@ -2035,7 +2054,7 @@ public class WebView extends AbsoluteLayout * code running inside WebViews. Please refer to WebView documentation * for the debugging guide. * - * The default is false. + * The default is {@code false}. * * @param enabled whether to enable web contents debugging */ @@ -2105,7 +2124,7 @@ public class WebView extends AbsoluteLayout } /** - * @deprecated Only the default case, true, will be supported in a future version. + * @deprecated Only the default case, {@code true}, will be supported in a future version. */ @Deprecated public void setMapTrackballToArrowKeys(boolean setMap) { @@ -2140,7 +2159,7 @@ public class WebView extends AbsoluteLayout /** * Gets whether this WebView can be zoomed in. * - * @return true if this WebView can be zoomed in + * @return {@code true} if this WebView can be zoomed in * * @deprecated This method is prone to inaccuracy due to race conditions * between the web rendering and UI threads; prefer @@ -2155,7 +2174,7 @@ public class WebView extends AbsoluteLayout /** * Gets whether this WebView can be zoomed out. * - * @return true if this WebView can be zoomed out + * @return {@code true} if this WebView can be zoomed out * * @deprecated This method is prone to inaccuracy due to race conditions * between the web rendering and UI threads; prefer @@ -2185,7 +2204,7 @@ public class WebView extends AbsoluteLayout /** * Performs zoom in in this WebView. * - * @return true if zoom in succeeds, false if no zoom changes + * @return {@code true} if zoom in succeeds, {@code false} if no zoom changes */ public boolean zoomIn() { checkThread(); @@ -2195,7 +2214,7 @@ public class WebView extends AbsoluteLayout /** * Performs zoom out in this WebView. * - * @return true if zoom out succeeds, false if no zoom changes + * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes */ public boolean zoomOut() { checkThread(); @@ -2286,7 +2305,7 @@ public class WebView extends AbsoluteLayout * * @param rendererRequestedPriority the minimum priority at which * this WebView desires the renderer process to be bound. - * @param waivedWhenNotVisible if true, this flag specifies that + * @param waivedWhenNotVisible if {@code true}, this flag specifies that * when this WebView is not visible, it will be treated as * if it had requested a priority of * {@link #RENDERER_PRIORITY_WAIVED}. @@ -2739,7 +2758,9 @@ public class WebView extends AbsoluteLayout * <p>For example, an HTML form with 2 fields for username and password: * * <pre class="prettyprint"> + * <label>Username:</label> * <input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username"> + * <label>Password:</label> * <input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password"> * </pre> * @@ -2753,6 +2774,7 @@ public class WebView extends AbsoluteLayout * username.setHtmlInfo(username.newHtmlInfoBuilder("input") * .addAttribute("type", "text") * .addAttribute("name", "username") + * .addAttribute("label", "Username:") * .build()); * username.setHint("Email or username"); * username.setAutofillType(View.AUTOFILL_TYPE_TEXT); @@ -2766,6 +2788,7 @@ public class WebView extends AbsoluteLayout * password.setHtmlInfo(password.newHtmlInfoBuilder("input") * .addAttribute("type", "password") * .addAttribute("name", "password") + * .addAttribute("label", "Password:") * .build()); * password.setHint("Password"); * password.setAutofillType(View.AUTOFILL_TYPE_TEXT); @@ -2959,16 +2982,21 @@ public class WebView extends AbsoluteLayout * uninstalled. It can also be changed through a Developer Setting. * If the WebView package changes, any app process that has loaded WebView will be killed. The * next time the app starts and loads WebView it will use the new WebView package instead. - * @return the current WebView package, or null if there is none. + * @return the current WebView package, or {@code null} if there is none. */ + @Nullable public static PackageInfo getCurrentWebViewPackage() { PackageInfo webviewPackage = WebViewFactory.getLoadedPackageInfo(); if (webviewPackage != null) { return webviewPackage; } + IWebViewUpdateService service = WebViewFactory.getUpdateService(); + if (service == null) { + return null; + } try { - return WebViewFactory.getUpdateService().getCurrentWebViewPackage(); + return service.getCurrentWebViewPackage(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java index cbe75c405fe4..46c39834060b 100644 --- a/core/java/android/webkit/WebViewClient.java +++ b/core/java/android/webkit/WebViewClient.java @@ -17,6 +17,7 @@ package android.webkit; import android.annotation.IntDef; +import android.annotation.Nullable; import android.graphics.Bitmap; import android.net.http.SslError; import android.os.Message; @@ -33,15 +34,15 @@ public class WebViewClient { * Give the host application a chance to take over the control when a new * url is about to be loaded in the current WebView. If WebViewClient is not * provided, by default WebView will ask Activity Manager to choose the - * proper handler for the url. If WebViewClient is provided, return true - * means the host application handles the url, while return false means the + * proper handler for the url. If WebViewClient is provided, return {@code true} + * means the host application handles the url, while return {@code false} means the * current WebView handles the url. * This method is not called for requests using the POST "method". * * @param view The WebView that is initiating the callback. * @param url The url to be loaded. - * @return True if the host application wants to leave the current WebView - * and handle the url itself, otherwise return false. + * @return {@code true} if the host application wants to leave the current WebView + * and handle the url itself, otherwise return {@code false}. * @deprecated Use {@link #shouldOverrideUrlLoading(WebView, WebResourceRequest) * shouldOverrideUrlLoading(WebView, WebResourceRequest)} instead. */ @@ -54,8 +55,8 @@ public class WebViewClient { * Give the host application a chance to take over the control when a new * url is about to be loaded in the current WebView. If WebViewClient is not * provided, by default WebView will ask Activity Manager to choose the - * proper handler for the url. If WebViewClient is provided, return true - * means the host application handles the url, while return false means the + * proper handler for the url. If WebViewClient is provided, return {@code true} + * means the host application handles the url, while return {@code false} means the * current WebView handles the url. * * <p>Notes: @@ -63,15 +64,14 @@ public class WebViewClient { * <li>This method is not called for requests using the POST "method".</li> * <li>This method is also called for subframes with non-http schemes, thus it is * strongly disadvised to unconditionally call {@link WebView#loadUrl(String)} - * with the request's url from inside the method and then return true, + * with the request's url from inside the method and then return {@code true}, * as this will make WebView to attempt loading a non-http url, and thus fail.</li> * </ul> - * </p> * * @param view The WebView that is initiating the callback. * @param request Object containing the details of the request. - * @return True if the host application wants to leave the current WebView - * and handle the url itself, otherwise return false. + * @return {@code true} if the host application wants to leave the current WebView + * and handle the url itself, otherwise return {@code false}. */ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { return shouldOverrideUrlLoading(view, request.getUrl().toString()); @@ -130,15 +130,15 @@ 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.</p> + * CSS and images) may not be available. * * <p>For more fine-grained notification of visual state updates, see {@link - * WebView#postVisualStateCallback}.</p> + * WebView#postVisualStateCallback}. * * <p>Please note that all the conditions and recommendations applicable to - * {@link WebView#postVisualStateCallback} also apply to this API.<p> + * {@link WebView#postVisualStateCallback} also apply to this API. * - * <p>This callback is only called for main frame navigations.</p> + * <p>This callback is only called for main frame navigations. * * @param view The {@link android.webkit.WebView} for which the navigation occurred. * @param url The URL corresponding to the page navigation that triggered this callback. @@ -148,26 +148,29 @@ public class WebViewClient { /** * Notify the host application of a resource request and allow the - * application to return the data. If the return value is null, the WebView + * 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. * @param url The raw url of the resource. * @return A {@link android.webkit.WebResourceResponse} containing the - * response information or null if the WebView should load the + * response information or {@code null} if the WebView should load the * resource itself. * @deprecated Use {@link #shouldInterceptRequest(WebView, WebResourceRequest) * shouldInterceptRequest(WebView, WebResourceRequest)} instead. */ @Deprecated + @Nullable public WebResourceResponse shouldInterceptRequest(WebView view, String url) { return null; @@ -175,23 +178,26 @@ public class WebViewClient { /** * Notify the host application of a resource request and allow the - * application to return the data. If the return value is null, the WebView + * 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. * @param request Object containing the details of the request. * @return A {@link android.webkit.WebResourceResponse} containing the - * response information or null if the WebView should load the + * response information or {@code null} if the WebView should load the * resource itself. */ + @Nullable public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { return shouldInterceptRequest(view, request.getUrl().toString()); @@ -246,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 */ @@ -270,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. @@ -287,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()) { @@ -304,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) { @@ -334,7 +340,7 @@ public class WebViewClient { * * @param view The WebView that is initiating the callback. * @param url The url being visited. - * @param isReload True if this url is being reloaded. + * @param isReload {@code true} if this url is being reloaded. */ public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { @@ -358,13 +364,13 @@ public class WebViewClient { } /** - * Notify the host application to handle a SSL client certificate - * request. The host application is responsible for showing the UI - * 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 - * same host and port pair. Webview does not store the response if ignore() + * Notify the host application to handle a SSL client certificate request. The host application + * is responsible for showing the UI if desired and providing the keys. There are three ways to + * respond: {@link ClientCertRequest#proceed}, {@link ClientCertRequest#cancel}, or {@link + * ClientCertRequest#ignore}. Webview stores the response in memory (for the life of the + * application) if {@link ClientCertRequest#proceed} or {@link ClientCertRequest#cancel} is + * called and does not call {@code onReceivedClientCertRequest()} again for the same host and + * port pair. Webview does not store the response if {@link ClientCertRequest#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 * effort. @@ -414,14 +420,14 @@ public class WebViewClient { /** * Give the host application a chance to handle the key event synchronously. * e.g. menu shortcut key events need to be filtered this way. If return - * true, WebView will not handle the key event. If return false, WebView + * true, WebView will not handle the key event. If return {@code false}, WebView * will always handle the key event, so none of the super in the view chain - * will see the key event. The default behavior returns false. + * will see the key event. The default behavior returns {@code false}. * * @param view The WebView that is initiating the callback. * @param event The key event. - * @return True if the host application wants to handle the key event - * itself, otherwise return false + * @return {@code true} if the host application wants to handle the key event + * itself, otherwise return {@code false} */ public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { return false; @@ -430,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 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. * @@ -444,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 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. * @@ -491,17 +497,17 @@ public class WebViewClient { * user has been processed. * @param view The WebView requesting the login. * @param realm The account realm used to look up accounts. - * @param account An optional account. If not null, the account should be + * @param account An optional account. If not {@code null}, the account should be * checked against accounts on the device. If it is a valid * account, it should be used to log in the user. * @param args Authenticator specific arguments used to log in the user. */ public void onReceivedLoginRequest(WebView view, String realm, - String account, String args) { + @Nullable String account, String args) { } /** - * 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. @@ -511,16 +517,16 @@ 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. * * @param view The WebView which needs to be cleaned up. * @param detail the reason why it exited. - * @return true if the host application handled the situation that process has + * @return {@code true} if the host application handled the situation that process has * exited, otherwise, application will crash if render process crashed, * or be killed if render process was killed by the system. */ @@ -535,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/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index 982c57b76b51..f6166c58a4c9 100644 --- a/core/java/android/webkit/WebViewDatabase.java +++ b/core/java/android/webkit/WebViewDatabase.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.content.Context; /** @@ -42,7 +43,7 @@ public abstract class WebViewDatabase { * Gets whether there are any saved username/password pairs for web forms. * Note that these are unrelated to HTTP authentication credentials. * - * @return true if there are any saved username/password pairs + * @return {@code true} if there are any saved username/password pairs * @see WebView#savePassword * @see #clearUsernamePassword * @deprecated Saving passwords in WebView will not be supported in future versions. @@ -129,12 +130,13 @@ public abstract class WebViewDatabase { * @param host the host to which the credentials apply * @param realm the realm to which the credentials apply * @return the credentials as a String array, if found. The first element - * is the username and the second element is the password. Null if + * is the username and the second element is the password. {@code null} if * no credentials are found. * @see #setHttpAuthUsernamePassword * @see #hasHttpAuthUsernamePassword * @see #clearHttpAuthUsernamePassword */ + @Nullable public abstract String[] getHttpAuthUsernamePassword(String host, String realm); /** diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index 92d0d7141370..73399313cbb5 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -68,22 +68,22 @@ public final class WebViewDelegate { } /** - * Returns true if the WebView trace tag is enabled and false otherwise. + * Returns {@code true} if the WebView trace tag is enabled and {@code false} otherwise. */ public boolean isTraceTagEnabled() { return Trace.isTagEnabled(Trace.TRACE_TAG_WEBVIEW); } /** - * Returns true if the draw GL functor can be invoked (see {@link #invokeDrawGlFunctor}) - * and false otherwise. + * Returns {@code true} if the draw GL functor can be invoked (see {@link #invokeDrawGlFunctor}) + * and {@code false} otherwise. */ public boolean canInvokeDrawGlFunctor(View containerView) { return true; } /** - * Invokes the draw GL functor. If waitForCompletion is false the functor + * Invokes the draw GL functor. If waitForCompletion is {@code false} the functor * may be invoked asynchronously. * * @param nativeDrawGLFunctor the pointer to the native functor that implements diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 668cfba94071..9db0e8d9a2fe 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -25,7 +25,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; -import android.os.Build; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; @@ -51,9 +50,6 @@ public final class WebViewFactory { private static final String CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"; - private static final String NULL_WEBVIEW_FACTORY = - "com.android.webview.nullwebview.NullWebViewFactoryProvider"; - public static final String CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY = "persist.sys.webview.vmsize"; @@ -66,6 +62,7 @@ public final class WebViewFactory { private static WebViewFactoryProvider sProviderInstance; private static final Object sProviderLock = new Object(); private static PackageInfo sPackageInfo; + private static Boolean sWebViewSupported; // Error codes for loadWebViewNativeLibraryFromPackage public static final int LIBLOAD_SUCCESS = 0; @@ -105,6 +102,16 @@ public final class WebViewFactory { public MissingWebViewPackageException(Exception e) { super(e); } } + private static boolean isWebViewSupported() { + // No lock; this is a benign race as Boolean's state is final and the PackageManager call + // will always return the same value. + if (sWebViewSupported == null) { + sWebViewSupported = AppGlobals.getInitialApplication().getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_WEBVIEW); + } + return sWebViewSupported; + } + /** * @hide */ @@ -130,11 +137,15 @@ public final class WebViewFactory { } /** - * Load the native library for the given package name iff that package + * Load the native library for the given package name if that package * name is the same as the one providing the webview. */ public static int loadWebViewNativeLibraryFromPackage(String packageName, ClassLoader clazzLoader) { + if (!isWebViewSupported()) { + return LIBLOAD_WRONG_PACKAGE_NAME; + } + WebViewProviderResponse response = null; try { response = getUpdateService().waitForAndGetProvider(); @@ -188,6 +199,11 @@ public final class WebViewFactory { "For security reasons, WebView is not allowed in privileged processes"); } + if (!isWebViewSupported()) { + // Device doesn't support WebView; don't try to load it, just throw. + throw new UnsupportedOperationException(); + } + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()"); try { @@ -222,7 +238,7 @@ public final class WebViewFactory { } /** - * Returns true if the signatures match, false otherwise + * Returns {@code true} if the signatures match, {@code false} otherwise */ private static boolean signaturesEquals(Signature[] s1, Signature[] s2) { if (s1 == null) { @@ -249,10 +265,10 @@ public final class WebViewFactory { + "packageName mismatch, expected: " + chosen.packageName + " actual: " + toUse.packageName); } - if (chosen.versionCode > toUse.versionCode) { + if (chosen.getLongVersionCode() > toUse.getLongVersionCode()) { throw new MissingWebViewPackageException("Failed to verify WebView provider, " - + "version code is lower than expected: " + chosen.versionCode - + " actual: " + toUse.versionCode); + + "version code is lower than expected: " + chosen.getLongVersionCode() + + " actual: " + toUse.getLongVersionCode()); } if (getWebViewLibrary(toUse.applicationInfo) == null) { throw new MissingWebViewPackageException("Tried to load an invalid WebView provider: " @@ -385,7 +401,7 @@ public final class WebViewFactory { Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); } Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " + - sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")"); + sPackageInfo.versionName + " (code " + sPackageInfo.getLongVersionCode() + ")"); Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()"); try { @@ -410,15 +426,6 @@ public final class WebViewFactory { Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); } } catch (MissingWebViewPackageException e) { - // If the package doesn't exist, then try loading the null WebView instead. - // If that succeeds, then this is a device without WebView support; if it fails then - // swallow the failure, complain that the real WebView is missing and rethrow the - // original exception. - try { - return (Class<WebViewFactoryProvider>) Class.forName(NULL_WEBVIEW_FACTORY); - } catch (ClassNotFoundException e2) { - // Ignore. - } Log.e(LOGTAG, "Chromium WebView package does not exist", e); throw new AndroidRuntimeException(e); } @@ -437,52 +444,40 @@ public final class WebViewFactory { } } - private static int prepareWebViewInSystemServer(String[] nativeLibraryPaths) { - if (DEBUG) Log.v(LOGTAG, "creating relro files"); - int numRelros = 0; - - // We must always trigger createRelRo regardless of the value of nativeLibraryPaths. Any - // unexpected values will be handled there to ensure that we trigger notifying any process - // waiting on relro creation. - if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { - if (DEBUG) Log.v(LOGTAG, "Create 32 bit relro"); - WebViewLibraryLoader.createRelroFile(false /* is64Bit */, nativeLibraryPaths); - numRelros++; - } - - if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { - if (DEBUG) Log.v(LOGTAG, "Create 64 bit relro"); - WebViewLibraryLoader.createRelroFile(true /* is64Bit */, nativeLibraryPaths); - numRelros++; - } - return numRelros; - } - /** * @hide */ public static int onWebViewProviderChanged(PackageInfo packageInfo) { - String[] nativeLibs = null; - String originalSourceDir = packageInfo.applicationInfo.sourceDir; + int startedRelroProcesses = 0; + ApplicationInfo originalAppInfo = new ApplicationInfo(packageInfo.applicationInfo); try { fixupStubApplicationInfo(packageInfo.applicationInfo, AppGlobals.getInitialApplication().getPackageManager()); - nativeLibs = WebViewLibraryLoader.updateWebViewZygoteVmSize(packageInfo); + startedRelroProcesses = WebViewLibraryLoader.prepareNativeLibraries(packageInfo); } catch (Throwable t) { // Log and discard errors at this stage as we must not crash the system server. Log.e(LOGTAG, "error preparing webview native library", t); } - WebViewZygote.onWebViewProviderChanged(packageInfo, originalSourceDir); + WebViewZygote.onWebViewProviderChanged(packageInfo, originalAppInfo); - return prepareWebViewInSystemServer(nativeLibs); + return startedRelroProcesses; } private static String WEBVIEW_UPDATE_SERVICE_NAME = "webviewupdate"; /** @hide */ public static IWebViewUpdateService getUpdateService() { + if (isWebViewSupported()) { + return getUpdateServiceUnchecked(); + } else { + return null; + } + } + + /** @hide */ + static IWebViewUpdateService getUpdateServiceUnchecked() { return IWebViewUpdateService.Stub.asInterface( ServiceManager.getService(WEBVIEW_UPDATE_SERVICE_NAME)); } diff --git a/core/java/android/webkit/WebViewFragment.java b/core/java/android/webkit/WebViewFragment.java index d803f62da011..e5b7c8d23a09 100644 --- a/core/java/android/webkit/WebViewFragment.java +++ b/core/java/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/core/java/android/webkit/WebViewLibraryLoader.java b/core/java/android/webkit/WebViewLibraryLoader.java index 6f9e8ece4b13..de0b97d15e23 100644 --- a/core/java/android/webkit/WebViewLibraryLoader.java +++ b/core/java/android/webkit/WebViewLibraryLoader.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManagerInternal; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -26,6 +28,7 @@ import android.os.SystemProperties; import android.text.TextUtils; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; import dalvik.system.VMRuntime; @@ -36,7 +39,11 @@ import java.util.Arrays; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -class WebViewLibraryLoader { +/** + * @hide + */ +@VisibleForTesting +public class WebViewLibraryLoader { private static final String LOGTAG = WebViewLibraryLoader.class.getSimpleName(); private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_32 = @@ -62,25 +69,23 @@ class WebViewLibraryLoader { boolean result = false; boolean is64Bit = VMRuntime.getRuntime().is64Bit(); try { - if (args.length != 2 || args[0] == null || args[1] == null) { + if (args.length != 1 || args[0] == null) { Log.e(LOGTAG, "Invalid RelroFileCreator args: " + Arrays.toString(args)); return; } - Log.v(LOGTAG, "RelroFileCreator (64bit = " + is64Bit + "), " - + " 32-bit lib: " + args[0] + ", 64-bit lib: " + args[1]); + Log.v(LOGTAG, "RelroFileCreator (64bit = " + is64Bit + "), lib: " + args[0]); if (!sAddressSpaceReserved) { Log.e(LOGTAG, "can't create relro file; address space not reserved"); return; } - result = nativeCreateRelroFile(args[0] /* path32 */, - args[1] /* path64 */, - CHROMIUM_WEBVIEW_NATIVE_RELRO_32, - CHROMIUM_WEBVIEW_NATIVE_RELRO_64); + result = nativeCreateRelroFile(args[0] /* path */, + is64Bit ? CHROMIUM_WEBVIEW_NATIVE_RELRO_64 : + CHROMIUM_WEBVIEW_NATIVE_RELRO_32); if (result && DEBUG) Log.v(LOGTAG, "created relro file"); } finally { // We must do our best to always notify the update service, even if something fails. try { - WebViewFactory.getUpdateService().notifyRelroCreationCompleted(); + WebViewFactory.getUpdateServiceUnchecked().notifyRelroCreationCompleted(); } catch (RemoteException e) { Log.e(LOGTAG, "error notifying update service", e); } @@ -96,7 +101,7 @@ class WebViewLibraryLoader { /** * Create a single relro file by invoking an isolated process that to do the actual work. */ - static void createRelroFile(final boolean is64Bit, String[] nativeLibraryPaths) { + static void createRelroFile(final boolean is64Bit, @NonNull WebViewNativeLibrary nativeLib) { final String abi = is64Bit ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0]; @@ -114,13 +119,12 @@ class WebViewLibraryLoader { }; try { - if (nativeLibraryPaths == null - || nativeLibraryPaths[0] == null || nativeLibraryPaths[1] == null) { + if (nativeLib == null || nativeLib.path == null) { throw new IllegalArgumentException( "Native library paths to the WebView RelRo process must not be null!"); } int pid = LocalServices.getService(ActivityManagerInternal.class).startIsolatedProcess( - RelroFileCreator.class.getName(), nativeLibraryPaths, + RelroFileCreator.class.getName(), new String[] { nativeLib.path }, "WebViewLoader-" + abi, abi, Process.SHARED_RELRO_UID, crashHandler); if (pid <= 0) throw new Exception("Failed to start the relro file creator process"); } catch (Throwable t) { @@ -131,56 +135,77 @@ class WebViewLibraryLoader { } /** + * Perform preparations needed to allow loading WebView from an application. This method should + * be called whenever we change WebView provider. + * @return the number of relro processes started. + */ + static int prepareNativeLibraries(PackageInfo webviewPackageInfo) + throws WebViewFactory.MissingWebViewPackageException { + WebViewNativeLibrary nativeLib32bit = + getWebViewNativeLibrary(webviewPackageInfo, false /* is64bit */); + WebViewNativeLibrary nativeLib64bit = + getWebViewNativeLibrary(webviewPackageInfo, true /* is64bit */); + updateWebViewZygoteVmSize(nativeLib32bit, nativeLib64bit); + + return createRelros(nativeLib32bit, nativeLib64bit); + } + + /** + * @return the number of relro processes started. + */ + private static int createRelros(@Nullable WebViewNativeLibrary nativeLib32bit, + @Nullable WebViewNativeLibrary nativeLib64bit) { + if (DEBUG) Log.v(LOGTAG, "creating relro files"); + int numRelros = 0; + + if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { + if (nativeLib32bit == null) { + Log.e(LOGTAG, "No 32-bit WebView library path, skipping relro creation."); + } else { + if (DEBUG) Log.v(LOGTAG, "Create 32 bit relro"); + createRelroFile(false /* is64Bit */, nativeLib32bit); + numRelros++; + } + } + + if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { + if (nativeLib64bit == null) { + Log.e(LOGTAG, "No 64-bit WebView library path, skipping relro creation."); + } else { + if (DEBUG) Log.v(LOGTAG, "Create 64 bit relro"); + createRelroFile(true /* is64Bit */, nativeLib64bit); + numRelros++; + } + } + return numRelros; + } + + /** * * @return the native WebView libraries in the new WebView APK. */ - static String[] updateWebViewZygoteVmSize(PackageInfo packageInfo) + private static void updateWebViewZygoteVmSize( + @Nullable WebViewNativeLibrary nativeLib32bit, + @Nullable WebViewNativeLibrary nativeLib64bit) throws WebViewFactory.MissingWebViewPackageException { // Find the native libraries of the new WebView package, to change the size of the // memory region in the Zygote reserved for the library. - String[] nativeLibs = getWebViewNativeLibraryPaths(packageInfo); - if (nativeLibs != null) { - long newVmSize = 0L; - - for (String path : nativeLibs) { - if (path == null || TextUtils.isEmpty(path)) continue; - if (DEBUG) Log.d(LOGTAG, "Checking file size of " + path); - File f = new File(path); - if (f.exists()) { - newVmSize = Math.max(newVmSize, f.length()); - continue; - } - if (path.contains("!/")) { - String[] split = TextUtils.split(path, "!/"); - if (split.length == 2) { - try (ZipFile z = new ZipFile(split[0])) { - ZipEntry e = z.getEntry(split[1]); - if (e != null && e.getMethod() == ZipEntry.STORED) { - newVmSize = Math.max(newVmSize, e.getSize()); - continue; - } - } - catch (IOException e) { - Log.e(LOGTAG, "error reading APK file " + split[0] + ", ", e); - } - } - } - Log.e(LOGTAG, "error sizing load for " + path); - } + long newVmSize = 0L; - if (DEBUG) { - Log.v(LOGTAG, "Based on library size, need " + newVmSize - + " bytes of address space."); - } - // The required memory can be larger than the file on disk (due to .bss), and an - // upgraded version of the library will likely be larger, so always attempt to - // reserve twice as much as we think to allow for the library to grow during this - // boot cycle. - newVmSize = Math.max(2 * newVmSize, CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES); - Log.d(LOGTAG, "Setting new address space to " + newVmSize); - setWebViewZygoteVmSize(newVmSize); + if (nativeLib32bit != null) newVmSize = Math.max(newVmSize, nativeLib32bit.size); + if (nativeLib64bit != null) newVmSize = Math.max(newVmSize, nativeLib64bit.size); + + if (DEBUG) { + Log.v(LOGTAG, "Based on library size, need " + newVmSize + + " bytes of address space."); } - return nativeLibs; + // The required memory can be larger than the file on disk (due to .bss), and an + // upgraded version of the library will likely be larger, so always attempt to + // reserve twice as much as we think to allow for the library to grow during this + // boot cycle. + newVmSize = Math.max(2 * newVmSize, CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES); + Log.d(LOGTAG, "Setting new address space to " + newVmSize); + setWebViewZygoteVmSize(newVmSize); } /** @@ -204,7 +229,9 @@ 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. */ @@ -217,8 +244,9 @@ class WebViewLibraryLoader { final String libraryFileName = WebViewFactory.getWebViewLibrary(packageInfo.applicationInfo); - int result = nativeLoadWithRelroFile(libraryFileName, CHROMIUM_WEBVIEW_NATIVE_RELRO_32, - CHROMIUM_WEBVIEW_NATIVE_RELRO_64, clazzLoader); + String relroPath = VMRuntime.getRuntime().is64Bit() ? CHROMIUM_WEBVIEW_NATIVE_RELRO_64 : + CHROMIUM_WEBVIEW_NATIVE_RELRO_32; + int result = nativeLoadWithRelroFile(libraryFileName, relroPath, clazzLoader); if (result != WebViewFactory.LIBLOAD_SUCCESS) { Log.w(LOGTAG, "failed to load with relro file, proceeding without"); } else if (DEBUG) { @@ -229,64 +257,78 @@ class WebViewLibraryLoader { /** * Fetch WebView's native library paths from {@param packageInfo}. + * @hide */ - static String[] getWebViewNativeLibraryPaths(PackageInfo packageInfo) - throws WebViewFactory.MissingWebViewPackageException { + @Nullable + @VisibleForTesting + public static WebViewNativeLibrary getWebViewNativeLibrary(PackageInfo packageInfo, + boolean is64bit) throws WebViewFactory.MissingWebViewPackageException { ApplicationInfo ai = packageInfo.applicationInfo; final String nativeLibFileName = WebViewFactory.getWebViewLibrary(ai); - String path32; - String path64; - boolean primaryArchIs64bit = VMRuntime.is64BitAbi(ai.primaryCpuAbi); - if (!TextUtils.isEmpty(ai.secondaryCpuAbi)) { - // Multi-arch case. - if (primaryArchIs64bit) { - // Primary arch: 64-bit, secondary: 32-bit. - path64 = ai.nativeLibraryDir; - path32 = ai.secondaryNativeLibraryDir; - } else { - // Primary arch: 32-bit, secondary: 64-bit. - path64 = ai.secondaryNativeLibraryDir; - path32 = ai.nativeLibraryDir; - } - } else if (primaryArchIs64bit) { - // Single-arch 64-bit. - path64 = ai.nativeLibraryDir; - path32 = ""; - } else { - // Single-arch 32-bit. - path32 = ai.nativeLibraryDir; - path64 = ""; + String dir = getWebViewNativeLibraryDirectory(ai, is64bit /* 64bit */); + + WebViewNativeLibrary lib = findNativeLibrary(ai, nativeLibFileName, + is64bit ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS, dir); + + if (DEBUG) { + Log.v(LOGTAG, String.format("Native %d-bit lib: %s", is64bit ? 64 : 32, lib.path)); } + return lib; + } - // Form the full paths to the extracted native libraries. - // If libraries were not extracted, try load from APK paths instead. - if (!TextUtils.isEmpty(path32)) { - path32 += "/" + nativeLibFileName; - File f = new File(path32); - if (!f.exists()) { - path32 = getLoadFromApkPath(ai.sourceDir, - Build.SUPPORTED_32_BIT_ABIS, - nativeLibFileName); - } + /** + * @return the directory of the native WebView library with bitness {@param is64bit}. + * @hide + */ + @VisibleForTesting + public static String getWebViewNativeLibraryDirectory(ApplicationInfo ai, boolean is64bit) { + // Primary arch has the same bitness as the library we are looking for. + if (is64bit == VMRuntime.is64BitAbi(ai.primaryCpuAbi)) return ai.nativeLibraryDir; + + // Secondary arch has the same bitness as the library we are looking for. + if (!TextUtils.isEmpty(ai.secondaryCpuAbi)) { + return ai.secondaryNativeLibraryDir; } - if (!TextUtils.isEmpty(path64)) { - path64 += "/" + nativeLibFileName; - File f = new File(path64); - if (!f.exists()) { - path64 = getLoadFromApkPath(ai.sourceDir, - Build.SUPPORTED_64_BIT_ABIS, - nativeLibFileName); - } + + return ""; + } + + /** + * @return an object describing a native WebView library given the directory path of that + * library, or null if the library couldn't be found. + */ + @Nullable + private static WebViewNativeLibrary findNativeLibrary(ApplicationInfo ai, + String nativeLibFileName, String[] abiList, String libDirectory) + throws WebViewFactory.MissingWebViewPackageException { + if (TextUtils.isEmpty(libDirectory)) return null; + String libPath = libDirectory + "/" + nativeLibFileName; + File f = new File(libPath); + if (f.exists()) { + return new WebViewNativeLibrary(libPath, f.length()); + } else { + return getLoadFromApkPath(ai.sourceDir, abiList, nativeLibFileName); } + } - if (DEBUG) Log.v(LOGTAG, "Native 32-bit lib: " + path32 + ", 64-bit lib: " + path64); - return new String[] { path32, path64 }; + /** + * @hide + */ + @VisibleForTesting + public static class WebViewNativeLibrary { + public final String path; + public final long size; + + WebViewNativeLibrary(String path, long size) { + this.path = path; + this.size = size; + } } - private static String getLoadFromApkPath(String apkPath, - String[] abiList, - String nativeLibFileName) + private static WebViewNativeLibrary getLoadFromApkPath(String apkPath, + String[] abiList, + String nativeLibFileName) throws WebViewFactory.MissingWebViewPackageException { // Search the APK for a native library conforming to a listed ABI. try (ZipFile z = new ZipFile(apkPath)) { @@ -295,13 +337,13 @@ class WebViewLibraryLoader { ZipEntry e = z.getEntry(entry); if (e != null && e.getMethod() == ZipEntry.STORED) { // Return a path formatted for dlopen() load from APK. - return apkPath + "!/" + entry; + return new WebViewNativeLibrary(apkPath + "!/" + entry, e.getSize()); } } } catch (IOException e) { throw new WebViewFactory.MissingWebViewPackageException(e); } - return ""; + return null; } /** @@ -313,8 +355,6 @@ class WebViewLibraryLoader { } static native boolean nativeReserveAddressSpace(long addressSpaceToReserve); - static native boolean nativeCreateRelroFile(String lib32, String lib64, - String relro32, String relro64); - static native int nativeLoadWithRelroFile(String lib, String relro32, String relro64, - ClassLoader clazzLoader); + static native boolean nativeCreateRelroFile(String lib, String relro); + static native int nativeLoadWithRelroFile(String lib, String relro, ClassLoader clazzLoader); } diff --git a/core/java/android/webkit/WebViewProvider.java b/core/java/android/webkit/WebViewProvider.java index 820b49accb65..a8969252ff2e 100644 --- a/core/java/android/webkit/WebViewProvider.java +++ b/core/java/android/webkit/WebViewProvider.java @@ -19,16 +19,16 @@ package android.webkit; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; -import android.content.res.Configuration; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Picture; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.net.http.SslCertificate; import android.net.Uri; +import android.net.http.SslCertificate; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -73,7 +73,8 @@ public interface WebViewProvider { * Initialize this WebViewProvider instance. Called after the WebView has fully constructed. * @param javaScriptInterfaces is a Map of interface names, as keys, and * object implementing those interfaces, as values. - * @param privateBrowsing If true the web view will be initialized in private / incognito mode. + * @param privateBrowsing If {@code true} the web view will be initialized in private / + * incognito mode. */ public void init(Map<String, Object> javaScriptInterfaces, boolean privateBrowsing); @@ -315,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/core/java/android/webkit/WebViewUpdateService.java b/core/java/android/webkit/WebViewUpdateService.java index 2f7d6854803d..629891cca4f6 100644 --- a/core/java/android/webkit/WebViewUpdateService.java +++ b/core/java/android/webkit/WebViewUpdateService.java @@ -31,8 +31,12 @@ public final class WebViewUpdateService { * Fetch all packages that could potentially implement WebView. */ public static WebViewProviderInfo[] getAllWebViewPackages() { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return new WebViewProviderInfo[0]; + } try { - return getUpdateService().getAllWebViewPackages(); + return service.getAllWebViewPackages(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -42,8 +46,12 @@ public final class WebViewUpdateService { * Fetch all packages that could potentially implement WebView and are currently valid. */ public static WebViewProviderInfo[] getValidWebViewPackages() { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return new WebViewProviderInfo[0]; + } try { - return getUpdateService().getValidWebViewPackages(); + return service.getValidWebViewPackages(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -53,8 +61,12 @@ public final class WebViewUpdateService { * Used by DevelopmentSetting to get the name of the WebView provider currently in use. */ public static String getCurrentWebViewPackageName() { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return null; + } try { - return getUpdateService().getCurrentWebViewPackageName(); + return service.getCurrentWebViewPackageName(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java index 0204dff9bf9d..db60ad8d1c05 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -17,6 +17,7 @@ package android.webkit; import android.app.LoadedApk; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.os.Build; import android.os.SystemService; @@ -28,7 +29,6 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -48,8 +48,8 @@ public class WebViewZygote { private static final Object sLock = new Object(); /** - * Instance that maintains the socket connection to the zygote. This is null if the zygote - * is not running or is not connected. + * Instance that maintains the socket connection to the zygote. This is {@code null} if the + * zygote is not running or is not connected. */ @GuardedBy("sLock") private static ZygoteProcess sZygote; @@ -68,14 +68,14 @@ public class WebViewZygote { private static PackageInfo sPackage; /** - * Cache key for the selected WebView package's classloader. This is set from + * Original ApplicationInfo for the selected WebView package before stub fixup. This is set from * #onWebViewProviderChanged(). */ @GuardedBy("sLock") - private static String sPackageCacheKey; + private static ApplicationInfo sPackageOriginalAppInfo; /** - * Flag for whether multi-process WebView is enabled. If this is false, the zygote + * Flag for whether multi-process WebView is enabled. If this is {@code false}, the zygote * will not be started. */ @GuardedBy("sLock") @@ -126,10 +126,11 @@ public class WebViewZygote { } } - public static void onWebViewProviderChanged(PackageInfo packageInfo, String cacheKey) { + public static void onWebViewProviderChanged(PackageInfo packageInfo, + ApplicationInfo originalAppInfo) { synchronized (sLock) { sPackage = packageInfo; - sPackageCacheKey = cacheKey; + sPackageOriginalAppInfo = originalAppInfo; // If multi-process is not enabled, then do not start the zygote service. if (!sMultiprocessEnabled) { @@ -218,10 +219,17 @@ public class WebViewZygote { final String zip = (zipPaths.size() == 1) ? zipPaths.get(0) : TextUtils.join(File.pathSeparator, zipPaths); + // In the case where the ApplicationInfo has been modified by the stub WebView, + // we need to use the original ApplicationInfo to determine what the original classpath + // would have been to use as a cache key. + LoadedApk.makePaths(null, false, sPackageOriginalAppInfo, zipPaths, null); + final String cacheKey = (zipPaths.size() == 1) ? zipPaths.get(0) : + TextUtils.join(File.pathSeparator, zipPaths); + ZygoteProcess.waitForConnectionToZygote(WEBVIEW_ZYGOTE_SOCKET); Log.d(LOGTAG, "Preloading package " + zip + " " + librarySearchPath); - sZygote.preloadPackageForAbi(zip, librarySearchPath, sPackageCacheKey, + sZygote.preloadPackageForAbi(zip, librarySearchPath, cacheKey, Build.SUPPORTED_ABIS[0]); } catch (Exception e) { Log.e(LOGTAG, "Error connecting to " + serviceName, e); diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 91e2f7d4ddd0..e0c897d3e25c 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -1480,11 +1480,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te /** @hide */ @Override - public void sendAccessibilityEventInternal(int eventType) { + public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { // Since this class calls onScrollChanged even if the mFirstPosition and the // child count have not changed we will avoid sending duplicate accessibility // events. - if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { final int firstVisiblePosition = getFirstVisiblePosition(); final int lastVisiblePosition = getLastVisiblePosition(); if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition @@ -1495,7 +1495,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mLastAccessibilityScrollEventToIndex = lastVisiblePosition; } } - super.sendAccessibilityEventInternal(eventType); + super.sendAccessibilityEventUnchecked(event); } @Override @@ -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/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java index 690067b48133..f18f2172b455 100644 --- a/core/java/android/widget/ArrayAdapter.java +++ b/core/java/android/widget/ArrayAdapter.java @@ -152,7 +152,8 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSp } /** - * Constructor + * Constructor. This constructor will result in the underlying data collection being + * immutable, so methods such as {@link #clear()} will throw an exception. * * @param context The current context. * @param resource The resource ID for a layout file containing a TextView to use when @@ -164,7 +165,8 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSp } /** - * Constructor + * Constructor. This constructor will result in the underlying data collection being + * immutable, so methods such as {@link #clear()} will throw an exception. * * @param context The current context. * @param resource The resource ID for a layout file containing a layout to use when @@ -218,6 +220,7 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSp * Adds the specified object at the end of the array. * * @param object The object to add at the end of the array. + * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void add(@Nullable T object) { synchronized (mLock) { @@ -261,6 +264,7 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSp * Adds the specified items at the end of the array. * * @param items The items to add at the end of the array. + * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void addAll(T ... items) { synchronized (mLock) { @@ -279,6 +283,7 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSp * * @param object The object to insert into the array. * @param index The index at which the object must be inserted. + * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void insert(@Nullable T object, int index) { synchronized (mLock) { @@ -296,6 +301,7 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSp * Removes the specified object from the array. * * @param object The object to remove. + * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void remove(@Nullable T object) { synchronized (mLock) { @@ -311,6 +317,8 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSp /** * Remove all elements from the list. + * + * @throws UnsupportedOperationException if the underlying data collection is immutable */ public void clear() { synchronized (mLock) { diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 5f2b3d0fbf6c..e065dc119880 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -41,7 +41,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.metrics.LogMaker; import android.os.Bundle; import android.os.LocaleList; import android.os.Parcel; @@ -128,7 +127,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.List; - +import java.util.Map; /** * Helper class used by TextView to handle editable text views. @@ -138,6 +137,9 @@ import java.util.List; public class Editor { private static final String TAG = "Editor"; private static final boolean DEBUG_UNDO = false; + // Specifies whether to use or not the magnifier when pressing the insertion or selection + // handles. + private static final boolean FLAG_USE_MAGNIFIER = true; static final int BLINK = 500; private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; @@ -159,8 +161,19 @@ public class Editor { private static final int MENU_ITEM_ORDER_REPLACE = 9; private static final int MENU_ITEM_ORDER_AUTOFILL = 10; private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11; + private static final int MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START = 50; private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100; + @IntDef({MagnifierHandleTrigger.SELECTION_START, + MagnifierHandleTrigger.SELECTION_END, + MagnifierHandleTrigger.INSERTION}) + @Retention(RetentionPolicy.SOURCE) + private @interface MagnifierHandleTrigger { + int INSERTION = 0; + int SELECTION_START = 1; + int SELECTION_END = 2; + } + // Each Editor manages its own undo stack. private final UndoManager mUndoManager = new UndoManager(); private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this); @@ -179,6 +192,29 @@ public class Editor { private final boolean mHapticTextHandleEnabled; + private final Magnifier mMagnifier; + private final Runnable mUpdateMagnifierRunnable = new Runnable() { + @Override + public void run() { + mMagnifier.update(); + } + }; + // Update the magnifier contents whenever anything in the view hierarchy is updated. + // Note: this only captures UI thread-visible changes, so it's a known issue that an animating + // VectorDrawable or Ripple animation will not trigger capture, since they're owned by + // RenderThread. + private final ViewTreeObserver.OnDrawListener mMagnifierOnDrawListener = + new ViewTreeObserver.OnDrawListener() { + @Override + public void onDraw() { + if (mMagnifier != null) { + // Posting the method will ensure that updating the magnifier contents will + // happen right after the rendering of the current frame. + mTextView.post(mUpdateMagnifierRunnable); + } + } + }; + // Used to highlight a word when it is corrected by the IME private CorrectionHighlighter mCorrectionHighlighter; @@ -250,8 +286,7 @@ public class Editor { SuggestionRangeSpan mSuggestionRangeSpan; private Runnable mShowSuggestionRunnable; - final Drawable[] mCursorDrawable = new Drawable[2]; - int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split) + Drawable mDrawableForCursor = null; private Drawable mSelectHandleLeft; private Drawable mSelectHandleRight; @@ -261,6 +296,7 @@ public class Editor { private PositionListener mPositionListener; private float mLastDownPositionX, mLastDownPositionY; + private float mLastUpPositionX, mLastUpPositionY; private float mContextMenuAnchorX, mContextMenuAnchorY; Callback mCustomSelectionActionModeCallback; Callback mCustomInsertionActionModeCallback; @@ -325,6 +361,8 @@ public class Editor { mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this); mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean( com.android.internal.R.bool.config_enableHapticTextHandle); + + mMagnifier = FLAG_USE_MAGNIFIER ? new Magnifier(mTextView) : null; } ParcelableParcel saveInstanceState() { @@ -396,15 +434,21 @@ public class Editor { } final ViewTreeObserver observer = mTextView.getViewTreeObserver(); - // No need to create the controller. - // The get method will add the listener on controller creation. - if (mInsertionPointCursorController != null) { - observer.addOnTouchModeChangeListener(mInsertionPointCursorController); - } - if (mSelectionModifierCursorController != null) { - mSelectionModifierCursorController.resetTouchOffsets(); - observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); + if (observer.isAlive()) { + // No need to create the controller. + // The get method will add the listener on controller creation. + if (mInsertionPointCursorController != null) { + observer.addOnTouchModeChangeListener(mInsertionPointCursorController); + } + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.resetTouchOffsets(); + observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); + } + if (FLAG_USE_MAGNIFIER) { + observer.addOnDrawListener(mMagnifierOnDrawListener); + } } + updateSpellCheckSpans(0, mTextView.getText().length(), true /* create the spell checker if needed */); @@ -453,6 +497,13 @@ public class Editor { mSpellChecker = null; } + if (FLAG_USE_MAGNIFIER) { + final ViewTreeObserver observer = mTextView.getViewTreeObserver(); + if (observer.isAlive()) { + observer.removeOnDrawListener(mMagnifierOnDrawListener); + } + } + hideCursorAndSpanControllers(); stopTextActionModeWithPreservingSelection(); } @@ -756,14 +807,18 @@ public class Editor { } } - private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { - int wid = tv.getPaddingLeft() + tv.getPaddingRight(); - int ht = tv.getPaddingTop() + tv.getPaddingBottom(); + private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text, + @NonNull TextView tv) { + final int wid = tv.getPaddingLeft() + tv.getPaddingRight(); + final int ht = tv.getPaddingTop() + tv.getPaddingBottom(); - int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize( + final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize( com.android.internal.R.dimen.textview_error_popup_default_width); - Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels, - Layout.Alignment.ALIGN_NORMAL, 1, 0, true); + final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(), + defaultWidthInPixels) + .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing) + .build(); + float max = 0; for (int i = 0; i < l.getLineCount(); i++) { max = Math.max(max, l.getLineWidth(i)); @@ -1134,6 +1189,14 @@ public class Editor { return handled; } + float getLastUpPositionX() { + return mLastUpPositionX; + } + + float getLastUpPositionY() { + return mLastUpPositionY; + } + private long getLastTouchOffsets() { SelectionModifierCursorController selectionController = getSelectionController(); final int minOffset = selectionController.getMinTouchOffset(); @@ -1376,6 +1439,11 @@ public class Editor { mShowSuggestionRunnable = null; } + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + mLastUpPositionX = event.getX(); + mLastUpPositionY = event.getY(); + } + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mLastDownPositionX = event.getX(); mLastDownPositionY = event.getY(); @@ -1570,49 +1638,49 @@ public class Editor { outText.startOffset = 0; outText.selectionStart = mTextView.getSelectionStart(); outText.selectionEnd = mTextView.getSelectionEnd(); + outText.hint = mTextView.getHint(); return true; } boolean reportExtractedText() { final Editor.InputMethodState ims = mInputMethodState; - if (ims != null) { - final boolean contentChanged = ims.mContentChanged; - if (contentChanged || ims.mSelectionModeChanged) { - ims.mContentChanged = false; - ims.mSelectionModeChanged = false; - final ExtractedTextRequest req = ims.mExtractedTextRequest; - if (req != null) { - InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) { - if (TextView.DEBUG_EXTRACT) { - Log.v(TextView.LOG_TAG, "Retrieving extracted start=" - + ims.mChangedStart - + " end=" + ims.mChangedEnd - + " delta=" + ims.mChangedDelta); - } - if (ims.mChangedStart < 0 && !contentChanged) { - ims.mChangedStart = EXTRACT_NOTHING; - } - if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, - ims.mChangedDelta, ims.mExtractedText)) { - if (TextView.DEBUG_EXTRACT) { - Log.v(TextView.LOG_TAG, - "Reporting extracted start=" - + ims.mExtractedText.partialStartOffset - + " end=" + ims.mExtractedText.partialEndOffset - + ": " + ims.mExtractedText.text); - } - - imm.updateExtractedText(mTextView, req.token, ims.mExtractedText); - ims.mChangedStart = EXTRACT_UNKNOWN; - ims.mChangedEnd = EXTRACT_UNKNOWN; - ims.mChangedDelta = 0; - ims.mContentChanged = false; - return true; - } - } - } - } + if (ims == null) { + return false; + } + ims.mSelectionModeChanged = false; + final ExtractedTextRequest req = ims.mExtractedTextRequest; + if (req == null) { + return false; + } + final InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm == null) { + return false; + } + if (TextView.DEBUG_EXTRACT) { + Log.v(TextView.LOG_TAG, "Retrieving extracted start=" + + ims.mChangedStart + + " end=" + ims.mChangedEnd + + " delta=" + ims.mChangedDelta); + } + if (ims.mChangedStart < 0 && !ims.mContentChanged) { + ims.mChangedStart = EXTRACT_NOTHING; + } + if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, + ims.mChangedDelta, ims.mExtractedText)) { + if (TextView.DEBUG_EXTRACT) { + Log.v(TextView.LOG_TAG, + "Reporting extracted start=" + + ims.mExtractedText.partialStartOffset + + " end=" + ims.mExtractedText.partialEndOffset + + ": " + ims.mExtractedText.text); + } + + imm.updateExtractedText(mTextView, req.token, ims.mExtractedText); + ims.mChangedStart = EXTRACT_UNKNOWN; + ims.mChangedEnd = EXTRACT_UNKNOWN; + ims.mChangedDelta = 0; + ims.mContentChanged = false; + return true; } return false; } @@ -1662,7 +1730,7 @@ public class Editor { mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); } - if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) { + if (highlight != null && selectionStart == selectionEnd && mDrawableForCursor != null) { drawCursor(canvas, cursorOffsetVertical); // Rely on the drawable entirely, do not draw the cursor line. // Has to be done after the IMM related code above which relies on the highlight. @@ -1675,6 +1743,10 @@ public class Editor { } else { layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical); } + + if (mSelectionActionModeHelper != null) { + mSelectionActionModeHelper.onDraw(canvas); + } } private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight, @@ -1853,8 +1925,8 @@ public class Editor { private void drawCursor(Canvas canvas, int cursorOffsetVertical) { final boolean translate = cursorOffsetVertical != 0; if (translate) canvas.translate(0, cursorOffsetVertical); - for (int i = 0; i < mCursorCount; i++) { - mCursorDrawable[i].draw(canvas); + if (mDrawableForCursor != null) { + mDrawableForCursor.draw(canvas); } if (translate) canvas.translate(0, -cursorOffsetVertical); } @@ -1911,32 +1983,20 @@ public class Editor { } } - void updateCursorsPositions() { + void updateCursorPosition() { if (mTextView.mCursorDrawableRes == 0) { - mCursorCount = 0; + mDrawableForCursor = null; return; } - Layout layout = mTextView.getLayout(); + final Layout layout = mTextView.getLayout(); final int offset = mTextView.getSelectionStart(); final int line = layout.getLineForOffset(offset); final int top = layout.getLineTop(line); - final int bottom = layout.getLineTop(line + 1); - - mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1; - - int middle = bottom; - if (mCursorCount == 2) { - // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)} - middle = (top + bottom) >> 1; - } - - boolean clamped = layout.shouldClampCursor(line); - updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped)); + final int bottom = layout.getLineBottomWithoutSpacing(line); - if (mCursorCount == 2) { - updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped)); - } + final boolean clamped = layout.shouldClampCursor(line); + updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped)); } void refreshTextActionMode() { @@ -2304,19 +2364,19 @@ public class Editor { } @VisibleForTesting - public Drawable[] getCursorDrawable() { - return mCursorDrawable; + @Nullable + public Drawable getCursorDrawable() { + return mDrawableForCursor; } - private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) { - if (mCursorDrawable[cursorIndex] == null) { - mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable( + private void updateCursorPosition(int top, int bottom, float horizontal) { + if (mDrawableForCursor == null) { + mDrawableForCursor = mTextView.getContext().getDrawable( mTextView.mCursorDrawableRes); } - final Drawable drawable = mCursorDrawable[cursorIndex]; - final int left = clampHorizontalPosition(drawable, horizontal); - final int width = drawable.getIntrinsicWidth(); - drawable.setBounds(left, top - mTempRect.top, left + width, + final int left = clampHorizontalPosition(mDrawableForCursor, horizontal); + final int width = mDrawableForCursor.getIntrinsicWidth(); + mDrawableForCursor.setBounds(left, top - mTempRect.top, left + width, bottom + mTempRect.bottom); } @@ -2989,7 +3049,8 @@ public class Editor { @Override protected int getVerticalLocalPosition(int line) { - return mTextView.getLayout().getLineBottom(line); + final Layout layout = mTextView.getLayout(); + return layout.getLineBottomWithoutSpacing(line); } @Override @@ -3646,7 +3707,8 @@ public class Editor { @Override protected int getVerticalLocalPosition(int line) { - return mTextView.getLayout().getLineBottom(line) - mContainerMarginTop; + final Layout layout = mTextView.getLayout(); + return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop; } @Override @@ -3764,6 +3826,7 @@ public class Editor { private final RectF mSelectionBounds = new RectF(); private final boolean mHasSelection; private final int mHandleHeight; + private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>(); public TextActionModeCallback(boolean hasSelection) { mHasSelection = hasSelection; @@ -3791,6 +3854,8 @@ public class Editor { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mAssistClickHandlers.clear(); + mode.setTitle(null); mode.setSubtitle(null); mode.setTitleOptionalHint(true); @@ -3810,14 +3875,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() { @@ -3859,7 +3920,7 @@ public class Editor { if (selected == null || selected.isEmpty()) { menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL, com.android.internal.R.string.autofill) - .setShowAsAction(MenuItem.SHOW_AS_OVERFLOW_ALWAYS); + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } } @@ -3874,14 +3935,14 @@ public class Editor { updateSelectAllItem(menu); updateReplaceItem(menu); - updateAssistMenuItem(menu); + updateAssistMenuItems(menu); } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { updateSelectAllItem(menu); updateReplaceItem(menu); - updateAssistMenuItem(menu); + updateAssistMenuItems(menu); Callback customCallback = getCustomCallback(); if (customCallback != null) { @@ -3914,32 +3975,112 @@ public class Editor { } } - private void updateAssistMenuItem(Menu menu) { - menu.removeItem(TextView.ID_ASSIST); + private void updateAssistMenuItems(Menu menu) { + clearAssistMenuItems(menu); + if (!mTextView.isDeviceProvisioned()) { + return; + } final TextClassification textClassification = getSelectionActionModeHelper().getTextClassification(); - if (canAssist()) { - menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST, + if (textClassification == null) { + return; + } + if (isValidAssistMenuItem( + textClassification.getIcon(), + textClassification.getLabel(), + textClassification.getOnClickListener(), + textClassification.getIntent())) { + final MenuItem item = menu.add( + TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST, textClassification.getLabel()) .setIcon(textClassification.getIcon()) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - mMetricsLogger.write( - new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST) - .setType(MetricsEvent.TYPE_OPEN) - .setSubtype(textClassification.getLogType())); + .setIntent(textClassification.getIntent()); + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + mAssistClickHandlers.put(item, textClassification.getOnClickListener()); + } + final int count = textClassification.getSecondaryActionsCount(); + for (int i = 0; i < count; i++) { + if (!isValidAssistMenuItem( + textClassification.getSecondaryIcon(i), + textClassification.getSecondaryLabel(i), + textClassification.getSecondaryOnClickListener(i), + textClassification.getSecondaryIntent(i))) { + continue; + } + final int order = MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i; + final MenuItem item = menu.add( + TextView.ID_ASSIST, Menu.NONE, order, + textClassification.getSecondaryLabel(i)) + .setIcon(textClassification.getSecondaryIcon(i)) + .setIntent(textClassification.getSecondaryIntent(i)); + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + mAssistClickHandlers.put(item, textClassification.getSecondaryOnClickListener(i)); } } - private boolean canAssist() { + private void clearAssistMenuItems(Menu menu) { + int i = 0; + while (i < menu.size()) { + final MenuItem menuItem = menu.getItem(i); + if (menuItem.getGroupId() == TextView.ID_ASSIST) { + menu.removeItem(menuItem.getItemId()); + continue; + } + i++; + } + } + + private boolean isValidAssistMenuItem( + Drawable icon, CharSequence label, OnClickListener onClick, Intent intent) { + final boolean hasUi = icon != null || !TextUtils.isEmpty(label); + final boolean hasAction = onClick != null || isSupportedIntent(intent); + return hasUi && hasAction; + } + + private boolean isSupportedIntent(Intent intent) { + if (intent == null) { + return false; + } + final Context context = mTextView.getContext(); + final ResolveInfo info = context.getPackageManager().resolveActivity(intent, 0); + final boolean samePackage = context.getPackageName().equals( + info.activityInfo.packageName); + if (samePackage) { + return true; + } + + final boolean exported = info.activityInfo.exported; + final boolean requiresPermission = info.activityInfo.permission != null; + final boolean hasPermission = !requiresPermission + || context.checkSelfPermission(info.activityInfo.permission) + == PackageManager.PERMISSION_GRANTED; + return exported && hasPermission; + } + + private boolean onAssistMenuItemClicked(MenuItem assistMenuItem) { + Preconditions.checkArgument(assistMenuItem.getGroupId() == TextView.ID_ASSIST); + final TextClassification textClassification = getSelectionActionModeHelper().getTextClassification(); - return mTextView.isDeviceProvisioned() - && textClassification != null - && (textClassification.getIcon() != null - || !TextUtils.isEmpty(textClassification.getLabel())) - && (textClassification.getOnClickListener() != null - || (textClassification.getIntent() != null - && mTextView.getContext().canStartActivityForResult())); + if (!mTextView.isDeviceProvisioned() || textClassification == null) { + // No textClassification result to handle the click. Eat the click. + return true; + } + + OnClickListener onClickListener = mAssistClickHandlers.get(assistMenuItem); + if (onClickListener == null) { + final Intent intent = assistMenuItem.getIntent(); + if (intent != null) { + onClickListener = TextClassification.createStartActivityOnClickListener( + mTextView.getContext(), intent); + } + } + if (onClickListener != null) { + onClickListener.onClick(mTextView); + stopTextActionMode(); + } + // We tried our best. + return true; } @Override @@ -3953,25 +4094,7 @@ public class Editor { if (customCallback != null && customCallback.onActionItemClicked(mode, item)) { return true; } - final TextClassification textClassification = - getSelectionActionModeHelper().getTextClassification(); - if (TextView.ID_ASSIST == item.getItemId() && textClassification != null) { - final OnClickListener onClickListener = - textClassification.getOnClickListener(); - if (onClickListener != null) { - onClickListener.onClick(mTextView); - } else { - final Intent intent = textClassification.getIntent(); - if (intent != null) { - TextClassification.createStartActivityOnClickListener( - mTextView.getContext(), intent) - .onClick(mTextView); - } - } - mMetricsLogger.action( - MetricsEvent.ACTION_TEXT_SELECTION_MENU_ITEM_ASSIST, - textClassification.getLogType()); - stopTextActionMode(); + if (item.getGroupId() == TextView.ID_ASSIST && onAssistMenuItemClicked(item)) { return true; } return mTextView.onTextContextMenuItem(item.getItemId()); @@ -4000,6 +4123,8 @@ public class Editor { if (mSelectionModifierCursorController != null) { mSelectionModifierCursorController.hide(); } + + mAssistClickHandlers.clear(); } @Override @@ -4015,19 +4140,8 @@ public class Editor { mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath); mSelectionPath.computeBounds(mSelectionBounds, true); mSelectionBounds.bottom += mHandleHeight; - } else if (mCursorCount == 2) { - // We have a split cursor. In this case, we take the rectangle that includes both - // parts of the cursor to ensure we don't obscure either of them. - Rect firstCursorBounds = mCursorDrawable[0].getBounds(); - Rect secondCursorBounds = mCursorDrawable[1].getBounds(); - mSelectionBounds.set( - Math.min(firstCursorBounds.left, secondCursorBounds.left), - Math.min(firstCursorBounds.top, secondCursorBounds.top), - Math.max(firstCursorBounds.right, secondCursorBounds.right), - Math.max(firstCursorBounds.bottom, secondCursorBounds.bottom) - + mHandleHeight); } else { - // We have a single cursor. + // We have a cursor. Layout layout = mTextView.getLayout(); int line = layout.getLineForOffset(mTextView.getSelectionStart()); float primaryHorizontal = clampHorizontalPosition(null, @@ -4036,7 +4150,7 @@ public class Editor { primaryHorizontal, layout.getLineTop(line), primaryHorizontal, - layout.getLineTop(line + 1) + mHandleHeight); + layout.getLineBottom(line) - layout.getLineBottom(line) + mHandleHeight); } // Take TextView's padding and scroll into account. int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset(); @@ -4131,7 +4245,7 @@ public class Editor { + viewportToContentVerticalOffset; final float insertionMarkerBaseline = layout.getLineBaseline(line) + viewportToContentVerticalOffset; - final float insertionMarkerBottom = layout.getLineBottom(line) + final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line) + viewportToContentVerticalOffset; final boolean isTopVisible = mTextView .isPositionVisible(insertionMarkerX, insertionMarkerTop); @@ -4354,6 +4468,9 @@ public class Editor { protected abstract void updatePosition(float x, float y, boolean fromTouchScreen); + @MagnifierHandleTrigger + protected abstract int getMagnifierHandleTrigger(); + protected boolean isAtRtlRun(@NonNull Layout layout, int offset) { return layout.isRtlCharAt(offset); } @@ -4399,7 +4516,7 @@ public class Editor { mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX - getHorizontalOffset() + getCursorOffset(); - mPositionY = layout.getLineBottom(line); + mPositionY = layout.getLineBottomWithoutSpacing(line); // Take TextView's padding and scroll into account. mPositionX += mTextView.viewportToContentHorizontalOffset(); @@ -4411,7 +4528,7 @@ public class Editor { } /** - * Return the clamped horizontal position for the first cursor. + * Return the clamped horizontal position for the cursor. * * @param layout Text layout. * @param offset Character offset for the cursor. @@ -4491,6 +4608,51 @@ public class Editor { return 0; } + protected final void showMagnifier() { + if (mMagnifier == null) { + return; + } + + final int trigger = getMagnifierHandleTrigger(); + final int offset; + switch (trigger) { + case MagnifierHandleTrigger.INSERTION: // Fall through. + case MagnifierHandleTrigger.SELECTION_START: + offset = mTextView.getSelectionStart(); + break; + case MagnifierHandleTrigger.SELECTION_END: + offset = mTextView.getSelectionEnd(); + break; + default: + offset = -1; + break; + } + + if (offset == -1) { + dismissMagnifier(); + } + + final Layout layout = mTextView.getLayout(); + final int lineNumber = layout.getLineForOffset(offset); + // Horizontally snap to character offset. + final float xPosInView = getHorizontal(mTextView.getLayout(), offset) + + mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); + // Vertically snap to middle of current line. + final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber) + + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f + + mTextView.getTotalPaddingTop() - mTextView.getScrollY(); + + suspendBlink(); + mMagnifier.show(xPosInView, yPosInView); + } + + protected final void dismissMagnifier() { + if (mMagnifier != null) { + mMagnifier.dismiss(); + resumeBlink(); + } + } + @Override public boolean onTouchEvent(MotionEvent ev) { updateFloatingToolbarVisibility(ev); @@ -4543,10 +4705,7 @@ public class Editor { case MotionEvent.ACTION_UP: filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)); - mIsDragging = false; - updateDrawable(); - break; - + // Fall through. case MotionEvent.ACTION_CANCEL: mIsDragging = false; updateDrawable(); @@ -4647,20 +4806,19 @@ public class Editor { @Override protected int getCursorOffset() { int offset = super.getCursorOffset(); - final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null; - if (cursor != null) { - cursor.getPadding(mTempRect); - offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2; + if (mDrawableForCursor != null) { + mDrawableForCursor.getPadding(mTempRect); + offset += (mDrawableForCursor.getIntrinsicWidth() + - mTempRect.left - mTempRect.right) / 2; } return offset; } @Override int getCursorHorizontalPosition(Layout layout, int offset) { - final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null; - if (drawable != null) { + if (mDrawableForCursor != null) { final float horizontal = getHorizontal(layout, offset); - return clampHorizontalPosition(drawable, horizontal) + mTempRect.left; + return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left; } return super.getCursorHorizontalPosition(layout, offset); } @@ -4673,6 +4831,11 @@ public class Editor { case MotionEvent.ACTION_DOWN: mDownPositionX = ev.getRawX(); mDownPositionY = ev.getRawY(); + showMagnifier(); + break; + + case MotionEvent.ACTION_MOVE: + showMagnifier(); break; case MotionEvent.ACTION_UP: @@ -4698,11 +4861,10 @@ public class Editor { mTextActionMode.invalidateContentRect(); } } - hideAfterDelay(); - break; - + // Fall through. case MotionEvent.ACTION_CANCEL: hideAfterDelay(); + dismissMagnifier(); break; default: @@ -4753,6 +4915,12 @@ public class Editor { super.onDetached(); removeHiderCallback(); } + + @Override + @MagnifierHandleTrigger + protected int getMagnifierHandleTrigger() { + return MagnifierHandleTrigger.INSERTION; + } } @Retention(RetentionPolicy.SOURCE) @@ -4761,7 +4929,9 @@ public class Editor { public static final int HANDLE_TYPE_SELECTION_START = 0; public static final int HANDLE_TYPE_SELECTION_END = 1; - private class SelectionHandleView extends HandleView { + /** For selection handles */ + @VisibleForTesting + public final class SelectionHandleView extends HandleView { // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection // end (HANDLE_TYPE_SELECTION_END). @HandleType @@ -5009,12 +5179,26 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent event) { boolean superResult = super.onTouchEvent(event); - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - // Reset the touch word offset and x value when the user - // re-engages the handle. - mTouchWordDelta = 0.0f; - mPrevX = UNSET_X_VALUE; + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + // Reset the touch word offset and x value when the user + // re-engages the handle. + mTouchWordDelta = 0.0f; + mPrevX = UNSET_X_VALUE; + showMagnifier(); + break; + + case MotionEvent.ACTION_MOVE: + showMagnifier(); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + dismissMagnifier(); + break; } + return superResult; } @@ -5110,6 +5294,13 @@ public class Editor { return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset; } } + + @MagnifierHandleTrigger + protected int getMagnifierHandleTrigger() { + return isStartHandle() + ? MagnifierHandleTrigger.SELECTION_START + : MagnifierHandleTrigger.SELECTION_END; + } } private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) { @@ -6389,15 +6580,15 @@ 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); + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } } @@ -6453,7 +6644,9 @@ public class Editor { private boolean fireIntent(Intent intent) { if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) { - intent.putExtra(Intent.EXTRA_PROCESS_TEXT, mTextView.getSelectedText()); + String selectedText = mTextView.getSelectedText(); + selectedText = TextUtils.trimToParcelableSize(selectedText); + intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText); mEditor.mPreserveSelection = true; mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE); return true; @@ -6463,6 +6656,9 @@ public class Editor { private void loadSupportedActivities() { mSupportedActivities.clear(); + if (!mContext.canStartActivityForResult()) { + return; + } PackageManager packageManager = mTextView.getContext().getPackageManager(); List<ResolveInfo> unfiltered = packageManager.queryIntentActivities(createProcessTextIntent(), 0); diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 1c15c7ae7987..cbd1e0ad0998 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -898,9 +898,6 @@ public class GridLayout extends ViewGroup { } } - /** - * @hide - */ @Override protected void onDebugDrawMargins(Canvas canvas, Paint paint) { // Apply defaults, so as to remove UNDEFINED values @@ -916,9 +913,6 @@ public class GridLayout extends ViewGroup { } } - /** - * @hide - */ @Override protected void onDebugDraw(Canvas canvas) { Paint paint = new Paint(); diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index 0d676153b288..adf366a49c8c 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -657,6 +657,7 @@ public class ListPopupWindow implements ShowableListMenu { mPopup.update(getAnchorView(), mDropDownHorizontalOffset, mDropDownVerticalOffset, (widthSpec < 0)? -1 : widthSpec, (heightSpec < 0)? -1 : heightSpec); + mPopup.getContentView().restoreDefaultFocus(); } else { final int widthSpec; if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { @@ -695,6 +696,7 @@ public class ListPopupWindow implements ShowableListMenu { mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset, mDropDownVerticalOffset, mDropDownGravity); mDropDownList.setSelection(ListView.INVALID_POSITION); + mPopup.getContentView().restoreDefaultFocus(); if (!mModal || mDropDownList.isInTouchMode()) { clearListSelection(); diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 5845719ee537..fc9e8e70c20a 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -624,9 +624,9 @@ public class ListView extends AbsListView { for (int i = 0; i < count; i++) { final View child = infos.get(i).view; - final LayoutParams p = (LayoutParams) child.getLayoutParams(); - if (p != null) { - p.recycledHeaderFooter = false; + final ViewGroup.LayoutParams params = child.getLayoutParams(); + if (checkLayoutParams(params)) { + ((LayoutParams) params).recycledHeaderFooter = false; } } } diff --git a/core/java/android/widget/Magnifier.java b/core/java/android/widget/Magnifier.java new file mode 100644 index 000000000000..bd48f4554c5d --- /dev/null +++ b/core/java/android/widget/Magnifier.java @@ -0,0 +1,221 @@ +/* + * 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.widget; + +import android.annotation.FloatRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Handler; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.PixelCopy; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.View; + +import com.android.internal.util.Preconditions; + +/** + * Android magnifier widget. Can be used by any view which is attached to a window. + */ +@UiThread +public final class Magnifier { + // Use this to specify that a previous configuration value does not exist. + private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1; + // The view to which this magnifier is attached. + private final View mView; + // The window containing the magnifier. + private final PopupWindow mWindow; + // The center coordinates of the window containing the magnifier. + private final Point mWindowCoords = new Point(); + // The width of the window containing the magnifier. + private final int mWindowWidth; + // The height of the window containing the magnifier. + private final int mWindowHeight; + // The bitmap used to display the contents of the magnifier. + private final Bitmap mBitmap; + // The center coordinates of the content that is to be magnified. + private final Point mCenterZoomCoords = new Point(); + // The callback of the pixel copy request will be invoked on this Handler when + // the copy is finished. + private final Handler mPixelCopyHandler = Handler.getMain(); + // Current magnification scale. + private final float mZoomScale; + // Variables holding previous states, used for detecting redundant calls and invalidation. + private final Point mPrevStartCoordsInSurface = new Point( + NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); + private final PointF mPrevPosInView = new PointF( + NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); + private final Rect mPixelCopyRequestRect = new Rect(); + + /** + * Initializes a magnifier. + * + * @param view the view for which this magnifier is attached + */ + public Magnifier(@NonNull View view) { + mView = Preconditions.checkNotNull(view); + final Context context = mView.getContext(); + final float elevation = context.getResources().getDimension( + com.android.internal.R.dimen.magnifier_elevation); + final View content = LayoutInflater.from(context).inflate( + com.android.internal.R.layout.magnifier, null); + content.findViewById(com.android.internal.R.id.magnifier_inner).setClipToOutline(true); + mWindowWidth = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.magnifier_width); + mWindowHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.magnifier_height); + mZoomScale = context.getResources().getFloat( + com.android.internal.R.dimen.magnifier_zoom_scale); + + mWindow = new PopupWindow(context); + mWindow.setContentView(content); + mWindow.setWidth(mWindowWidth); + mWindow.setHeight(mWindowHeight); + mWindow.setElevation(elevation); + mWindow.setTouchable(false); + mWindow.setBackgroundDrawable(null); + + final int bitmapWidth = Math.round(mWindowWidth / mZoomScale); + final int bitmapHeight = Math.round(mWindowHeight / mZoomScale); + mBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + getImageView().setImageBitmap(mBitmap); + } + + /** + * Shows the magnifier on the screen. + * + * @param xPosInView horizontal coordinate of the center point of the magnifier source relative + * to the view. The lower end is clamped to 0 and the higher end is clamped to the view + * width. + * @param yPosInView vertical coordinate of the center point of the magnifier source + * relative to the view. The lower end is clamped to 0 and the higher end is clamped to + * the view height. + */ + public void show(@FloatRange(from = 0) float xPosInView, + @FloatRange(from = 0) float yPosInView) { + xPosInView = Math.max(0, Math.min(xPosInView, mView.getWidth())); + yPosInView = Math.max(0, Math.min(yPosInView, mView.getHeight())); + + configureCoordinates(xPosInView, yPosInView); + + // Clamp startX value to avoid distorting the rendering of the magnifier content. + final int startX = Math.max(0, Math.min( + mCenterZoomCoords.x - mBitmap.getWidth() / 2, + mView.getWidth() - mBitmap.getWidth())); + final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2; + + if (startX != mPrevStartCoordsInSurface.x || startY != mPrevStartCoordsInSurface.y) { + performPixelCopy(startX, startY); + + mPrevPosInView.x = xPosInView; + mPrevPosInView.y = yPosInView; + + if (mWindow.isShowing()) { + mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(), + mWindow.getHeight()); + } else { + mWindow.showAtLocation(mView, Gravity.NO_GRAVITY, mWindowCoords.x, mWindowCoords.y); + } + } + } + + /** + * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op. + */ + public void dismiss() { + mWindow.dismiss(); + } + + /** + * Forces the magnifier to update its content. It uses the previous coordinates passed to + * {@link #show(float, float)}. This only happens if the magnifier is currently showing. + * + * @hide + */ + public void update() { + if (mWindow.isShowing()) { + // Update the contents shown in the magnifier. + performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y); + } + } + + private void configureCoordinates(float xPosInView, float yPosInView) { + final float posX; + final float posY; + + if (mView instanceof SurfaceView) { + // No offset required if the backing Surface matches the size of the SurfaceView. + posX = xPosInView; + posY = yPosInView; + } else { + final int[] coordinatesInSurface = new int[2]; + mView.getLocationInSurface(coordinatesInSurface); + posX = xPosInView + coordinatesInSurface[0]; + posY = yPosInView + coordinatesInSurface[1]; + } + + mCenterZoomCoords.x = Math.round(posX); + mCenterZoomCoords.y = Math.round(posY); + + final int verticalMagnifierOffset = mView.getContext().getResources().getDimensionPixelSize( + com.android.internal.R.dimen.magnifier_offset); + mWindowCoords.x = mCenterZoomCoords.x - mWindowWidth / 2; + mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalMagnifierOffset; + } + + private void performPixelCopy(final int startXInSurface, final int startYInSurface) { + final Surface surface = getValidViewSurface(); + if (surface != null) { + mPixelCopyRequestRect.set(startXInSurface, startYInSurface, + startXInSurface + mBitmap.getWidth(), startYInSurface + mBitmap.getHeight()); + + PixelCopy.request(surface, mPixelCopyRequestRect, mBitmap, + result -> { + getImageView().invalidate(); + mPrevStartCoordsInSurface.x = startXInSurface; + mPrevStartCoordsInSurface.y = startYInSurface; + }, + mPixelCopyHandler); + } + } + + @Nullable + private Surface getValidViewSurface() { + final Surface surface; + if (mView instanceof SurfaceView) { + surface = ((SurfaceView) mView).getHolder().getSurface(); + } else if (mView.getViewRootImpl() != null) { + surface = mView.getViewRootImpl().mSurface; + } else { + surface = null; + } + + return (surface != null && surface.isValid()) ? surface : null; + } + + private ImageView getImageView() { + return mWindow.getContentView().findViewById( + com.android.internal.R.id.magnifier_image); + } +} diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 23ebadb3806a..e91db1390582 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/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); @@ -2461,14 +2462,14 @@ public class PopupWindow { for (int i = 0; i < count; i++) { final View child = getChildAt(i); enterTransition.addTarget(child); - child.setVisibility(View.INVISIBLE); + child.setTransitionVisibility(View.INVISIBLE); } TransitionManager.beginDelayedTransition(this, enterTransition); for (int i = 0; i < count; i++) { final View child = getChildAt(i); - child.setVisibility(View.VISIBLE); + child.setTransitionVisibility(View.VISIBLE); } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 5adbdbe8ab4f..a2c55b091860 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -16,9 +16,11 @@ package android.widget; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + import android.annotation.ColorInt; import android.annotation.DimenRes; -import android.app.ActivityManager.StackId; +import android.annotation.NonNull; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Application; @@ -73,9 +75,13 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.Map; import java.util.Stack; import java.util.concurrent.Executor; @@ -84,6 +90,31 @@ import java.util.concurrent.Executor; * another process. The hierarchy is inflated from a layout resource * file, and this class provides some basic operations for modifying * the content of the inflated hierarchy. + * + * <p>{@code RemoteViews} is limited to support for the following layouts:</p> + * <ul> + * <li>{@link android.widget.AdapterViewFlipper}</li> + * <li>{@link android.widget.FrameLayout}</li> + * <li>{@link android.widget.GridLayout}</li> + * <li>{@link android.widget.GridView}</li> + * <li>{@link android.widget.LinearLayout}</li> + * <li>{@link android.widget.ListView}</li> + * <li>{@link android.widget.RelativeLayout}</li> + * <li>{@link android.widget.StackView}</li> + * <li>{@link android.widget.ViewFlipper}</li> + * </ul> + * <p>And the following widgets:</p> + * <ul> + * <li>{@link android.widget.AnalogClock}</li> + * <li>{@link android.widget.Button}</li> + * <li>{@link android.widget.Chronometer}</li> + * <li>{@link android.widget.ImageButton}</li> + * <li>{@link android.widget.ImageView}</li> + * <li>{@link android.widget.ProgressBar}</li> + * <li>{@link android.widget.TextClock}</li> + * <li>{@link android.widget.TextView}</li> + * </ul> + * <p>Descendants of these classes are not supported.</p> */ public class RemoteViews implements Parcelable, Filter { @@ -104,9 +135,9 @@ public class RemoteViews implements Parcelable, Filter { // The unique identifiers for each custom {@link Action}. private static final int SET_ON_CLICK_PENDING_INTENT_TAG = 1; private static final int REFLECTION_ACTION_TAG = 2; - private static final int SET_DRAWABLE_PARAMETERS_TAG = 3; + private static final int SET_DRAWABLE_TINT_TAG = 3; private static final int VIEW_GROUP_ACTION_ADD_TAG = 4; - private static final int SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG = 5; + private static final int VIEW_CONTENT_NAVIGATION_TAG = 5; private static final int SET_EMPTY_VIEW_ACTION_TAG = 6; private static final int VIEW_GROUP_ACTION_REMOVE_TAG = 7; private static final int SET_PENDING_INTENT_TEMPLATE_TAG = 8; @@ -117,7 +148,6 @@ public class RemoteViews implements Parcelable, Filter { private static final int TEXT_VIEW_SIZE_ACTION_TAG = 13; private static final int VIEW_PADDING_ACTION_TAG = 14; private static final int SET_REMOTE_VIEW_ADAPTER_LIST_TAG = 15; - private static final int TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG = 17; private static final int SET_REMOTE_INPUTS_ACTION_TAG = 18; private static final int LAYOUT_PARAM_ACTION_TAG = 19; private static final int OVERRIDE_TEXT_COLORS_TAG = 20; @@ -127,7 +157,7 @@ public class RemoteViews implements Parcelable, Filter { * * @hide */ - private ApplicationInfo mApplication; + public ApplicationInfo mApplication; /** * The resource ID of the layout file. (Added to the parcel) @@ -141,11 +171,6 @@ public class RemoteViews implements Parcelable, Filter { private ArrayList<Action> mActions; /** - * A class to keep track of memory usage by this RemoteViews - */ - private MemoryUsageCounter mMemoryUsageCounter; - - /** * Maps bitmaps to unique indicies to avoid Bitmap duplication. */ private BitmapCache mBitmapCache; @@ -186,19 +211,17 @@ public class RemoteViews implements Parcelable, Filter { */ private boolean mIsWidgetCollectionChild = false; + /** Class cookies of the Parcel this instance was read from. */ + private final Map<Class, Object> mClassCookies; + private static final OnClickHandler DEFAULT_ON_CLICK_HANDLER = new OnClickHandler(); - private static final Object[] sMethodsLock = new Object[0]; - private static final ArrayMap<Class<? extends View>, ArrayMap<MutablePair<String, Class<?>>, Method>> sMethods = - new ArrayMap<Class<? extends View>, ArrayMap<MutablePair<String, Class<?>>, Method>>(); - private static final ArrayMap<Method, Method> sAsyncMethods = new ArrayMap<>(); + private static final ArrayMap<MethodKey, MethodArgs> sMethods = new ArrayMap<>(); - private static final ThreadLocal<Object[]> sInvokeArgsTls = new ThreadLocal<Object[]>() { - @Override - protected Object[] initialValue() { - return new Object[1]; - } - }; + /** + * This key is used to perform lookups in sMethods without causing allocations. + */ + private static final MethodKey sLookupKey = new MethodKey(); /** * @hide @@ -255,37 +278,46 @@ public class RemoteViews implements Parcelable, Filter { } /** - * Handle with care! + * Stores information related to reflection method lookup. */ - static class MutablePair<F, S> { - F first; - S second; - - MutablePair(F first, S second) { - this.first = first; - this.second = second; - } + static class MethodKey { + public Class targetClass; + public Class paramClass; + public String methodName; @Override public boolean equals(Object o) { - if (!(o instanceof MutablePair)) { + if (!(o instanceof MethodKey)) { return false; } - MutablePair<?, ?> p = (MutablePair<?, ?>) o; - return Objects.equal(p.first, first) && Objects.equal(p.second, second); + MethodKey p = (MethodKey) o; + return Objects.equal(p.targetClass, targetClass) + && Objects.equal(p.paramClass, paramClass) + && Objects.equal(p.methodName, methodName); } @Override public int hashCode() { - return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()); + return Objects.hashCode(targetClass) ^ Objects.hashCode(paramClass) + ^ Objects.hashCode(methodName); + } + + public void set(Class targetClass, Class paramClass, String methodName) { + this.targetClass = targetClass; + this.paramClass = paramClass; + this.methodName = methodName; } } + /** - * This pair is used to perform lookups in sMethods without causing allocations. + * Stores information related to reflection method lookup result. */ - private final MutablePair<String, Class<?>> mPair = - new MutablePair<String, Class<?>>(null, null); + static class MethodArgs { + public MethodHandle syncMethod; + public MethodHandle asyncMethod; + public String asyncMethodName; + } /** * This annotation indicates that a subclass of View is allowed to be used @@ -307,6 +339,12 @@ public class RemoteViews implements Parcelable, Filter { public ActionException(String message) { super(message); } + /** + * @hide + */ + public ActionException(Throwable t) { + super(t); + } } /** @hide */ @@ -316,11 +354,11 @@ public class RemoteViews implements Parcelable, Filter { public boolean onClickHandler(View view, PendingIntent pendingIntent, Intent fillInIntent) { - return onClickHandler(view, pendingIntent, fillInIntent, StackId.INVALID_STACK_ID); + return onClickHandler(view, pendingIntent, fillInIntent, WINDOWING_MODE_UNDEFINED); } public boolean onClickHandler(View view, PendingIntent pendingIntent, - Intent fillInIntent, int launchStackId) { + Intent fillInIntent, int windowingMode) { try { // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT? Context context = view.getContext(); @@ -331,8 +369,8 @@ public class RemoteViews implements Parcelable, Filter { opts = ActivityOptions.makeBasic(); } - if (launchStackId != StackId.INVALID_STACK_ID) { - opts.setLaunchStackId(launchStackId); + if (windowingMode != WINDOWING_MODE_UNDEFINED) { + opts.setLaunchWindowingMode(windowingMode); } context.startIntentSender( pendingIntent.getIntentSender(), fillInIntent, @@ -372,14 +410,6 @@ public class RemoteViews implements Parcelable, Filter { return 0; } - /** - * Overridden by each class to report on it's own memory usage - */ - public void updateMemoryUsageEstimate(MemoryUsageCounter counter) { - // We currently only calculate Bitmap memory usage, so by default, - // don't do anything here - } - public void setBitmapCache(BitmapCache bitmapCache) { // Do nothing } @@ -388,10 +418,10 @@ public class RemoteViews implements Parcelable, Filter { return MERGE_REPLACE; } - public abstract String getActionName(); + public abstract int getActionTag(); public String getUniqueKey() { - return (getActionName() + viewId); + return (getActionTag() + "_" + viewId); } /** @@ -423,8 +453,8 @@ public class RemoteViews implements Parcelable, Filter { */ private static abstract class RuntimeAction extends Action { @Override - public final String getActionName() { - return "RuntimeAction"; + public final int getActionTag() { + return 0; } @Override @@ -452,7 +482,7 @@ public class RemoteViews implements Parcelable, Filter { // We first copy the new RemoteViews, as the process of merging modifies the way the actions // reference the bitmap cache. We don't want to modify the object as it may need to // be merged and applied multiple times. - RemoteViews copy = newRv.clone(); + RemoteViews copy = new RemoteViews(newRv); HashMap<String, Action> map = new HashMap<String, Action>(); if (mActions == null) { @@ -486,7 +516,6 @@ public class RemoteViews implements Parcelable, Filter { // Because pruning can remove the need for bitmaps, we reconstruct the bitmap cache mBitmapCache = new BitmapCache(); setBitmapCache(mBitmapCache); - recalculateMemoryUsage(); } private static class RemoteViewsContextWrapper extends ContextWrapper { @@ -514,7 +543,6 @@ public class RemoteViews implements Parcelable, Filter { } private class SetEmptyView extends Action { - int viewId; int emptyViewId; SetEmptyView(int viewId, int emptyViewId) { @@ -528,7 +556,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel out, int flags) { - out.writeInt(SET_EMPTY_VIEW_ACTION_TAG); out.writeInt(this.viewId); out.writeInt(this.emptyViewId); } @@ -546,8 +573,9 @@ public class RemoteViews implements Parcelable, Filter { adapterView.setEmptyView(emptyView); } - public String getActionName() { - return "SetEmptyView"; + @Override + public int getActionTag() { + return SET_EMPTY_VIEW_ACTION_TAG; } } @@ -559,13 +587,12 @@ public class RemoteViews implements Parcelable, Filter { public SetOnClickFillInIntent(Parcel parcel) { viewId = parcel.readInt(); - fillInIntent = Intent.CREATOR.createFromParcel(parcel); + fillInIntent = parcel.readTypedObject(Intent.CREATOR); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_ON_CLICK_FILL_IN_INTENT_TAG); dest.writeInt(viewId); - fillInIntent.writeToParcel(dest, 0 /* no flags */); + dest.writeTypedObject(fillInIntent, 0 /* no flags */); } @Override @@ -624,8 +651,9 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { - return "SetOnClickFillInIntent"; + @Override + public int getActionTag() { + return SET_ON_CLICK_FILL_IN_INTENT_TAG; } Intent fillInIntent; @@ -643,9 +671,8 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_PENDING_INTENT_TEMPLATE_TAG); dest.writeInt(viewId); - pendingIntentTemplate.writeToParcel(dest, 0 /* no flags */); + PendingIntent.writePendingIntentOrNullToParcel(pendingIntentTemplate, dest); } @Override @@ -699,8 +726,9 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { - return "SetPendingIntentTemplate"; + @Override + public int getActionTag() { + return SET_PENDING_INTENT_TEMPLATE_TAG; } PendingIntent pendingIntentTemplate; @@ -716,30 +744,13 @@ public class RemoteViews implements Parcelable, Filter { public SetRemoteViewsAdapterList(Parcel parcel) { viewId = parcel.readInt(); viewTypeCount = parcel.readInt(); - int count = parcel.readInt(); - list = new ArrayList<RemoteViews>(); - - for (int i = 0; i < count; i++) { - RemoteViews rv = RemoteViews.CREATOR.createFromParcel(parcel); - list.add(rv); - } + list = parcel.createTypedArrayList(RemoteViews.CREATOR); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_REMOTE_VIEW_ADAPTER_LIST_TAG); dest.writeInt(viewId); dest.writeInt(viewTypeCount); - - if (list == null || list.size() == 0) { - dest.writeInt(0); - } else { - int count = list.size(); - dest.writeInt(count); - for (int i = 0; i < count; i++) { - RemoteViews rv = list.get(i); - rv.writeToParcel(dest, flags); - } - } + dest.writeTypedList(list, flags); } @Override @@ -779,8 +790,9 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { - return "SetRemoteViewsAdapterList"; + @Override + public int getActionTag() { + return SET_REMOTE_VIEW_ADAPTER_LIST_TAG; } int viewTypeCount; @@ -795,13 +807,12 @@ public class RemoteViews implements Parcelable, Filter { public SetRemoteViewsAdapterIntent(Parcel parcel) { viewId = parcel.readInt(); - intent = Intent.CREATOR.createFromParcel(parcel); + intent = parcel.readTypedObject(Intent.CREATOR); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_REMOTE_VIEW_ADAPTER_INTENT_TAG); dest.writeInt(viewId); - intent.writeToParcel(dest, flags); + dest.writeTypedObject(intent, flags); } @Override @@ -845,8 +856,9 @@ public class RemoteViews implements Parcelable, Filter { return copy; } - public String getActionName() { - return "SetRemoteViewsAdapterIntent"; + @Override + public int getActionTag() { + return SET_REMOTE_VIEW_ADAPTER_INTENT_TAG; } Intent intent; @@ -866,22 +878,12 @@ public class RemoteViews implements Parcelable, Filter { public SetOnClickPendingIntent(Parcel parcel) { viewId = parcel.readInt(); - - // We check a flag to determine if the parcel contains a PendingIntent. - if (parcel.readInt() != 0) { - pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); - } + pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_ON_CLICK_PENDING_INTENT_TAG); dest.writeInt(viewId); - - // We use a flag to indicate whether the parcel contains a valid object. - dest.writeInt(pendingIntent != null ? 1 : 0); - if (pendingIntent != null) { - pendingIntent.writeToParcel(dest, 0 /* no flags */); - } + PendingIntent.writePendingIntentOrNullToParcel(pendingIntent, dest); } @Override @@ -922,8 +924,9 @@ public class RemoteViews implements Parcelable, Filter { target.setOnClickListener(listener); } - public String getActionName() { - return "SetOnClickPendingIntent"; + @Override + public int getActionTag() { + return SET_ON_CLICK_PENDING_INTENT_TAG; } PendingIntent pendingIntent; @@ -943,73 +946,66 @@ public class RemoteViews implements Parcelable, Filter { return rect; } - private Method getMethod(View view, String methodName, Class<?> paramType) { - Method method; + private MethodHandle getMethod(View view, String methodName, Class<?> paramType, + boolean async) { + MethodArgs result; Class<? extends View> klass = view.getClass(); - synchronized (sMethodsLock) { - ArrayMap<MutablePair<String, Class<?>>, Method> methods = sMethods.get(klass); - if (methods == null) { - methods = new ArrayMap<MutablePair<String, Class<?>>, Method>(); - sMethods.put(klass, methods); - } - - mPair.first = methodName; - mPair.second = paramType; + synchronized (sMethods) { + // The key is defined by the view class, param class and method name. + sLookupKey.set(klass, paramType, methodName); + result = sMethods.get(sLookupKey); - method = methods.get(mPair); - if (method == null) { + if (result == null) { + Method method; try { if (paramType == null) { method = klass.getMethod(methodName); } else { method = klass.getMethod(methodName, paramType); } - } catch (NoSuchMethodException ex) { - throw new ActionException("view: " + klass.getName() + " doesn't have method: " - + methodName + getParameters(paramType)); - } + if (!method.isAnnotationPresent(RemotableViewMethod.class)) { + throw new ActionException("view: " + klass.getName() + + " can't use method with RemoteViews: " + + methodName + getParameters(paramType)); + } - if (!method.isAnnotationPresent(RemotableViewMethod.class)) { - throw new ActionException("view: " + klass.getName() - + " can't use method with RemoteViews: " + result = new MethodArgs(); + result.syncMethod = MethodHandles.publicLookup().unreflect(method); + result.asyncMethodName = + method.getAnnotation(RemotableViewMethod.class).asyncImpl(); + } catch (NoSuchMethodException | IllegalAccessException ex) { + throw new ActionException("view: " + klass.getName() + " doesn't have method: " + methodName + getParameters(paramType)); } - methods.put(new MutablePair<String, Class<?>>(methodName, paramType), method); + MethodKey key = new MethodKey(); + key.set(klass, paramType, methodName); + sMethods.put(key, result); } - } - return method; - } - - /** - * @return the async implementation of the provided method. - */ - private Method getAsyncMethod(Method method) { - synchronized (sAsyncMethods) { - int valueIndex = sAsyncMethods.indexOfKey(method); - if (valueIndex >= 0) { - return sAsyncMethods.valueAt(valueIndex); + if (!async) { + return result.syncMethod; } - - RemotableViewMethod annotation = method.getAnnotation(RemotableViewMethod.class); - Method asyncMethod = null; - if (!annotation.asyncImpl().isEmpty()) { + // Check this so see if async method is implemented or not. + if (result.asyncMethodName.isEmpty()) { + return null; + } + // Async method is lazily loaded. If it is not yet loaded, load now. + if (result.asyncMethod == null) { + MethodType asyncType = result.syncMethod.type() + .dropParameterTypes(0, 1).changeReturnType(Runnable.class); try { - asyncMethod = method.getDeclaringClass() - .getMethod(annotation.asyncImpl(), method.getParameterTypes()); - if (!asyncMethod.getReturnType().equals(Runnable.class)) { - throw new ActionException("Async implementation for " + method.getName() + - " does not return a Runnable"); - } - } catch (NoSuchMethodException ex) { - throw new ActionException("Async implementation declared but not defined for " + - method.getName()); + result.asyncMethod = MethodHandles.publicLookup().findVirtual( + klass, result.asyncMethodName, asyncType); + } catch (NoSuchMethodException | IllegalAccessException ex) { + throw new ActionException("Async implementation declared as " + + result.asyncMethodName + " but not defined for " + methodName + + ": public Runnable " + result.asyncMethodName + " (" + + TextUtils.join(",", asyncType.parameterArray()) + ")"); } } - sAsyncMethods.put(method, asyncMethod); - return asyncMethod; + return result.asyncMethod; } } @@ -1018,62 +1014,38 @@ public class RemoteViews implements Parcelable, Filter { return "(" + paramType + ")"; } - private static Object[] wrapArg(Object value) { - Object[] args = sInvokeArgsTls.get(); - args[0] = value; - return args; - } - /** - * Equivalent to calling a combination of {@link Drawable#setAlpha(int)}, + * Equivalent to calling * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}, - * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given view. + * on the {@link Drawable} of a given view. * <p> - * These operations will be performed on the {@link Drawable} returned by the + * The operation will be performed on the {@link Drawable} returned by the * target {@link View#getBackground()} by default. If targetBackground is false, * we assume the target is an {@link ImageView} and try applying the operations * to {@link ImageView#getDrawable()}. * <p> - * You can omit specific calls by marking their values with null or -1. */ - private class SetDrawableParameters extends Action { - public SetDrawableParameters(int id, boolean targetBackground, int alpha, - int colorFilter, PorterDuff.Mode mode, int level) { + private class SetDrawableTint extends Action { + SetDrawableTint(int id, boolean targetBackground, + int colorFilter, @NonNull PorterDuff.Mode mode) { this.viewId = id; this.targetBackground = targetBackground; - this.alpha = alpha; this.colorFilter = colorFilter; this.filterMode = mode; - this.level = level; } - public SetDrawableParameters(Parcel parcel) { + SetDrawableTint(Parcel parcel) { viewId = parcel.readInt(); targetBackground = parcel.readInt() != 0; - alpha = parcel.readInt(); colorFilter = parcel.readInt(); - boolean hasMode = parcel.readInt() != 0; - if (hasMode) { - filterMode = PorterDuff.Mode.valueOf(parcel.readString()); - } else { - filterMode = null; - } - level = parcel.readInt(); + filterMode = PorterDuff.intToMode(parcel.readInt()); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_DRAWABLE_PARAMETERS_TAG); dest.writeInt(viewId); dest.writeInt(targetBackground ? 1 : 0); - dest.writeInt(alpha); dest.writeInt(colorFilter); - if (filterMode != null) { - dest.writeInt(1); - dest.writeString(filterMode.toString()); - } else { - dest.writeInt(0); - } - dest.writeInt(level); + dest.writeInt(PorterDuff.modeToInt(filterMode)); } @Override @@ -1091,47 +1063,36 @@ public class RemoteViews implements Parcelable, Filter { } if (targetDrawable != null) { - // Perform modifications only if values are set correctly - if (alpha != -1) { - targetDrawable.mutate().setAlpha(alpha); - } - if (filterMode != null) { - targetDrawable.mutate().setColorFilter(colorFilter, filterMode); - } - if (level != -1) { - targetDrawable.mutate().setLevel(level); - } + targetDrawable.mutate().setColorFilter(colorFilter, filterMode); } } - public String getActionName() { - return "SetDrawableParameters"; + @Override + public int getActionTag() { + return SET_DRAWABLE_TINT_TAG; } boolean targetBackground; - int alpha; int colorFilter; PorterDuff.Mode filterMode; - int level; } - private final class ReflectionActionWithoutParams extends Action { - final String methodName; + private final class ViewContentNavigation extends Action { + final boolean mNext; - ReflectionActionWithoutParams(int viewId, String methodName) { + ViewContentNavigation(int viewId, boolean next) { this.viewId = viewId; - this.methodName = methodName; + this.mNext = next; } - ReflectionActionWithoutParams(Parcel in) { + ViewContentNavigation(Parcel in) { this.viewId = in.readInt(); - this.methodName = in.readString(); + this.mNext = in.readBoolean(); } public void writeToParcel(Parcel out, int flags) { - out.writeInt(SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG); out.writeInt(this.viewId); - out.writeString(this.methodName); + out.writeBoolean(this.mNext); } @Override @@ -1140,42 +1101,34 @@ public class RemoteViews implements Parcelable, Filter { if (view == null) return; try { - getMethod(view, this.methodName, null).invoke(view); - } catch (ActionException e) { - throw e; - } catch (Exception ex) { + getMethod(view, + mNext ? "showNext" : "showPrevious", null, false /* async */).invoke(view); + } catch (Throwable ex) { throw new ActionException(ex); } } public int mergeBehavior() { - // we don't need to build up showNext or showPrevious calls - if (methodName.equals("showNext") || methodName.equals("showPrevious")) { - return MERGE_IGNORE; - } else { - return MERGE_REPLACE; - } + return MERGE_IGNORE; } - public String getActionName() { - return "ReflectionActionWithoutParams"; + @Override + public int getActionTag() { + return VIEW_CONTENT_NAVIGATION_TAG; } } private static class BitmapCache { + ArrayList<Bitmap> mBitmaps; + int mBitmapMemory = -1; public BitmapCache() { - mBitmaps = new ArrayList<Bitmap>(); + mBitmaps = new ArrayList<>(); } public BitmapCache(Parcel source) { - int count = source.readInt(); - mBitmaps = new ArrayList<Bitmap>(); - for (int i = 0; i < count; i++) { - Bitmap b = Bitmap.CREATOR.createFromParcel(source); - mBitmaps.add(b); - } + mBitmaps = source.createTypedArrayList(Bitmap.CREATOR); } public int getBitmapId(Bitmap b) { @@ -1186,6 +1139,7 @@ public class RemoteViews implements Parcelable, Filter { return mBitmaps.indexOf(b); } else { mBitmaps.add(b); + mBitmapMemory = -1; return (mBitmaps.size() - 1); } } @@ -1200,35 +1154,18 @@ public class RemoteViews implements Parcelable, Filter { } public void writeBitmapsToParcel(Parcel dest, int flags) { - int count = mBitmaps.size(); - dest.writeInt(count); - for (int i = 0; i < count; i++) { - mBitmaps.get(i).writeToParcel(dest, flags); - } + dest.writeTypedList(mBitmaps, flags); } - public void assimilate(BitmapCache bitmapCache) { - ArrayList<Bitmap> bitmapsToBeAdded = bitmapCache.mBitmaps; - int count = bitmapsToBeAdded.size(); - for (int i = 0; i < count; i++) { - Bitmap b = bitmapsToBeAdded.get(i); - if (!mBitmaps.contains(b)) { - mBitmaps.add(b); + public int getBitmapMemory() { + if (mBitmapMemory < 0) { + mBitmapMemory = 0; + int count = mBitmaps.size(); + for (int i = 0; i < count; i++) { + mBitmapMemory += mBitmaps.get(i).getAllocationByteCount(); } } - } - - public void addBitmapMemory(MemoryUsageCounter memoryCounter) { - for (int i = 0; i < mBitmaps.size(); i++) { - memoryCounter.addBitmapMemory(mBitmaps.get(i)); - } - } - - @Override - protected BitmapCache clone() { - BitmapCache bitmapCache = new BitmapCache(); - bitmapCache.mBitmaps.addAll(mBitmaps); - return bitmapCache; + return mBitmapMemory; } } @@ -1253,7 +1190,6 @@ public class RemoteViews implements Parcelable, Filter { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(BITMAP_REFLECTION_ACTION_TAG); dest.writeInt(viewId); dest.writeString(methodName); dest.writeInt(bitmapId); @@ -1272,8 +1208,9 @@ public class RemoteViews implements Parcelable, Filter { bitmapId = bitmapCache.getBitmapId(bitmap); } - public String getActionName() { - return "BitmapReflectionAction"; + @Override + public int getActionTag() { + return BITMAP_REFLECTION_ACTION_TAG; } } @@ -1325,7 +1262,7 @@ public class RemoteViews implements Parcelable, Filter { // written to the parcel. switch (this.type) { case BOOLEAN: - this.value = in.readInt() != 0; + this.value = in.readBoolean(); break; case BYTE: this.value = in.readByte(); @@ -1355,39 +1292,28 @@ public class RemoteViews implements Parcelable, Filter { this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); break; case URI: - if (in.readInt() != 0) { - this.value = Uri.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(Uri.CREATOR); break; case BITMAP: - if (in.readInt() != 0) { - this.value = Bitmap.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(Bitmap.CREATOR); break; case BUNDLE: this.value = in.readBundle(); break; case INTENT: - if (in.readInt() != 0) { - this.value = Intent.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(Intent.CREATOR); break; case COLOR_STATE_LIST: - if (in.readInt() != 0) { - this.value = ColorStateList.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(ColorStateList.CREATOR); break; case ICON: - if (in.readInt() != 0) { - this.value = Icon.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(Icon.CREATOR); default: break; } } public void writeToParcel(Parcel out, int flags) { - out.writeInt(REFLECTION_ACTION_TAG); out.writeInt(this.viewId); out.writeString(this.methodName); out.writeInt(this.type); @@ -1401,7 +1327,7 @@ public class RemoteViews implements Parcelable, Filter { // we have written a valid value to the parcel. switch (this.type) { case BOOLEAN: - out.writeInt((Boolean) this.value ? 1 : 0); + out.writeBoolean((Boolean) this.value); break; case BYTE: out.writeByte((Byte) this.value); @@ -1430,38 +1356,15 @@ public class RemoteViews implements Parcelable, Filter { case CHAR_SEQUENCE: TextUtils.writeToParcel((CharSequence)this.value, out, flags); break; - case URI: - out.writeInt(this.value != null ? 1 : 0); - if (this.value != null) { - ((Uri)this.value).writeToParcel(out, flags); - } - break; - case BITMAP: - out.writeInt(this.value != null ? 1 : 0); - if (this.value != null) { - ((Bitmap)this.value).writeToParcel(out, flags); - } - break; case BUNDLE: out.writeBundle((Bundle) this.value); break; + case URI: + case BITMAP: case INTENT: - out.writeInt(this.value != null ? 1 : 0); - if (this.value != null) { - ((Intent)this.value).writeToParcel(out, flags); - } - break; case COLOR_STATE_LIST: - out.writeInt(this.value != null ? 1 : 0); - if (this.value != null) { - ((ColorStateList)this.value).writeToParcel(out, flags); - } - break; case ICON: - out.writeInt(this.value != null ? 1 : 0); - if (this.value != null) { - ((Icon)this.value).writeToParcel(out, flags); - } + out.writeTypedObject((Parcelable) this.value, flags); break; default: break; @@ -1516,12 +1419,9 @@ public class RemoteViews implements Parcelable, Filter { if (param == null) { throw new ActionException("bad type: " + this.type); } - try { - getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value)); - } catch (ActionException e) { - throw e; - } catch (Exception ex) { + getMethod(view, this.methodName, param, false /* async */).invoke(view, this.value); + } catch (Throwable ex) { throw new ActionException(ex); } } @@ -1537,11 +1437,10 @@ public class RemoteViews implements Parcelable, Filter { } try { - Method method = getMethod(view, this.methodName, param); - Method asyncMethod = getAsyncMethod(method); + MethodHandle method = getMethod(view, this.methodName, param, true /* async */); - if (asyncMethod != null) { - Runnable endAction = (Runnable) asyncMethod.invoke(view, wrapArg(this.value)); + if (method != null) { + Runnable endAction = (Runnable) method.invoke(view, this.value); if (endAction == null) { return ACTION_NOOP; } else { @@ -1555,9 +1454,7 @@ public class RemoteViews implements Parcelable, Filter { return new RunnableAction(endAction); } } - } catch (ActionException e) { - throw e; - } catch (Exception ex) { + } catch (Throwable ex) { throw new ActionException(ex); } @@ -1573,10 +1470,16 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { + @Override + public int getActionTag() { + return REFLECTION_ACTION_TAG; + } + + @Override + public String getUniqueKey() { // Each type of reflection action corresponds to a setter, so each should be seen as // unique from the standpoint of merging. - return "ReflectionAction" + this.methodName + this.type; + return super.getUniqueKey() + this.methodName + this.type; } @Override @@ -1602,7 +1505,6 @@ public class RemoteViews implements Parcelable, Filter { } private void configureRemoteViewsAsChild(RemoteViews rv) { - mBitmapCache.assimilate(rv.mBitmapCache); rv.setBitmapCache(mBitmapCache); rv.setNotRoot(); } @@ -1632,14 +1534,13 @@ public class RemoteViews implements Parcelable, Filter { } ViewGroupActionAdd(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info, - int depth) { + int depth, Map<Class, Object> classCookies) { viewId = parcel.readInt(); mIndex = parcel.readInt(); - mNestedViews = new RemoteViews(parcel, bitmapCache, info, depth); + mNestedViews = new RemoteViews(parcel, bitmapCache, info, depth, classCookies); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(VIEW_GROUP_ACTION_ADD_TAG); dest.writeInt(viewId); dest.writeInt(mIndex); mNestedViews.writeToParcel(dest, flags); @@ -1647,8 +1548,7 @@ public class RemoteViews implements Parcelable, Filter { @Override public boolean hasSameAppInfo(ApplicationInfo parentInfo) { - return mNestedViews.mApplication.packageName.equals(parentInfo.packageName) - && mNestedViews.mApplication.uid == parentInfo.uid; + return mNestedViews.hasSameAppInfo(parentInfo); } @Override @@ -1700,11 +1600,6 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void updateMemoryUsageEstimate(MemoryUsageCounter counter) { - counter.increment(mNestedViews.estimateMemoryUsage()); - } - - @Override public void setBitmapCache(BitmapCache bitmapCache) { mNestedViews.setBitmapCache(bitmapCache); } @@ -1719,10 +1614,9 @@ public class RemoteViews implements Parcelable, Filter { return mNestedViews.prefersAsyncApply(); } - @Override - public String getActionName() { - return "ViewGroupActionAdd"; + public int getActionTag() { + return VIEW_GROUP_ACTION_ADD_TAG; } } @@ -1754,7 +1648,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(VIEW_GROUP_ACTION_REMOVE_TAG); dest.writeInt(viewId); dest.writeInt(mViewIdToKeep); } @@ -1820,8 +1713,8 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public String getActionName() { - return "ViewGroupActionRemove"; + public int getActionTag() { + return VIEW_GROUP_ACTION_REMOVE_TAG; } @Override @@ -1861,18 +1754,10 @@ public class RemoteViews implements Parcelable, Filter { isRelative = (parcel.readInt() != 0); useIcons = (parcel.readInt() != 0); if (useIcons) { - if (parcel.readInt() != 0) { - i1 = Icon.CREATOR.createFromParcel(parcel); - } - if (parcel.readInt() != 0) { - i2 = Icon.CREATOR.createFromParcel(parcel); - } - if (parcel.readInt() != 0) { - i3 = Icon.CREATOR.createFromParcel(parcel); - } - if (parcel.readInt() != 0) { - i4 = Icon.CREATOR.createFromParcel(parcel); - } + i1 = parcel.readTypedObject(Icon.CREATOR); + i2 = parcel.readTypedObject(Icon.CREATOR); + i3 = parcel.readTypedObject(Icon.CREATOR); + i4 = parcel.readTypedObject(Icon.CREATOR); } else { d1 = parcel.readInt(); d2 = parcel.readInt(); @@ -1882,35 +1767,14 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TEXT_VIEW_DRAWABLE_ACTION_TAG); dest.writeInt(viewId); dest.writeInt(isRelative ? 1 : 0); dest.writeInt(useIcons ? 1 : 0); if (useIcons) { - if (i1 != null) { - dest.writeInt(1); - i1.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } - if (i2 != null) { - dest.writeInt(1); - i2.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } - if (i3 != null) { - dest.writeInt(1); - i3.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } - if (i4 != null) { - dest.writeInt(1); - i4.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } + dest.writeTypedObject(i1, 0); + dest.writeTypedObject(i2, 0); + dest.writeTypedObject(i3, 0); + dest.writeTypedObject(i4, 0); } else { dest.writeInt(d1); dest.writeInt(d2); @@ -1981,8 +1845,9 @@ public class RemoteViews implements Parcelable, Filter { return useIcons; } - public String getActionName() { - return "TextViewDrawableAction"; + @Override + public int getActionTag() { + return TEXT_VIEW_DRAWABLE_ACTION_TAG; } boolean isRelative = false; @@ -2011,7 +1876,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TEXT_VIEW_SIZE_ACTION_TAG); dest.writeInt(viewId); dest.writeInt(units); dest.writeFloat(size); @@ -2024,8 +1888,9 @@ public class RemoteViews implements Parcelable, Filter { target.setTextSize(units, size); } - public String getActionName() { - return "TextViewSizeAction"; + @Override + public int getActionTag() { + return TEXT_VIEW_SIZE_ACTION_TAG; } int units; @@ -2053,7 +1918,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(VIEW_PADDING_ACTION_TAG); dest.writeInt(viewId); dest.writeInt(left); dest.writeInt(top); @@ -2068,8 +1932,9 @@ public class RemoteViews implements Parcelable, Filter { target.setPadding(left, top, right, bottom); } - public String getActionName() { - return "ViewPaddingAction"; + @Override + public int getActionTag() { + return VIEW_PADDING_ACTION_TAG; } int left, top, right, bottom; @@ -2086,6 +1951,9 @@ public class RemoteViews implements Parcelable, Filter { public static final int LAYOUT_WIDTH = 2; public static final int LAYOUT_MARGIN_BOTTOM_DIMEN = 3; + final int mProperty; + final int mValue; + /** * @param viewId ID of the view alter * @param property which layout parameter to alter @@ -2093,21 +1961,20 @@ public class RemoteViews implements Parcelable, Filter { */ public LayoutParamAction(int viewId, int property, int value) { this.viewId = viewId; - this.property = property; - this.value = value; + this.mProperty = property; + this.mValue = value; } public LayoutParamAction(Parcel parcel) { viewId = parcel.readInt(); - property = parcel.readInt(); - value = parcel.readInt(); + mProperty = parcel.readInt(); + mValue = parcel.readInt(); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(LAYOUT_PARAM_ACTION_TAG); dest.writeInt(viewId); - dest.writeInt(property); - dest.writeInt(value); + dest.writeInt(mProperty); + dest.writeInt(mValue); } @Override @@ -2120,27 +1987,27 @@ public class RemoteViews implements Parcelable, Filter { if (layoutParams == null) { return; } - switch (property) { + switch (mProperty) { case LAYOUT_MARGIN_END_DIMEN: if (layoutParams instanceof ViewGroup.MarginLayoutParams) { - int resolved = resolveDimenPixelOffset(target, value); + int resolved = resolveDimenPixelOffset(target, mValue); ((ViewGroup.MarginLayoutParams) layoutParams).setMarginEnd(resolved); target.setLayoutParams(layoutParams); } break; case LAYOUT_MARGIN_BOTTOM_DIMEN: if (layoutParams instanceof ViewGroup.MarginLayoutParams) { - int resolved = resolveDimenPixelOffset(target, value); + int resolved = resolveDimenPixelOffset(target, mValue); ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = resolved; target.setLayoutParams(layoutParams); } break; case LAYOUT_WIDTH: - layoutParams.width = value; + layoutParams.width = mValue; target.setLayoutParams(layoutParams); break; default: - throw new IllegalArgumentException("Unknown property " + property); + throw new IllegalArgumentException("Unknown property " + mProperty); } } @@ -2151,79 +2018,15 @@ public class RemoteViews implements Parcelable, Filter { return target.getContext().getResources().getDimensionPixelOffset(value); } - public String getActionName() { - return "LayoutParamAction" + property + "."; - } - - int property; - int value; - } - - /** - * Helper action to set a color filter on a compound drawable on a TextView. Supports relative - * (s/t/e/b) or cardinal (l/t/r/b) arrangement. - */ - private class TextViewDrawableColorFilterAction extends Action { - public TextViewDrawableColorFilterAction(int viewId, boolean isRelative, int index, - int color, PorterDuff.Mode mode) { - this.viewId = viewId; - this.isRelative = isRelative; - this.index = index; - this.color = color; - this.mode = mode; - } - - public TextViewDrawableColorFilterAction(Parcel parcel) { - viewId = parcel.readInt(); - isRelative = (parcel.readInt() != 0); - index = parcel.readInt(); - color = parcel.readInt(); - mode = readPorterDuffMode(parcel); - } - - private PorterDuff.Mode readPorterDuffMode(Parcel parcel) { - int mode = parcel.readInt(); - if (mode >= 0 && mode < PorterDuff.Mode.values().length) { - return PorterDuff.Mode.values()[mode]; - } else { - return PorterDuff.Mode.CLEAR; - } - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG); - dest.writeInt(viewId); - dest.writeInt(isRelative ? 1 : 0); - dest.writeInt(index); - dest.writeInt(color); - dest.writeInt(mode.ordinal()); - } - @Override - public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { - final TextView target = root.findViewById(viewId); - if (target == null) return; - Drawable[] drawables = isRelative - ? target.getCompoundDrawablesRelative() - : target.getCompoundDrawables(); - if (index < 0 || index >= 4) { - throw new IllegalStateException("index must be in range [0, 3]."); - } - Drawable d = drawables[index]; - if (d != null) { - d.mutate(); - d.setColorFilter(color, mode); - } + public int getActionTag() { + return LAYOUT_PARAM_ACTION_TAG; } - public String getActionName() { - return "TextViewDrawableColorFilterAction"; + @Override + public String getUniqueKey() { + return super.getUniqueKey() + mProperty; } - - final boolean isRelative; - final int index; - final int color; - final PorterDuff.Mode mode; } /** @@ -2242,7 +2045,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_REMOTE_INPUTS_ACTION_TAG); dest.writeInt(viewId); dest.writeTypedArray(remoteInputs, flags); } @@ -2255,8 +2057,9 @@ public class RemoteViews implements Parcelable, Filter { target.setTagInternal(R.id.remote_input_tag, remoteInputs); } - public String getActionName() { - return "SetRemoteInputsAction"; + @Override + public int getActionTag() { + return SET_REMOTE_INPUTS_ACTION_TAG; } final Parcelable[] remoteInputs; @@ -2278,7 +2081,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(OVERRIDE_TEXT_COLORS_TAG); dest.writeInt(textColor); } @@ -2303,33 +2105,10 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { - return "OverrideTextColorsAction"; - } - } - - /** - * Simple class used to keep track of memory usage in a RemoteViews. - * - */ - private class MemoryUsageCounter { - public void clear() { - mMemoryUsage = 0; - } - - public void increment(int numBytes) { - mMemoryUsage += numBytes; - } - - public int getMemoryUsage() { - return mMemoryUsage; - } - - public void addBitmapMemory(Bitmap b) { - increment(b.getAllocationByteCount()); + @Override + public int getActionTag() { + return OVERRIDE_TEXT_COLORS_TAG; } - - int mMemoryUsage; } /** @@ -2370,9 +2149,7 @@ public class RemoteViews implements Parcelable, Filter { mApplication = application; mLayoutId = layoutId; mBitmapCache = new BitmapCache(); - // setup the memory usage statistics - mMemoryUsageCounter = new MemoryUsageCounter(); - recalculateMemoryUsage(); + mClassCookies = null; } private boolean hasLandscapeAndPortraitLayouts() { @@ -2390,8 +2167,7 @@ public class RemoteViews implements Parcelable, Filter { if (landscape == null || portrait == null) { throw new RuntimeException("Both RemoteViews must be non-null"); } - if (landscape.mApplication.uid != portrait.mApplication.uid - || !landscape.mApplication.packageName.equals(portrait.mApplication.packageName)) { + if (!landscape.hasSameAppInfo(portrait.mApplication)) { throw new RuntimeException("Both RemoteViews must share the same package and user"); } mApplication = portrait.mApplication; @@ -2400,14 +2176,45 @@ public class RemoteViews implements Parcelable, Filter { mLandscape = landscape; mPortrait = portrait; - // setup the memory usage statistics - mMemoryUsageCounter = new MemoryUsageCounter(); - mBitmapCache = new BitmapCache(); configureRemoteViewsAsChild(landscape); configureRemoteViewsAsChild(portrait); - recalculateMemoryUsage(); + mClassCookies = (portrait.mClassCookies != null) + ? portrait.mClassCookies : landscape.mClassCookies; + } + + /** + * Creates a copy of another RemoteViews. + */ + public RemoteViews(RemoteViews src) { + mBitmapCache = src.mBitmapCache; + mApplication = src.mApplication; + mIsRoot = src.mIsRoot; + mLayoutId = src.mLayoutId; + mIsWidgetCollectionChild = src.mIsWidgetCollectionChild; + mReapplyDisallowed = src.mReapplyDisallowed; + mClassCookies = src.mClassCookies; + + if (src.hasLandscapeAndPortraitLayouts()) { + mLandscape = new RemoteViews(src.mLandscape); + mPortrait = new RemoteViews(src.mPortrait); + } + + if (src.mActions != null) { + Parcel p = Parcel.obtain(); + p.putClassCookies(mClassCookies); + src.writeActionsToParcel(p); + p.setDataPosition(0); + // Since src is already in memory, we do not care about stack overflow as it has + // already been read once. + readActionsFromParcel(p, 0); + p.recycle(); + } + + // Now that everything is initialized and duplicated, setting a new BitmapCache will + // re-initialize the cache. + setBitmapCache(new BitmapCache()); } /** @@ -2416,10 +2223,11 @@ public class RemoteViews implements Parcelable, Filter { * @param parcel */ public RemoteViews(Parcel parcel) { - this(parcel, null, null, 0); + this(parcel, null, null, 0, null); } - private RemoteViews(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info, int depth) { + private RemoteViews(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info, int depth, + Map<Class, Object> classCookies) { if (depth > MAX_NESTED_VIEWS && (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) { throw new IllegalArgumentException("Too many nested views."); @@ -2431,8 +2239,11 @@ public class RemoteViews implements Parcelable, Filter { // We only store a bitmap cache in the root of the RemoteViews. if (bitmapCache == null) { mBitmapCache = new BitmapCache(parcel); + // Store the class cookies such that they are available when we clone this RemoteView. + mClassCookies = parcel.copyClassCookies(); } else { setBitmapCache(bitmapCache); + mClassCookies = classCookies; setNotRoot(); } @@ -2442,117 +2253,88 @@ public class RemoteViews implements Parcelable, Filter { mLayoutId = parcel.readInt(); mIsWidgetCollectionChild = parcel.readInt() == 1; - int count = parcel.readInt(); - if (count > 0) { - mActions = new ArrayList<Action>(count); - for (int i=0; i<count; i++) { - int tag = parcel.readInt(); - switch (tag) { - case SET_ON_CLICK_PENDING_INTENT_TAG: - mActions.add(new SetOnClickPendingIntent(parcel)); - break; - case SET_DRAWABLE_PARAMETERS_TAG: - mActions.add(new SetDrawableParameters(parcel)); - break; - case REFLECTION_ACTION_TAG: - mActions.add(new ReflectionAction(parcel)); - break; - case VIEW_GROUP_ACTION_ADD_TAG: - mActions.add(new ViewGroupActionAdd(parcel, mBitmapCache, mApplication, - depth)); - break; - case VIEW_GROUP_ACTION_REMOVE_TAG: - mActions.add(new ViewGroupActionRemove(parcel)); - break; - case SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG: - mActions.add(new ReflectionActionWithoutParams(parcel)); - break; - case SET_EMPTY_VIEW_ACTION_TAG: - mActions.add(new SetEmptyView(parcel)); - break; - case SET_PENDING_INTENT_TEMPLATE_TAG: - mActions.add(new SetPendingIntentTemplate(parcel)); - break; - case SET_ON_CLICK_FILL_IN_INTENT_TAG: - mActions.add(new SetOnClickFillInIntent(parcel)); - break; - case SET_REMOTE_VIEW_ADAPTER_INTENT_TAG: - mActions.add(new SetRemoteViewsAdapterIntent(parcel)); - break; - case TEXT_VIEW_DRAWABLE_ACTION_TAG: - mActions.add(new TextViewDrawableAction(parcel)); - break; - case TEXT_VIEW_SIZE_ACTION_TAG: - mActions.add(new TextViewSizeAction(parcel)); - break; - case VIEW_PADDING_ACTION_TAG: - mActions.add(new ViewPaddingAction(parcel)); - break; - case BITMAP_REFLECTION_ACTION_TAG: - mActions.add(new BitmapReflectionAction(parcel)); - break; - case SET_REMOTE_VIEW_ADAPTER_LIST_TAG: - mActions.add(new SetRemoteViewsAdapterList(parcel)); - break; - case TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG: - mActions.add(new TextViewDrawableColorFilterAction(parcel)); - break; - case SET_REMOTE_INPUTS_ACTION_TAG: - mActions.add(new SetRemoteInputsAction(parcel)); - break; - case LAYOUT_PARAM_ACTION_TAG: - mActions.add(new LayoutParamAction(parcel)); - break; - case OVERRIDE_TEXT_COLORS_TAG: - mActions.add(new OverrideTextColorsAction(parcel)); - break; - default: - throw new ActionException("Tag " + tag + " not found"); - } - } - } + readActionsFromParcel(parcel, depth); } else { // MODE_HAS_LANDSCAPE_AND_PORTRAIT - mLandscape = new RemoteViews(parcel, mBitmapCache, info, depth); - mPortrait = new RemoteViews(parcel, mBitmapCache, mLandscape.mApplication, depth); + mLandscape = new RemoteViews(parcel, mBitmapCache, info, depth, mClassCookies); + mPortrait = new RemoteViews(parcel, mBitmapCache, mLandscape.mApplication, depth, + mClassCookies); mApplication = mPortrait.mApplication; mLayoutId = mPortrait.getLayoutId(); } mReapplyDisallowed = parcel.readInt() == 0; - - // setup the memory usage statistics - mMemoryUsageCounter = new MemoryUsageCounter(); - recalculateMemoryUsage(); } + private void readActionsFromParcel(Parcel parcel, int depth) { + int count = parcel.readInt(); + if (count > 0) { + mActions = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + mActions.add(getActionFromParcel(parcel, depth)); + } + } + } + + private Action getActionFromParcel(Parcel parcel, int depth) { + int tag = parcel.readInt(); + switch (tag) { + case SET_ON_CLICK_PENDING_INTENT_TAG: + return new SetOnClickPendingIntent(parcel); + case SET_DRAWABLE_TINT_TAG: + return new SetDrawableTint(parcel); + case REFLECTION_ACTION_TAG: + return new ReflectionAction(parcel); + case VIEW_GROUP_ACTION_ADD_TAG: + return new ViewGroupActionAdd(parcel, mBitmapCache, mApplication, depth, + mClassCookies); + case VIEW_GROUP_ACTION_REMOVE_TAG: + return new ViewGroupActionRemove(parcel); + case VIEW_CONTENT_NAVIGATION_TAG: + return new ViewContentNavigation(parcel); + case SET_EMPTY_VIEW_ACTION_TAG: + return new SetEmptyView(parcel); + case SET_PENDING_INTENT_TEMPLATE_TAG: + return new SetPendingIntentTemplate(parcel); + case SET_ON_CLICK_FILL_IN_INTENT_TAG: + return new SetOnClickFillInIntent(parcel); + case SET_REMOTE_VIEW_ADAPTER_INTENT_TAG: + return new SetRemoteViewsAdapterIntent(parcel); + case TEXT_VIEW_DRAWABLE_ACTION_TAG: + return new TextViewDrawableAction(parcel); + case TEXT_VIEW_SIZE_ACTION_TAG: + return new TextViewSizeAction(parcel); + case VIEW_PADDING_ACTION_TAG: + return new ViewPaddingAction(parcel); + case BITMAP_REFLECTION_ACTION_TAG: + return new BitmapReflectionAction(parcel); + case SET_REMOTE_VIEW_ADAPTER_LIST_TAG: + return new SetRemoteViewsAdapterList(parcel); + case SET_REMOTE_INPUTS_ACTION_TAG: + return new SetRemoteInputsAction(parcel); + case LAYOUT_PARAM_ACTION_TAG: + return new LayoutParamAction(parcel); + case OVERRIDE_TEXT_COLORS_TAG: + return new OverrideTextColorsAction(parcel); + default: + throw new ActionException("Tag " + tag + " not found"); + } + }; + /** * Returns a deep copy of the RemoteViews object. The RemoteView may not be * attached to another RemoteView -- it must be the root of a hierarchy. * + * @deprecated use {@link #RemoteViews(RemoteViews)} instead. * @throws IllegalStateException if this is not the root of a RemoteView * hierarchy */ @Override + @Deprecated public RemoteViews clone() { - synchronized (this) { - Preconditions.checkState(mIsRoot, "RemoteView has been attached to another RemoteView. " - + "May only clone the root of a RemoteView hierarchy."); - - Parcel p = Parcel.obtain(); - - // Do not parcel the Bitmap cache - doing so creates an expensive copy of all bitmaps. - // Instead pretend we're not owning the cache while parceling. - mIsRoot = false; - writeToParcel(p, PARCELABLE_ELIDE_DUPLICATES); - p.setDataPosition(0); - mIsRoot = true; - - RemoteViews rv = new RemoteViews(p, mBitmapCache.clone(), mApplication, 0); - rv.mIsRoot = true; + Preconditions.checkState(mIsRoot, "RemoteView has been attached to another RemoteView. " + + "May only clone the root of a RemoteView hierarchy."); - p.recycle(); - return rv; - } + return new RemoteViews(this); } public String getPackage() { @@ -2582,30 +2364,6 @@ public class RemoteViews implements Parcelable, Filter { } /** - * Updates the memory usage statistics. - */ - private void recalculateMemoryUsage() { - mMemoryUsageCounter.clear(); - - if (!hasLandscapeAndPortraitLayouts()) { - // Accumulate the memory usage for each action - if (mActions != null) { - final int count = mActions.size(); - for (int i= 0; i < count; ++i) { - mActions.get(i).updateMemoryUsageEstimate(mMemoryUsageCounter); - } - } - if (mIsRoot) { - mBitmapCache.addBitmapMemory(mMemoryUsageCounter); - } - } else { - mMemoryUsageCounter.increment(mLandscape.estimateMemoryUsage()); - mMemoryUsageCounter.increment(mPortrait.estimateMemoryUsage()); - mBitmapCache.addBitmapMemory(mMemoryUsageCounter); - } - } - - /** * Recursively sets BitmapCache in the hierarchy and update the bitmap ids. */ private void setBitmapCache(BitmapCache bitmapCache) { @@ -2628,7 +2386,7 @@ public class RemoteViews implements Parcelable, Filter { */ /** @hide */ public int estimateMemoryUsage() { - return mMemoryUsageCounter.getMemoryUsage(); + return mBitmapCache.getBitmapMemory(); } /** @@ -2643,12 +2401,9 @@ public class RemoteViews implements Parcelable, Filter { " portrait layouts individually before constructing the combined layout."); } if (mActions == null) { - mActions = new ArrayList<Action>(); + mActions = new ArrayList<>(); } mActions.add(a); - - // update the memory usage stats - a.updateMemoryUsageEstimate(mMemoryUsageCounter); } /** @@ -2672,7 +2427,7 @@ public class RemoteViews implements Parcelable, Filter { * given {@link RemoteViews}. * * @param viewId The id of the parent {@link ViewGroup} to add the child into. - * @param nestedView {@link RemoveViews} of the child to add. + * @param nestedView {@link RemoteViews} of the child to add. * @param index The position at which to add the child. * * @hide @@ -2710,7 +2465,7 @@ public class RemoteViews implements Parcelable, Filter { * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showNext()} */ public void showNext(int viewId) { - addAction(new ReflectionActionWithoutParams(viewId, "showNext")); + addAction(new ViewContentNavigation(viewId, true /* next */)); } /** @@ -2719,7 +2474,7 @@ public class RemoteViews implements Parcelable, Filter { * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showPrevious()} */ public void showPrevious(int viewId) { - addAction(new ReflectionActionWithoutParams(viewId, "showPrevious")); + addAction(new ViewContentNavigation(viewId, false /* next */)); } /** @@ -2793,28 +2548,6 @@ public class RemoteViews implements Parcelable, Filter { } /** - * Equivalent to applying a color filter on one of the drawables in - * {@link android.widget.TextView#getCompoundDrawablesRelative()}. - * - * @param viewId The id of the view whose text should change. - * @param index The index of the drawable in the array of - * {@link android.widget.TextView#getCompoundDrawablesRelative()} to set the color - * filter on. Must be in [0, 3]. - * @param color The color of the color filter. See - * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}. - * @param mode The mode of the color filter. See - * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}. - * @hide - */ - public void setTextViewCompoundDrawablesRelativeColorFilter(int viewId, - int index, int color, PorterDuff.Mode mode) { - if (index < 0 || index >= 4) { - throw new IllegalArgumentException("index must be in range [0, 3]."); - } - addAction(new TextViewDrawableColorFilterAction(viewId, true, index, color, mode)); - } - - /** * Equivalent to calling {@link * TextView#setCompoundDrawablesWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)} * using the drawables yielded by {@link Icon#loadDrawable(Context)}. @@ -2958,7 +2691,11 @@ public class RemoteViews implements Parcelable, Filter { /** * Equivalent to calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} - * to launch the provided {@link PendingIntent}. + * to launch the provided {@link PendingIntent}. The source bounds + * ({@link Intent#getSourceBounds()}) of the intent will be set to the bounds of the clicked + * view in screen space. + * Note that any activity options associated with the pendingIntent may get overridden + * before starting the intent. * * When setting the on-click action of items within collections (eg. {@link ListView}, * {@link StackView} etc.), this method will not work. Instead, use {@link @@ -3011,12 +2748,10 @@ public class RemoteViews implements Parcelable, Filter { /** * @hide - * Equivalent to calling a combination of {@link Drawable#setAlpha(int)}, + * Equivalent to calling * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}, - * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given - * view. + * on the {@link Drawable} of a given view. * <p> - * You can omit specific calls by marking their values with null or -1. * * @param viewId The id of the view that contains the target * {@link Drawable} @@ -3025,20 +2760,15 @@ public class RemoteViews implements Parcelable, Filter { * {@link android.view.View#getBackground()}. Otherwise, assume * the target view is an {@link ImageView} and apply them to * {@link ImageView#getDrawable()}. - * @param alpha Specify an alpha value for the drawable, or -1 to leave - * unchanged. * @param colorFilter Specify a color for a * {@link android.graphics.ColorFilter} for this drawable. This will be ignored if * {@code mode} is {@code null}. * @param mode Specify a PorterDuff mode for this drawable, or null to leave * unchanged. - * @param level Specify the level for the drawable, or -1 to leave - * unchanged. */ - public void setDrawableParameters(int viewId, boolean targetBackground, int alpha, - int colorFilter, PorterDuff.Mode mode, int level) { - addAction(new SetDrawableParameters(viewId, targetBackground, alpha, - colorFilter, mode, level)); + public void setDrawableTint(int viewId, boolean targetBackground, + int colorFilter, @NonNull PorterDuff.Mode mode) { + addAction(new SetDrawableTint(viewId, targetBackground, colorFilter, mode)); } /** @@ -3805,18 +3535,7 @@ public class RemoteViews implements Parcelable, Filter { } dest.writeInt(mLayoutId); dest.writeInt(mIsWidgetCollectionChild ? 1 : 0); - int count; - if (mActions != null) { - count = mActions.size(); - } else { - count = 0; - } - dest.writeInt(count); - for (int i=0; i<count; i++) { - Action a = mActions.get(i); - a.writeToParcel(dest, a.hasSameAppInfo(mApplication) - ? PARCELABLE_ELIDE_DUPLICATES : 0); - } + writeActionsToParcel(dest); } else { dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT); // We only write the bitmap cache if we are the root RemoteViews, as this cache @@ -3831,6 +3550,22 @@ public class RemoteViews implements Parcelable, Filter { dest.writeInt(mReapplyDisallowed ? 1 : 0); } + private void writeActionsToParcel(Parcel parcel) { + int count; + if (mActions != null) { + count = mActions.size(); + } else { + count = 0; + } + parcel.writeInt(count); + for (int i = 0; i < count; i++) { + Action a = mActions.get(i); + parcel.writeInt(a.getActionTag()); + a.writeToParcel(parcel, a.hasSameAppInfo(mApplication) + ? PARCELABLE_ELIDE_DUPLICATES : 0); + } + } + private static ApplicationInfo getApplicationInfo(String packageName, int userId) { if (packageName == null) { return null; @@ -3858,6 +3593,15 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Returns true if the {@link #mApplication} is same as the provided info. + * + * @hide + */ + public boolean hasSameAppInfo(ApplicationInfo info) { + return mApplication.packageName.equals(info.packageName) && mApplication.uid == info.uid; + } + + /** * Parcelable.Creator that instantiates RemoteViews objects */ public static final Parcelable.Creator<RemoteViews> CREATOR = new Parcelable.Creator<RemoteViews>() { diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 09686521b432..e5ae0ca0070c 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -16,11 +16,15 @@ package android.widget; -import android.Manifest; +import android.annotation.WorkerThread; +import android.app.IServiceConnection; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -28,7 +32,6 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; -import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; @@ -38,7 +41,6 @@ import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.widget.RemoteViews.OnClickHandler; -import com.android.internal.widget.IRemoteViewsAdapterConnection; import com.android.internal.widget.IRemoteViewsFactory; import java.lang.ref.WeakReference; @@ -48,73 +50,80 @@ import java.util.LinkedList; import java.util.concurrent.Executor; /** - * An adapter to a RemoteViewsService which fetches and caches RemoteViews - * to be later inflated as child views. + * An adapter to a RemoteViewsService which fetches and caches RemoteViews to be later inflated as + * child views. + * + * The adapter runs in the host process, typically a Launcher app. + * + * It makes a service connection to the {@link RemoteViewsService} running in the + * AppWidgetsProvider's process. This connection is made on a background thread (and proxied via + * the platform to get the bind permissions) and all interaction with the service is done on the + * background thread. + * + * On first bind, the adapter will load can cache the RemoteViews locally. Afterwards the + * connection is only made when new RemoteViews are required. + * @hide */ -/** @hide */ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback { - private static final String MULTI_USER_PERM = Manifest.permission.INTERACT_ACROSS_USERS_FULL; private static final String TAG = "RemoteViewsAdapter"; // The max number of items in the cache - private static final int sDefaultCacheSize = 40; + private static final int DEFAULT_CACHE_SIZE = 40; // The delay (in millis) to wait until attempting to unbind from a service after a request. // This ensures that we don't stay continually bound to the service and that it can be destroyed // if we need the memory elsewhere in the system. - private static final int sUnbindServiceDelay = 5000; + private static final int UNBIND_SERVICE_DELAY = 5000; // Default height for the default loading view, in case we cannot get inflate the first view - private static final int sDefaultLoadingViewHeight = 50; + private static final int DEFAULT_LOADING_VIEW_HEIGHT = 50; + + // We cache the FixedSizeRemoteViewsCaches across orientation. These are the related data + // structures; + private static final HashMap<RemoteViewsCacheKey, FixedSizeRemoteViewsCache> + sCachedRemoteViewsCaches = new HashMap<>(); + private static final HashMap<RemoteViewsCacheKey, Runnable> + sRemoteViewsCacheRemoveRunnables = new HashMap<>(); - // Type defs for controlling different messages across the main and worker message queues - private static final int sDefaultMessageType = 0; - private static final int sUnbindServiceMessageType = 1; + private static HandlerThread sCacheRemovalThread; + private static Handler sCacheRemovalQueue; + + // We keep the cache around for a duration after onSaveInstanceState for use on re-inflation. + // If a new RemoteViewsAdapter with the same intent / widget id isn't constructed within this + // duration, the cache is dropped. + private static final int REMOTE_VIEWS_CACHE_DURATION = 5000; private final Context mContext; private final Intent mIntent; private final int mAppWidgetId; private final Executor mAsyncViewLoadExecutor; - private RemoteViewsAdapterServiceConnection mServiceConnection; - private WeakReference<RemoteAdapterConnectionCallback> mCallback; private OnClickHandler mRemoteViewsOnClickHandler; private final FixedSizeRemoteViewsCache mCache; private int mVisibleWindowLowerBound; private int mVisibleWindowUpperBound; - // A flag to determine whether we should notify data set changed after we connect - private boolean mNotifyDataSetChangedAfterOnServiceConnected = false; - // The set of requested views that are to be notified when the associated RemoteViews are // loaded. private RemoteViewsFrameLayoutRefSet mRequestedViews; - private HandlerThread mWorkerThread; + private final HandlerThread mWorkerThread; // items may be interrupted within the normally processed queues - private Handler mWorkerQueue; - private Handler mMainQueue; - - // We cache the FixedSizeRemoteViewsCaches across orientation. These are the related data - // structures; - private static final HashMap<RemoteViewsCacheKey, FixedSizeRemoteViewsCache> - sCachedRemoteViewsCaches = new HashMap<>(); - private static final HashMap<RemoteViewsCacheKey, Runnable> - sRemoteViewsCacheRemoveRunnables = new HashMap<>(); - - private static HandlerThread sCacheRemovalThread; - private static Handler sCacheRemovalQueue; - - // We keep the cache around for a duration after onSaveInstanceState for use on re-inflation. - // If a new RemoteViewsAdapter with the same intent / widget id isn't constructed within this - // duration, the cache is dropped. - private static final int REMOTE_VIEWS_CACHE_DURATION = 5000; + private final Handler mMainHandler; + private final RemoteServiceHandler mServiceHandler; + private final RemoteAdapterConnectionCallback mCallback; // Used to indicate to the AdapterView that it can use this Adapter immediately after // construction (happens when we have a cached FixedSizeRemoteViewsCache). private boolean mDataReady = false; /** + * USed to dedupe {@link RemoteViews#mApplication} so that we do not hold on to + * multiple copies of the same ApplicationInfo object. + */ + private ApplicationInfo mLastRemoteViewAppInfo; + + /** * An interface for the RemoteAdapter to notify other classes when adapters * are actually connected to/disconnected from their actual services. */ @@ -151,154 +160,192 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } } + static final int MSG_REQUEST_BIND = 1; + static final int MSG_NOTIFY_DATA_SET_CHANGED = 2; + static final int MSG_LOAD_NEXT_ITEM = 3; + static final int MSG_UNBIND_SERVICE = 4; + + private static final int MSG_MAIN_HANDLER_COMMIT_METADATA = 1; + private static final int MSG_MAIN_HANDLER_SUPER_NOTIFY_DATA_SET_CHANGED = 2; + private static final int MSG_MAIN_HANDLER_REMOTE_ADAPTER_CONNECTED = 3; + private static final int MSG_MAIN_HANDLER_REMOTE_ADAPTER_DISCONNECTED = 4; + private static final int MSG_MAIN_HANDLER_REMOTE_VIEWS_LOADED = 5; + /** - * The service connection that gets populated when the RemoteViewsService is - * bound. This must be a static inner class to ensure that no references to the outer - * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being - * garbage collected, and would cause us to leak activities due to the caching mechanism for - * FrameLayouts in the adapter). + * Handler for various interactions with the {@link RemoteViewsService}. */ - private static class RemoteViewsAdapterServiceConnection extends - IRemoteViewsAdapterConnection.Stub { - private boolean mIsConnected; - private boolean mIsConnecting; - private WeakReference<RemoteViewsAdapter> mAdapter; + private static class RemoteServiceHandler extends Handler implements ServiceConnection { + + private final WeakReference<RemoteViewsAdapter> mAdapter; + private final Context mContext; + private IRemoteViewsFactory mRemoteViewsFactory; - public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) { - mAdapter = new WeakReference<RemoteViewsAdapter>(adapter); + // The last call to notifyDataSetChanged didn't succeed, try again on next service bind. + private boolean mNotifyDataSetChangedPending = false; + private boolean mBindRequested = false; + + RemoteServiceHandler(Looper workerLooper, RemoteViewsAdapter adapter, Context context) { + super(workerLooper); + mAdapter = new WeakReference<>(adapter); + mContext = context; } - public synchronized void bind(Context context, int appWidgetId, Intent intent) { - if (!mIsConnecting) { - try { - RemoteViewsAdapter adapter; - final AppWidgetManager mgr = AppWidgetManager.getInstance(context); - if ((adapter = mAdapter.get()) != null) { - mgr.bindRemoteViewsService(context.getOpPackageName(), appWidgetId, - intent, asBinder()); - } else { - Slog.w(TAG, "bind: adapter was null"); - } - mIsConnecting = true; - } catch (Exception e) { - Log.e("RVAServiceConnection", "bind(): " + e.getMessage()); - mIsConnecting = false; - mIsConnected = false; + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + // This is called on the same thread. + mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service); + enqueueDeferredUnbindServiceMessage(); + + RemoteViewsAdapter adapter = mAdapter.get(); + if (adapter == null) { + return; + } + + if (mNotifyDataSetChangedPending) { + mNotifyDataSetChangedPending = false; + Message msg = Message.obtain(this, MSG_NOTIFY_DATA_SET_CHANGED); + handleMessage(msg); + msg.recycle(); + } else { + if (!sendNotifyDataSetChange(false)) { + return; } + + // Request meta data so that we have up to date data when calling back to + // the remote adapter callback + adapter.updateTemporaryMetaData(mRemoteViewsFactory); + adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_COMMIT_METADATA); + adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_REMOTE_ADAPTER_CONNECTED); } } - public synchronized void unbind(Context context, int appWidgetId, Intent intent) { - try { - RemoteViewsAdapter adapter; - final AppWidgetManager mgr = AppWidgetManager.getInstance(context); - if ((adapter = mAdapter.get()) != null) { - mgr.unbindRemoteViewsService(context.getOpPackageName(), appWidgetId, intent); - } else { - Slog.w(TAG, "unbind: adapter was null"); - } - mIsConnecting = false; - } catch (Exception e) { - Log.e("RVAServiceConnection", "unbind(): " + e.getMessage()); - mIsConnecting = false; - mIsConnected = false; + @Override + public void onServiceDisconnected(ComponentName name) { + mRemoteViewsFactory = null; + RemoteViewsAdapter adapter = mAdapter.get(); + if (adapter != null) { + adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_REMOTE_ADAPTER_DISCONNECTED); } } - public synchronized void onServiceConnected(IBinder service) { - mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service); + @Override + public void handleMessage(Message msg) { + RemoteViewsAdapter adapter = mAdapter.get(); - // Remove any deferred unbind messages - final RemoteViewsAdapter adapter = mAdapter.get(); - if (adapter == null) return; - - // Queue up work that we need to do for the callback to run - adapter.mWorkerQueue.post(new Runnable() { - @Override - public void run() { - if (adapter.mNotifyDataSetChangedAfterOnServiceConnected) { - // Handle queued notifyDataSetChanged() if necessary - adapter.onNotifyDataSetChanged(); - } else { - IRemoteViewsFactory factory = - adapter.mServiceConnection.getRemoteViewsFactory(); - try { - if (!factory.isCreated()) { - // We only call onDataSetChanged() if this is the factory was just - // create in response to this bind - factory.onDataSetChanged(); - } - } catch (RemoteException e) { - Log.e(TAG, "Error notifying factory of data set changed in " + - "onServiceConnected(): " + e.getMessage()); - - // Return early to prevent anything further from being notified - // (effectively nothing has changed) - return; - } catch (RuntimeException e) { - Log.e(TAG, "Error notifying factory of data set changed in " + - "onServiceConnected(): " + e.getMessage()); - } + switch (msg.what) { + case MSG_REQUEST_BIND: { + if (adapter == null || mRemoteViewsFactory != null) { + enqueueDeferredUnbindServiceMessage(); + } + if (mBindRequested) { + return; + } + int flags = Context.BIND_AUTO_CREATE + | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE; + final IServiceConnection sd = mContext.getServiceDispatcher(this, this, flags); + Intent intent = (Intent) msg.obj; + int appWidgetId = msg.arg1; + mBindRequested = AppWidgetManager.getInstance(mContext) + .bindRemoteViewsService(mContext, appWidgetId, intent, sd, flags); + return; + } + case MSG_NOTIFY_DATA_SET_CHANGED: { + enqueueDeferredUnbindServiceMessage(); + if (adapter == null) { + return; + } + if (mRemoteViewsFactory == null) { + mNotifyDataSetChangedPending = true; + adapter.requestBindService(); + return; + } + if (!sendNotifyDataSetChange(true)) { + return; + } - // Request meta data so that we have up to date data when calling back to - // the remote adapter callback - adapter.updateTemporaryMetaData(); - - // Notify the host that we've connected - adapter.mMainQueue.post(new Runnable() { - @Override - public void run() { - synchronized (adapter.mCache) { - adapter.mCache.commitTemporaryMetaData(); - } - - final RemoteAdapterConnectionCallback callback = - adapter.mCallback.get(); - if (callback != null) { - callback.onRemoteAdapterConnected(); - } - } - }); + // Flush the cache so that we can reload new items from the service + synchronized (adapter.mCache) { + adapter.mCache.reset(); } - // Enqueue unbind message - adapter.enqueueDeferredUnbindServiceMessage(); - mIsConnected = true; - mIsConnecting = false; - } - }); - } + // Re-request the new metadata (only after the notification to the factory) + adapter.updateTemporaryMetaData(mRemoteViewsFactory); + int newCount; + int[] visibleWindow; + synchronized (adapter.mCache.getTemporaryMetaData()) { + newCount = adapter.mCache.getTemporaryMetaData().count; + visibleWindow = adapter.getVisibleWindow(newCount); + } - public synchronized void onServiceDisconnected() { - mIsConnected = false; - mIsConnecting = false; - mRemoteViewsFactory = null; + // Pre-load (our best guess of) the views which are currently visible in the + // AdapterView. This mitigates flashing and flickering of loading views when a + // widget notifies that its data has changed. + for (int position : visibleWindow) { + // Because temporary meta data is only ever modified from this thread + // (ie. mWorkerThread), it is safe to assume that count is a valid + // representation. + if (position < newCount) { + adapter.updateRemoteViews(mRemoteViewsFactory, position, false); + } + } - // Clear the main/worker queues - final RemoteViewsAdapter adapter = mAdapter.get(); - if (adapter == null) return; + // Propagate the notification back to the base adapter + adapter.mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_COMMIT_METADATA); + adapter.mMainHandler.sendEmptyMessage( + MSG_MAIN_HANDLER_SUPER_NOTIFY_DATA_SET_CHANGED); + return; + } - adapter.mMainQueue.post(new Runnable() { - @Override - public void run() { - // Dequeue any unbind messages - adapter.mMainQueue.removeMessages(sUnbindServiceMessageType); + case MSG_LOAD_NEXT_ITEM: { + if (adapter == null || mRemoteViewsFactory == null) { + return; + } + removeMessages(MSG_UNBIND_SERVICE); + // Get the next index to load + final int position = adapter.mCache.getNextIndexToLoad(); + if (position > -1) { + // Load the item, and notify any existing RemoteViewsFrameLayouts + adapter.updateRemoteViews(mRemoteViewsFactory, position, true); - final RemoteAdapterConnectionCallback callback = adapter.mCallback.get(); - if (callback != null) { - callback.onRemoteAdapterDisconnected(); + // Queue up for the next one to load + sendEmptyMessage(MSG_LOAD_NEXT_ITEM); + } else { + // No more items to load, so queue unbind + enqueueDeferredUnbindServiceMessage(); } + return; + } + case MSG_UNBIND_SERVICE: { + unbindNow(); + return; } - }); + } } - public synchronized IRemoteViewsFactory getRemoteViewsFactory() { - return mRemoteViewsFactory; + protected void unbindNow() { + if (mBindRequested) { + mBindRequested = false; + mContext.unbindService(this); + } + mRemoteViewsFactory = null; } - public synchronized boolean isConnected() { - return mIsConnected; + private boolean sendNotifyDataSetChange(boolean always) { + try { + if (always || !mRemoteViewsFactory.isCreated()) { + mRemoteViewsFactory.onDataSetChanged(); + } + return true; + } catch (RemoteException | RuntimeException e) { + Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); + return false; + } + } + + private void enqueueDeferredUnbindServiceMessage() { + removeMessages(MSG_UNBIND_SERVICE); + sendEmptyMessageDelayed(MSG_UNBIND_SERVICE, UNBIND_SERVICE_DELAY); } } @@ -309,6 +356,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback static class RemoteViewsFrameLayout extends AppWidgetHostView { private final FixedSizeRemoteViewsCache mCache; + public int cacheIndex = -1; + public RemoteViewsFrameLayout(Context context, FixedSizeRemoteViewsCache cache) { super(context); mCache = cache; @@ -359,26 +408,23 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the * adapter that have not yet had their RemoteViews loaded. */ - private class RemoteViewsFrameLayoutRefSet { - private final SparseArray<LinkedList<RemoteViewsFrameLayout>> mReferences = - new SparseArray<>(); - private final HashMap<RemoteViewsFrameLayout, LinkedList<RemoteViewsFrameLayout>> - mViewToLinkedList = new HashMap<>(); + private class RemoteViewsFrameLayoutRefSet + extends SparseArray<LinkedList<RemoteViewsFrameLayout>> { /** * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter. */ public void add(int position, RemoteViewsFrameLayout layout) { - LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(position); + LinkedList<RemoteViewsFrameLayout> refs = get(position); // Create the list if necessary if (refs == null) { - refs = new LinkedList<RemoteViewsFrameLayout>(); - mReferences.put(position, refs); + refs = new LinkedList<>(); + put(position, refs); } - mViewToLinkedList.put(layout, refs); // Add the references to the list + layout.cacheIndex = position; refs.add(layout); } @@ -389,18 +435,13 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback public void notifyOnRemoteViewsLoaded(int position, RemoteViews view) { if (view == null) return; - final LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(position); + // Remove this set from the original mapping + final LinkedList<RemoteViewsFrameLayout> refs = removeReturnOld(position); if (refs != null) { // Notify all the references for that position of the newly loaded RemoteViews for (final RemoteViewsFrameLayout ref : refs) { ref.onRemoteViewsLoaded(view, mRemoteViewsOnClickHandler, true); - if (mViewToLinkedList.containsKey(ref)) { - mViewToLinkedList.remove(ref); - } } - refs.clear(); - // Remove this set from the original mapping - mReferences.remove(position); } } @@ -408,20 +449,14 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback * We need to remove views from this set if they have been recycled by the AdapterView. */ public void removeView(RemoteViewsFrameLayout rvfl) { - if (mViewToLinkedList.containsKey(rvfl)) { - mViewToLinkedList.get(rvfl).remove(rvfl); - mViewToLinkedList.remove(rvfl); + if (rvfl.cacheIndex < 0) { + return; } - } - - /** - * Removes all references to all RemoteViewsFrameLayouts returned by the adapter. - */ - public void clear() { - // We currently just clear the references, and leave all the previous layouts returned - // in their default state of the loading view. - mReferences.clear(); - mViewToLinkedList.clear(); + final LinkedList<RemoteViewsFrameLayout> refs = get(rvfl.cacheIndex); + if (refs != null) { + refs.remove(rvfl); + } + rvfl.cacheIndex = -1; } } @@ -512,7 +547,6 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback * */ private static class FixedSizeRemoteViewsCache { - private static final String TAG = "FixedSizeRemoteViewsCache"; // The meta data related to all the RemoteViews, ie. count, is stable, etc. // The meta data objects are made final so that they can be locked on independently @@ -534,7 +568,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // too much memory. private final SparseArray<RemoteViews> mIndexRemoteViews = new SparseArray<>(); - // An array of indices to load, Indices which are explicitely requested are set to true, + // An array of indices to load, Indices which are explicitly requested are set to true, // and those determined by the preloading algorithm to prefetch are set to false. private final SparseBooleanArray mIndicesToLoad = new SparseBooleanArray(); @@ -676,7 +710,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } } - int count = 0; + int count; synchronized (mMetaData) { count = mMetaData.count; } @@ -791,9 +825,11 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // Initialize the worker thread mWorkerThread = new HandlerThread("RemoteViewsCache-loader"); mWorkerThread.start(); - mWorkerQueue = new Handler(mWorkerThread.getLooper()); - mMainQueue = new Handler(Looper.myLooper(), this); + mMainHandler = new Handler(Looper.myLooper(), this); + mServiceHandler = new RemoteServiceHandler(mWorkerThread.getLooper(), this, + context.getApplicationContext()); mAsyncViewLoadExecutor = useAsyncLoader ? new HandlerThreadExecutor(mWorkerThread) : null; + mCallback = callback; if (sCacheRemovalThread == null) { sCacheRemovalThread = new HandlerThread("RemoteViewsAdapter-cachePruner"); @@ -801,10 +837,6 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback sCacheRemovalQueue = new Handler(sCacheRemovalThread.getLooper()); } - // Initialize the cache and the service connection on startup - mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback); - mServiceConnection = new RemoteViewsAdapterServiceConnection(this); - RemoteViewsCacheKey key = new RemoteViewsCacheKey(new Intent.FilterComparison(mIntent), mAppWidgetId); @@ -819,7 +851,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } } } else { - mCache = new FixedSizeRemoteViewsCache(sDefaultCacheSize); + mCache = new FixedSizeRemoteViewsCache(DEFAULT_CACHE_SIZE); } if (!mDataReady) { requestBindService(); @@ -830,9 +862,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback @Override protected void finalize() throws Throwable { try { - if (mWorkerThread != null) { - mWorkerThread.quit(); - } + mServiceHandler.unbindNow(); + mWorkerThread.quit(); } finally { super.finalize(); } @@ -869,16 +900,13 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback sCachedRemoteViewsCaches.put(key, mCache); } - Runnable r = new Runnable() { - @Override - public void run() { - synchronized (sCachedRemoteViewsCaches) { - if (sCachedRemoteViewsCaches.containsKey(key)) { - sCachedRemoteViewsCaches.remove(key); - } - if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) { - sRemoteViewsCacheRemoveRunnables.remove(key); - } + Runnable r = () -> { + synchronized (sCachedRemoteViewsCaches) { + if (sCachedRemoteViewsCaches.containsKey(key)) { + sCachedRemoteViewsCaches.remove(key); + } + if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) { + sRemoteViewsCacheRemoveRunnables.remove(key); } } }; @@ -887,54 +915,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } } - private void loadNextIndexInBackground() { - mWorkerQueue.post(new Runnable() { - @Override - public void run() { - if (mServiceConnection.isConnected()) { - // Get the next index to load - int position = -1; - synchronized (mCache) { - position = mCache.getNextIndexToLoad(); - } - if (position > -1) { - // Load the item, and notify any existing RemoteViewsFrameLayouts - updateRemoteViews(position, true); - - // Queue up for the next one to load - loadNextIndexInBackground(); - } else { - // No more items to load, so queue unbind - enqueueDeferredUnbindServiceMessage(); - } - } - } - }); - } - - private void processException(String method, Exception e) { - Log.e("RemoteViewsAdapter", "Error in " + method + ": " + e.getMessage()); - - // If we encounter a crash when updating, we should reset the metadata & cache and trigger - // a notifyDataSetChanged to update the widget accordingly - final RemoteViewsMetaData metaData = mCache.getMetaData(); - synchronized (metaData) { - metaData.reset(); - } - synchronized (mCache) { - mCache.reset(); - } - mMainQueue.post(new Runnable() { - @Override - public void run() { - superNotifyDataSetChanged(); - } - }); - } - - private void updateTemporaryMetaData() { - IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); - + @WorkerThread + private void updateTemporaryMetaData(IRemoteViewsFactory factory) { try { // get the properties/first view (so that we can use it to // measure our dummy views) @@ -958,40 +940,54 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback tmpMetaData.count = count; tmpMetaData.loadingTemplate = loadingTemplate; } - } catch(RemoteException e) { - processException("updateMetaData", e); - } catch(RuntimeException e) { - processException("updateMetaData", e); + } catch (RemoteException | RuntimeException e) { + Log.e("RemoteViewsAdapter", "Error in updateMetaData: " + e.getMessage()); + + // If we encounter a crash when updating, we should reset the metadata & cache + // and trigger a notifyDataSetChanged to update the widget accordingly + synchronized (mCache.getMetaData()) { + mCache.getMetaData().reset(); + } + synchronized (mCache) { + mCache.reset(); + } + mMainHandler.sendEmptyMessage(MSG_MAIN_HANDLER_SUPER_NOTIFY_DATA_SET_CHANGED); } } - private void updateRemoteViews(final int position, boolean notifyWhenLoaded) { - IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); - + @WorkerThread + private void updateRemoteViews(IRemoteViewsFactory factory, int position, + boolean notifyWhenLoaded) { // Load the item information from the remote service - RemoteViews remoteViews = null; - long itemId = 0; + final RemoteViews remoteViews; + final long itemId; try { remoteViews = factory.getViewAt(position); itemId = factory.getItemId(position); - } catch (RemoteException e) { + + if (remoteViews == null) { + throw new RuntimeException("Null remoteViews"); + } + } catch (RemoteException | RuntimeException e) { Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage()); // Return early to prevent additional work in re-centering the view cache, and // swapping from the loading view return; - } catch (RuntimeException e) { - Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage()); - return; } - if (remoteViews == null) { - // If a null view was returned, we break early to prevent it from getting - // into our cache and causing problems later. The effect is that the child at this - // position will remain as a loading view until it is updated. - Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + " null RemoteViews " + - "returned from RemoteViewsFactory."); - return; + if (remoteViews.mApplication != null) { + // We keep track of last application info. This helps when all the remoteViews have + // same applicationInfo, which should be the case for a typical adapter. But if every + // view has different application info, there will not be any optimization. + if (mLastRemoteViewAppInfo != null + && remoteViews.hasSameAppInfo(mLastRemoteViewAppInfo)) { + // We should probably also update the remoteViews for nested ViewActions. + // Hopefully, RemoteViews in an adapter would be less complicated. + remoteViews.mApplication = mLastRemoteViewAppInfo; + } else { + mLastRemoteViewAppInfo = remoteViews.mApplication; + } } int layoutId = remoteViews.getLayoutId(); @@ -1004,21 +1000,15 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } synchronized (mCache) { if (viewTypeInRange) { - int[] visibleWindow = getVisibleWindow(mVisibleWindowLowerBound, - mVisibleWindowUpperBound, cacheCount); + int[] visibleWindow = getVisibleWindow(cacheCount); // Cache the RemoteViews we loaded mCache.insert(position, remoteViews, itemId, visibleWindow); - // Notify all the views that we have previously returned for this index that - // there is new data for it. - final RemoteViews rv = remoteViews; if (notifyWhenLoaded) { - mMainQueue.post(new Runnable() { - @Override - public void run() { - mRequestedViews.notifyOnRemoteViewsLoaded(position, rv); - } - }); + // Notify all the views that we have previously returned for this index that + // there is new data for it. + Message.obtain(mMainHandler, MSG_MAIN_HANDLER_REMOTE_VIEWS_LOADED, position, 0, + remoteViews).sendToTarget(); } } else { // We need to log an error here, as the the view type count specified by the @@ -1057,7 +1047,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } public int getItemViewType(int position) { - int typeId = 0; + final int typeId; synchronized (mCache) { if (mCache.containsMetaDataAt(position)) { typeId = mCache.getMetaDataAt(position).typeId; @@ -1088,14 +1078,13 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback synchronized (mCache) { RemoteViews rv = mCache.getRemoteViewsAt(position); boolean isInCache = (rv != null); - boolean isConnected = mServiceConnection.isConnected(); boolean hasNewItems = false; if (convertView != null && convertView instanceof RemoteViewsFrameLayout) { mRequestedViews.removeView((RemoteViewsFrameLayout) convertView); } - if (!isInCache && !isConnected) { + if (!isInCache) { // Requesting bind service will trigger a super.notifyDataSetChanged(), which will // in turn trigger another request to getView() requestBindService(); @@ -1115,7 +1104,9 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback if (isInCache) { // Apply the view synchronously if possible, to avoid flickering layout.onRemoteViewsLoaded(rv, mRemoteViewsOnClickHandler, false); - if (hasNewItems) loadNextIndexInBackground(); + if (hasNewItems) { + mServiceHandler.sendEmptyMessage(MSG_LOAD_NEXT_ITEM); + } } else { // If the views is not loaded, apply the loading view. If the loading view doesn't // exist, the layout will create a default view based on the firstView height. @@ -1125,7 +1116,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback false); mRequestedViews.add(position, layout); mCache.queueRequestedPositionToLoad(position); - loadNextIndexInBackground(); + mServiceHandler.sendEmptyMessage(MSG_LOAD_NEXT_ITEM); } return layout; } @@ -1149,69 +1140,12 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback return getCount() <= 0; } - private void onNotifyDataSetChanged() { - // Complete the actual notifyDataSetChanged() call initiated earlier - IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); - try { - factory.onDataSetChanged(); - } catch (RemoteException e) { - Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); - - // Return early to prevent from further being notified (since nothing has - // changed) - return; - } catch (RuntimeException e) { - Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); - return; - } - - // Flush the cache so that we can reload new items from the service - synchronized (mCache) { - mCache.reset(); - } - - // Re-request the new metadata (only after the notification to the factory) - updateTemporaryMetaData(); - int newCount; - int[] visibleWindow; - synchronized(mCache.getTemporaryMetaData()) { - newCount = mCache.getTemporaryMetaData().count; - visibleWindow = getVisibleWindow(mVisibleWindowLowerBound, - mVisibleWindowUpperBound, newCount); - } - - // Pre-load (our best guess of) the views which are currently visible in the AdapterView. - // This mitigates flashing and flickering of loading views when a widget notifies that - // its data has changed. - for (int i: visibleWindow) { - // Because temporary meta data is only ever modified from this thread (ie. - // mWorkerThread), it is safe to assume that count is a valid representation. - if (i < newCount) { - updateRemoteViews(i, false); - } - } - - // Propagate the notification back to the base adapter - mMainQueue.post(new Runnable() { - @Override - public void run() { - synchronized (mCache) { - mCache.commitTemporaryMetaData(); - } - - superNotifyDataSetChanged(); - enqueueDeferredUnbindServiceMessage(); - } - }); - - // Reset the notify flagflag - mNotifyDataSetChangedAfterOnServiceConnected = false; - } - /** * Returns a sorted array of all integers between lower and upper. */ - private int[] getVisibleWindow(int lower, int upper, int count) { + private int[] getVisibleWindow(int count) { + int lower = mVisibleWindowLowerBound; + int upper = mVisibleWindowUpperBound; // In the case that the window is invalid or uninitialized, return an empty window. if ((lower == 0 && upper == 0) || lower < 0 || upper < 0) { return new int[0]; @@ -1241,23 +1175,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } public void notifyDataSetChanged() { - // Dequeue any unbind messages - mMainQueue.removeMessages(sUnbindServiceMessageType); - - // If we are not connected, queue up the notifyDataSetChanged to be handled when we do - // connect - if (!mServiceConnection.isConnected()) { - mNotifyDataSetChangedAfterOnServiceConnected = true; - requestBindService(); - return; - } - - mWorkerQueue.post(new Runnable() { - @Override - public void run() { - onNotifyDataSetChanged(); - } - }); + mServiceHandler.removeMessages(MSG_UNBIND_SERVICE); + mServiceHandler.sendEmptyMessage(MSG_NOTIFY_DATA_SET_CHANGED); } void superNotifyDataSetChanged() { @@ -1266,35 +1185,38 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback @Override public boolean handleMessage(Message msg) { - boolean result = false; switch (msg.what) { - case sUnbindServiceMessageType: - if (mServiceConnection.isConnected()) { - mServiceConnection.unbind(mContext, mAppWidgetId, mIntent); + case MSG_MAIN_HANDLER_COMMIT_METADATA: { + mCache.commitTemporaryMetaData(); + return true; + } + case MSG_MAIN_HANDLER_SUPER_NOTIFY_DATA_SET_CHANGED: { + superNotifyDataSetChanged(); + return true; + } + case MSG_MAIN_HANDLER_REMOTE_ADAPTER_CONNECTED: { + if (mCallback != null) { + mCallback.onRemoteAdapterConnected(); + } + return true; + } + case MSG_MAIN_HANDLER_REMOTE_ADAPTER_DISCONNECTED: { + if (mCallback != null) { + mCallback.onRemoteAdapterDisconnected(); + } + return true; + } + case MSG_MAIN_HANDLER_REMOTE_VIEWS_LOADED: { + mRequestedViews.notifyOnRemoteViewsLoaded(msg.arg1, (RemoteViews) msg.obj); + return true; } - result = true; - break; - default: - break; } - return result; - } - - private void enqueueDeferredUnbindServiceMessage() { - // Remove any existing deferred-unbind messages - mMainQueue.removeMessages(sUnbindServiceMessageType); - mMainQueue.sendEmptyMessageDelayed(sUnbindServiceMessageType, sUnbindServiceDelay); + return false; } - private boolean requestBindService() { - // Try binding the service (which will start it if it's not already running) - if (!mServiceConnection.isConnected()) { - mServiceConnection.bind(mContext, mAppWidgetId, mIntent); - } - - // Remove any existing deferred-unbind messages - mMainQueue.removeMessages(sUnbindServiceMessageType); - return mServiceConnection.isConnected(); + private void requestBindService() { + mServiceHandler.removeMessages(MSG_UNBIND_SERVICE); + Message.obtain(mServiceHandler, MSG_REQUEST_BIND, mAppWidgetId, 0, mIntent).sendToTarget(); } private static class HandlerThreadExecutor implements Executor { @@ -1322,7 +1244,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback remoteViews = views; float density = context.getResources().getDisplayMetrics().density; - defaultHeight = Math.round(sDefaultLoadingViewHeight * density); + defaultHeight = Math.round(DEFAULT_LOADING_VIEW_HEIGHT * density); } public void loadFirstViewHeight( diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index 384e254e205f..d0ad27af0a92 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -20,8 +20,14 @@ 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; import android.text.Spannable; import android.text.TextUtils; @@ -34,23 +40,32 @@ import android.view.textclassifier.logging.SmartSelectionEventTracker; import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent; import android.widget.Editor.SelectionModifierCursorController; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; /** * Helper class for starting selection action mode * (synchronously without the TextClassifier, asynchronously with the TextClassifier). + * @hide */ @UiThread -final class SelectionActionModeHelper { +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public final class SelectionActionModeHelper { private static final String LOG_TAG = "SelectActionModeHelper"; + private static final boolean SMART_SELECT_ANIMATION_ENABLED = true; + private final Editor mEditor; private final TextView mTextView; private final TextClassificationHelper mTextClassificationHelper; @@ -60,14 +75,26 @@ final class SelectionActionModeHelper { private final SelectionTracker mSelectionTracker; + // TODO remove nullable marker once the switch gating the feature gets removed + @Nullable + private final SmartSelectSprite mSmartSelectSprite; + SelectionActionModeHelper(@NonNull Editor editor) { mEditor = Preconditions.checkNotNull(editor); mTextView = mEditor.getTextView(); mTextClassificationHelper = new TextClassificationHelper( + mTextView.getContext(), mTextView.getTextClassifier(), getText(mTextView), 0, 1, mTextView.getTextLocales()); mSelectionTracker = new SelectionTracker(mTextView); + + if (SMART_SELECT_ANIMATION_ENABLED) { + mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(), + mTextView::invalidate); + } else { + mSmartSelectSprite = null; + } } public void startActionModeAsync(boolean adjustSelection) { @@ -91,7 +118,9 @@ final class SelectionActionModeHelper { adjustSelection ? mTextClassificationHelper::suggestSelection : mTextClassificationHelper::classifyText, - this::startActionMode) + mSmartSelectSprite != null + ? this::startActionModeWithSmartSelectAnimation + : this::startActionMode) .execute(); } } @@ -141,10 +170,17 @@ final class SelectionActionModeHelper { } public void onDestroyActionMode() { + cancelSmartSelectAnimation(); mSelectionTracker.onSelectionDestroyed(); cancelAsyncTask(); } + public void onDraw(final Canvas canvas) { + if (mSmartSelectSprite != null) { + mSmartSelectSprite.draw(canvas); + } + } + private void cancelAsyncTask() { if (mTextClassificationAsyncTask != null) { mTextClassificationAsyncTask.cancel(true); @@ -188,7 +224,158 @@ final class SelectionActionModeHelper { mTextClassificationAsyncTask = null; } + private void startActionModeWithSmartSelectAnimation(@Nullable SelectionResult result) { + final Layout layout = mTextView.getLayout(); + + final Runnable onAnimationEndCallback = () -> startActionMode(result); + // TODO do not trigger the animation if the change included only non-printable characters + final boolean didSelectionChange = + result != null && (mTextView.getSelectionStart() != result.mStart + || mTextView.getSelectionEnd() != result.mEnd); + + if (!didSelectionChange) { + onAnimationEndCallback.run(); + return; + } + + final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles = + convertSelectionToRectangles(layout, result.mStart, result.mEnd); + + final PointF touchPoint = new PointF( + mEditor.getLastUpPositionX(), + mEditor.getLastUpPositionY()); + + final PointF animationStartPoint = + movePointInsideNearestRectangle(touchPoint, selectionRectangles, + SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle); + + mSmartSelectSprite.startAnimation( + animationStartPoint, + selectionRectangles, + onAnimationEndCallback); + } + + private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles( + final Layout layout, final int start, final int end) { + final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>(); + + final Layout.SelectionRectangleConsumer consumer = + (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList( + result, + new RectF(left, top, right, bottom), + SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle, + r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r, + textSelectionLayout) + ); + + layout.getSelection(start, end, consumer); + + result.sort(Comparator.comparing( + SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle, + SmartSelectSprite.RECTANGLE_COMPARATOR)); + + return result; + } + + // TODO: Move public pure functions out of this class and make it package-private. + /** + * Merges a {@link RectF} into an existing list of any objects which contain a rectangle. + * While merging, this method makes sure that: + * + * <ol> + * <li>No rectangle is redundant (contained within a bigger rectangle)</li> + * <li>Rectangles of the same height and vertical position that intersect get merged</li> + * </ol> + * + * @param list the list of rectangles (or other rectangle containers) to merge the new + * rectangle into + * @param candidate the {@link RectF} to merge into the list + * @param extractor a function that can extract a {@link RectF} from an element of the given + * list + * @param packer a function that can wrap the resulting {@link RectF} into an element that + * the list contains + * @hide + */ + @VisibleForTesting + public static <T> void mergeRectangleIntoList(final List<T> list, + final RectF candidate, final Function<T, RectF> extractor, + final Function<RectF, T> packer) { + if (candidate.isEmpty()) { + return; + } + + final int elementCount = list.size(); + for (int index = 0; index < elementCount; ++index) { + final RectF existingRectangle = extractor.apply(list.get(index)); + if (existingRectangle.contains(candidate)) { + return; + } + if (candidate.contains(existingRectangle)) { + existingRectangle.setEmpty(); + continue; + } + + final boolean rectanglesContinueEachOther = candidate.left == existingRectangle.right + || candidate.right == existingRectangle.left; + final boolean canMerge = candidate.top == existingRectangle.top + && candidate.bottom == existingRectangle.bottom + && (RectF.intersects(candidate, existingRectangle) + || rectanglesContinueEachOther); + + if (canMerge) { + candidate.union(existingRectangle); + existingRectangle.setEmpty(); + } + } + + for (int index = elementCount - 1; index >= 0; --index) { + final RectF rectangle = extractor.apply(list.get(index)); + if (rectangle.isEmpty()) { + list.remove(index); + } + } + + list.add(packer.apply(candidate)); + } + + + /** @hide */ + @VisibleForTesting + public static <T> PointF movePointInsideNearestRectangle(final PointF point, + final List<T> list, final Function<T, RectF> extractor) { + float bestX = -1; + float bestY = -1; + double bestDistance = Double.MAX_VALUE; + + final int elementCount = list.size(); + for (int index = 0; index < elementCount; ++index) { + final RectF rectangle = extractor.apply(list.get(index)); + final float candidateY = rectangle.centerY(); + final float candidateX; + + if (point.x > rectangle.right) { + candidateX = rectangle.right; + } else if (point.x < rectangle.left) { + candidateX = rectangle.left; + } else { + candidateX = point.x; + } + + final double candidateDistance = Math.pow(point.x - candidateX, 2) + + Math.pow(point.y - candidateY, 2); + + if (candidateDistance < bestDistance) { + bestX = candidateX; + bestY = candidateY; + bestDistance = candidateDistance; + } + } + + return new PointF(bestX, bestY); + } + private void invalidateActionMode(@Nullable SelectionResult result) { + cancelSmartSelectAnimation(); mTextClassification = result != null ? result.mClassification : null; final ActionMode actionMode = mEditor.getTextActionMode(); if (actionMode != null) { @@ -201,12 +388,19 @@ final class SelectionActionModeHelper { private void resetTextClassificationHelper() { mTextClassificationHelper.init( + mTextView.getContext(), mTextView.getTextClassifier(), getText(mTextView), mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextView.getTextLocales()); } + private void cancelSmartSelectAnimation() { + if (mSmartSelectSprite != null) { + mSmartSelectSprite.cancelAnimation(); + } + } + /** * Tracks and logs smart selection changes. * It is important to trigger this object's methods at the appropriate event so that it tracks @@ -397,7 +591,9 @@ 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()); @@ -479,8 +675,8 @@ final class SelectionActionModeHelper { // For the selection start index, avoid counting a partial word backwards. if (!mWordIterator.isBoundary(start) && !isWhitespace( - mWordIterator.preceding(start), - mWordIterator.following(start))) { + mWordIterator.preceding(start), + mWordIterator.following(start))) { // We counted a partial word. Remove it. wordIndices[0]--; } @@ -538,7 +734,7 @@ final class SelectionActionModeHelper { private static final class TextClassificationAsyncTask extends AsyncTask<Void, Void, SelectionResult> { - private final long mTimeOutDuration; + private final int mTimeOutDuration; private final Supplier<SelectionResult> mSelectionResultSupplier; private final Consumer<SelectionResult> mSelectionResultCallback; private final TextView mTextView; @@ -551,7 +747,7 @@ final class SelectionActionModeHelper { * @param selectionResultCallback receives the selection results. Runs on the UiThread */ TextClassificationAsyncTask( - @NonNull TextView textView, long timeOut, + @NonNull TextView textView, int timeOut, @NonNull Supplier<SelectionResult> selectionResultSupplier, @NonNull Consumer<SelectionResult> selectionResultCallback) { super(textView != null ? textView.getHandler() : null); @@ -597,6 +793,7 @@ final class SelectionActionModeHelper { private static final int TRIM_DELTA = 120; // characters + private Context mContext; private TextClassifier mTextClassifier; /** The original TextView text. **/ @@ -605,7 +802,10 @@ 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; @@ -626,21 +826,24 @@ 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 @@ -653,8 +856,16 @@ 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); @@ -668,7 +879,7 @@ final class SelectionActionModeHelper { * Maximum time (in milliseconds) to wait for a textclassifier result before timing out. */ // TODO: Consider making this a ViewConfiguration. - public long getTimeoutDuration() { + public int getTimeoutDuration() { if (mHot) { return 200; } else { @@ -684,20 +895,28 @@ 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; diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java index 855d1d288a65..9982732384da 100644 --- a/core/java/android/widget/SimpleMonthView.java +++ b/core/java/android/widget/SimpleMonthView.java @@ -517,6 +517,8 @@ class SimpleMonthView extends View { private int findClosestRow(@Nullable Rect previouslyFocusedRect) { if (previouslyFocusedRect == null) { return 3; + } else if (mDayHeight == 0) { + return 0; // There hasn't been a layout, so just choose the first row } else { int centerY = previouslyFocusedRect.centerY(); @@ -545,6 +547,8 @@ class SimpleMonthView extends View { private int findClosestColumn(@Nullable Rect previouslyFocusedRect) { if (previouslyFocusedRect == null) { return DAYS_IN_WEEK / 2; + } else if (mCellWidth == 0) { + return 0; // There hasn't been a layout, so we can just choose the first column } else { int centerX = previouslyFocusedRect.centerX() - mPaddingLeft; final int columnFromLeft = diff --git a/core/java/android/widget/SmartSelectSprite.java b/core/java/android/widget/SmartSelectSprite.java new file mode 100644 index 000000000000..a391c6ee8ec3 --- /dev/null +++ b/core/java/android/widget/SmartSelectSprite.java @@ -0,0 +1,696 @@ +/* + * 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.widget; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.ColorInt; +import android.annotation.FloatRange; +import android.annotation.IntDef; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.Shape; +import android.text.Layout; +import android.util.TypedValue; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * A utility class for creating and animating the Smart Select animation. + */ +final class SmartSelectSprite { + + private static final int EXPAND_DURATION = 300; + private static final int CORNER_DURATION = 150; + private static final float STROKE_WIDTH_DP = 1.5F; + + // GBLUE700 + @ColorInt + private static final int DEFAULT_STROKE_COLOR = 0xFF3367D6; + + private final Interpolator mExpandInterpolator; + private final Interpolator mCornerInterpolator; + private final float mStrokeWidth; + + private Animator mActiveAnimator = null; + private final Runnable mInvalidator; + @ColorInt + private final int mStrokeColor; + + static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator + .<RectF>comparingDouble(e -> e.bottom) + .thenComparingDouble(e -> e.left); + + private Drawable mExistingDrawable = null; + private RectangleList mExistingRectangleList = null; + + static final class RectangleWithTextSelectionLayout { + private final RectF mRectangle; + @Layout.TextSelectionLayout + private final int mTextSelectionLayout; + + RectangleWithTextSelectionLayout(RectF rectangle, int textSelectionLayout) { + mRectangle = Preconditions.checkNotNull(rectangle); + mTextSelectionLayout = textSelectionLayout; + } + + public RectF getRectangle() { + return mRectangle; + } + + @Layout.TextSelectionLayout + public int getTextSelectionLayout() { + return mTextSelectionLayout; + } + } + + /** + * A rounded rectangle with a configurable corner radius and the ability to expand outside of + * its bounding rectangle and clip against it. + */ + private static final class RoundedRectangleShape extends Shape { + + private static final String PROPERTY_ROUND_RATIO = "roundRatio"; + + /** + * The direction in which the rectangle will perform its expansion. A rectangle can expand + * from its left edge, its right edge or from the center (or, more precisely, the user's + * touch point). For example, in left-to-right text, a selection spanning two lines with the + * user's action being on the first line will have the top rectangle and expansion direction + * of CENTER, while the bottom one will have an expansion direction of RIGHT. + */ + @Retention(SOURCE) + @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT}) + private @interface ExpansionDirection { + int LEFT = -1; + int CENTER = 0; + int RIGHT = 1; + } + + private static @ExpansionDirection int invert(@ExpansionDirection int expansionDirection) { + return expansionDirection * -1; + } + + @Retention(SOURCE) + @IntDef({RectangleBorderType.FIT, RectangleBorderType.OVERSHOOT}) + private @interface RectangleBorderType { + /** A rectangle which, fully expanded, fits inside of its bounding rectangle. */ + int FIT = 0; + /** + * A rectangle which, when fully expanded, clips outside of its bounding rectangle so that + * its edges no longer appear rounded. + */ + int OVERSHOOT = 1; + } + + private final float mStrokeWidth; + private final RectF mBoundingRectangle; + private float mRoundRatio = 1.0f; + private final @ExpansionDirection int mExpansionDirection; + private final @RectangleBorderType int mRectangleBorderType; + + private final RectF mDrawRect = new RectF(); + private final RectF mClipRect = new RectF(); + private final Path mClipPath = new Path(); + + /** How offset the left edge of the rectangle is from the left side of the bounding box. */ + private float mLeftBoundary = 0; + /** How offset the right edge of the rectangle is from the left side of the bounding box. */ + private float mRightBoundary = 0; + + /** Whether the horizontal bounds are inverted (for RTL scenarios). */ + private final boolean mInverted; + + private final float mBoundingWidth; + + private RoundedRectangleShape( + final RectF boundingRectangle, + final @ExpansionDirection int expansionDirection, + final @RectangleBorderType int rectangleBorderType, + final boolean inverted, + final float strokeWidth) { + mBoundingRectangle = new RectF(boundingRectangle); + mBoundingWidth = boundingRectangle.width(); + mRectangleBorderType = rectangleBorderType; + mStrokeWidth = strokeWidth; + mInverted = inverted && expansionDirection != ExpansionDirection.CENTER; + + if (inverted) { + mExpansionDirection = invert(expansionDirection); + } else { + mExpansionDirection = expansionDirection; + } + + if (boundingRectangle.height() > boundingRectangle.width()) { + setRoundRatio(0.0f); + } else { + setRoundRatio(1.0f); + } + } + + /* + * In order to achieve the "rounded rectangle hits the wall" effect, the drawing needs to be + * done in two passes. In this context, the wall is the bounding rectangle and in the first + * pass we need to draw the rounded rectangle (expanded and with a corner radius as per + * object properties) clipped by the bounding box. If the rounded rectangle expands outside + * of the bounding box, one more pass needs to be done, as there will now be a hole in the + * rounded rectangle where it "flattened" against the bounding box. In order to fill just + * this hole, we need to draw the bounding box, but clip it with the rounded rectangle and + * this will connect the missing pieces. + */ + @Override + public void draw(Canvas canvas, Paint paint) { + if (mLeftBoundary == mRightBoundary) { + return; + } + + final float cornerRadius = getCornerRadius(); + final float adjustedCornerRadius = getAdjustedCornerRadius(); + + mDrawRect.set(mBoundingRectangle); + mDrawRect.left = mBoundingRectangle.left + mLeftBoundary; + mDrawRect.right = mBoundingRectangle.left + mRightBoundary; + + if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) { + mDrawRect.left -= cornerRadius / 2; + mDrawRect.right += cornerRadius / 2; + } else { + switch (mExpansionDirection) { + case ExpansionDirection.CENTER: + break; + case ExpansionDirection.LEFT: + mDrawRect.right += cornerRadius; + break; + case ExpansionDirection.RIGHT: + mDrawRect.left -= cornerRadius; + break; + } + } + + canvas.save(); + mClipRect.set(mBoundingRectangle); + mClipRect.inset(-mStrokeWidth / 2, -mStrokeWidth / 2); + canvas.clipRect(mClipRect); + canvas.drawRoundRect(mDrawRect, adjustedCornerRadius, adjustedCornerRadius, paint); + canvas.restore(); + + canvas.save(); + mClipPath.reset(); + mClipPath.addRoundRect( + mDrawRect, + adjustedCornerRadius, + adjustedCornerRadius, + Path.Direction.CW); + canvas.clipPath(mClipPath); + canvas.drawRect(mBoundingRectangle, paint); + canvas.restore(); + } + + void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) { + mRoundRatio = roundRatio; + } + + float getRoundRatio() { + return mRoundRatio; + } + + private void setStartBoundary(final float startBoundary) { + if (mInverted) { + mRightBoundary = mBoundingWidth - startBoundary; + } else { + mLeftBoundary = startBoundary; + } + } + + private void setEndBoundary(final float endBoundary) { + if (mInverted) { + mLeftBoundary = mBoundingWidth - endBoundary; + } else { + mRightBoundary = endBoundary; + } + } + + private float getCornerRadius() { + return Math.min(mBoundingRectangle.width(), mBoundingRectangle.height()); + } + + private float getAdjustedCornerRadius() { + return (getCornerRadius() * mRoundRatio); + } + + private float getBoundingWidth() { + if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) { + return (int) (mBoundingRectangle.width() + getCornerRadius()); + } else { + return mBoundingRectangle.width(); + } + } + + } + + /** + * A collection of {@link RoundedRectangleShape}s that abstracts them to a single shape whose + * collective left and right boundary can be manipulated. + */ + private static final class RectangleList extends Shape { + + @Retention(SOURCE) + @IntDef({DisplayType.RECTANGLES, DisplayType.POLYGON}) + private @interface DisplayType { + int RECTANGLES = 0; + int POLYGON = 1; + } + + private static final String PROPERTY_RIGHT_BOUNDARY = "rightBoundary"; + private static final String PROPERTY_LEFT_BOUNDARY = "leftBoundary"; + + private final List<RoundedRectangleShape> mRectangles; + private final List<RoundedRectangleShape> mReversedRectangles; + + private final Path mOutlinePolygonPath; + private @DisplayType int mDisplayType = DisplayType.RECTANGLES; + + private RectangleList(final List<RoundedRectangleShape> rectangles) { + mRectangles = new ArrayList<>(rectangles); + mReversedRectangles = new ArrayList<>(rectangles); + Collections.reverse(mReversedRectangles); + mOutlinePolygonPath = generateOutlinePolygonPath(rectangles); + } + + private void setLeftBoundary(final float leftBoundary) { + float boundarySoFar = getTotalWidth(); + for (RoundedRectangleShape rectangle : mReversedRectangles) { + final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth(); + if (leftBoundary < rectangleLeftBoundary) { + rectangle.setStartBoundary(0); + } else if (leftBoundary > boundarySoFar) { + rectangle.setStartBoundary(rectangle.getBoundingWidth()); + } else { + rectangle.setStartBoundary( + rectangle.getBoundingWidth() - boundarySoFar + leftBoundary); + } + + boundarySoFar = rectangleLeftBoundary; + } + } + + private void setRightBoundary(final float rightBoundary) { + float boundarySoFar = 0; + for (RoundedRectangleShape rectangle : mRectangles) { + final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar; + if (rectangleRightBoundary < rightBoundary) { + rectangle.setEndBoundary(rectangle.getBoundingWidth()); + } else if (boundarySoFar > rightBoundary) { + rectangle.setEndBoundary(0); + } else { + rectangle.setEndBoundary(rightBoundary - boundarySoFar); + } + + boundarySoFar = rectangleRightBoundary; + } + } + + void setDisplayType(@DisplayType int displayType) { + mDisplayType = displayType; + } + + private int getTotalWidth() { + int sum = 0; + for (RoundedRectangleShape rectangle : mRectangles) { + sum += rectangle.getBoundingWidth(); + } + return sum; + } + + @Override + public void draw(Canvas canvas, Paint paint) { + if (mDisplayType == DisplayType.POLYGON) { + drawPolygon(canvas, paint); + } else { + drawRectangles(canvas, paint); + } + } + + private void drawRectangles(final Canvas canvas, final Paint paint) { + for (RoundedRectangleShape rectangle : mRectangles) { + rectangle.draw(canvas, paint); + } + } + + private void drawPolygon(final Canvas canvas, final Paint paint) { + canvas.drawPath(mOutlinePolygonPath, paint); + } + + private static Path generateOutlinePolygonPath( + final List<RoundedRectangleShape> rectangles) { + final Path path = new Path(); + for (final RoundedRectangleShape shape : rectangles) { + final Path rectanglePath = new Path(); + rectanglePath.addRect(shape.mBoundingRectangle, Path.Direction.CW); + path.op(rectanglePath, Path.Op.UNION); + } + return path; + } + + } + + /** + * @param context the {@link Context} in which the animation will run + * @param invalidator a {@link Runnable} which will be called every time the animation updates, + * indicating that the view drawing the animation should invalidate itself + */ + SmartSelectSprite(final Context context, final Runnable invalidator) { + mExpandInterpolator = AnimationUtils.loadInterpolator( + context, + android.R.interpolator.fast_out_slow_in); + mCornerInterpolator = AnimationUtils.loadInterpolator( + context, + android.R.interpolator.fast_out_linear_in); + mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP); + mStrokeColor = getStrokeColor(context); + mInvalidator = Preconditions.checkNotNull(invalidator); + } + + /** + * Performs the Smart Select animation on the view bound to this SmartSelectSprite. + * + * @param start The point from which the animation will start. Must be inside + * destinationRectangles. + * @param destinationRectangles The rectangles which the animation will fill out by its + * "selection" and finally join them into a single polygon. In + * order to get the correct visual behavior, these rectangles + * should be sorted according to {@link #RECTANGLE_COMPARATOR}. + * @param onAnimationEnd the callback which will be invoked once the whole animation + * completes + * @throws IllegalArgumentException if the given start point is not in any of the + * destinationRectangles + * @see #cancelAnimation() + */ + // TODO nullability checks on parameters + public void startAnimation( + final PointF start, + final List<RectangleWithTextSelectionLayout> destinationRectangles, + final Runnable onAnimationEnd) { + cancelAnimation(); + + final ValueAnimator.AnimatorUpdateListener updateListener = + valueAnimator -> mInvalidator.run(); + + final int rectangleCount = destinationRectangles.size(); + + final List<RoundedRectangleShape> shapes = new ArrayList<>(rectangleCount); + final List<Animator> cornerAnimators = new ArrayList<>(rectangleCount); + + RectangleWithTextSelectionLayout centerRectangle = null; + + int startingOffset = 0; + int startingRectangleIndex = 0; + for (int index = 0; index < rectangleCount; ++index) { + final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = + destinationRectangles.get(index); + final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); + if (contains(rectangle, start)) { + centerRectangle = rectangleWithTextSelectionLayout; + break; + } + startingOffset += rectangle.width(); + ++startingRectangleIndex; + } + + if (centerRectangle == null) { + throw new IllegalArgumentException("Center point is not inside any of the rectangles!"); + } + + startingOffset += start.x - centerRectangle.getRectangle().left; + + final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections = + generateDirections(centerRectangle, destinationRectangles); + + final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes = + generateBorderTypes(rectangleCount); + + for (int index = 0; index < rectangleCount; ++index) { + final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = + destinationRectangles.get(index); + final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); + final RoundedRectangleShape shape = new RoundedRectangleShape( + rectangle, + expansionDirections[index], + rectangleBorderTypes[index], + rectangleWithTextSelectionLayout.getTextSelectionLayout() + == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, + mStrokeWidth); + cornerAnimators.add(createCornerAnimator(shape, updateListener)); + shapes.add(shape); + } + + final RectangleList rectangleList = new RectangleList(shapes); + final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList); + + final float startingOffsetLeft; + final float startingOffsetRight; + + final RoundedRectangleShape startingRectangleShape = shapes.get(startingRectangleIndex); + final float cornerRadius = startingRectangleShape.getCornerRadius(); + if (startingRectangleShape.mRectangleBorderType + == RoundedRectangleShape.RectangleBorderType.FIT) { + switch (startingRectangleShape.mExpansionDirection) { + case RoundedRectangleShape.ExpansionDirection.LEFT: + startingOffsetLeft = startingOffsetRight = startingOffset - cornerRadius / 2; + break; + case RoundedRectangleShape.ExpansionDirection.RIGHT: + startingOffsetLeft = startingOffsetRight = startingOffset + cornerRadius / 2; + break; + case RoundedRectangleShape.ExpansionDirection.CENTER: // fall through + default: + startingOffsetLeft = startingOffset - cornerRadius / 2; + startingOffsetRight = startingOffset + cornerRadius / 2; + break; + } + } else { + startingOffsetLeft = startingOffsetRight = startingOffset; + } + + final Paint paint = shapeDrawable.getPaint(); + paint.setColor(mStrokeColor); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(mStrokeWidth); + + mExistingRectangleList = rectangleList; + mExistingDrawable = shapeDrawable; + + mActiveAnimator = createAnimator(rectangleList, startingOffsetLeft, startingOffsetRight, + cornerAnimators, updateListener, + onAnimationEnd); + mActiveAnimator.start(); + } + + private Animator createAnimator( + final RectangleList rectangleList, + final float startingOffsetLeft, + final float startingOffsetRight, + final List<Animator> cornerAnimators, + final ValueAnimator.AnimatorUpdateListener updateListener, + final Runnable onAnimationEnd) { + final ObjectAnimator rightBoundaryAnimator = ObjectAnimator.ofFloat( + rectangleList, + RectangleList.PROPERTY_RIGHT_BOUNDARY, + startingOffsetRight, + rectangleList.getTotalWidth()); + + final ObjectAnimator leftBoundaryAnimator = ObjectAnimator.ofFloat( + rectangleList, + RectangleList.PROPERTY_LEFT_BOUNDARY, + startingOffsetLeft, + 0); + + rightBoundaryAnimator.setDuration(EXPAND_DURATION); + leftBoundaryAnimator.setDuration(EXPAND_DURATION); + + rightBoundaryAnimator.addUpdateListener(updateListener); + leftBoundaryAnimator.addUpdateListener(updateListener); + + rightBoundaryAnimator.setInterpolator(mExpandInterpolator); + leftBoundaryAnimator.setInterpolator(mExpandInterpolator); + + final AnimatorSet cornerAnimator = new AnimatorSet(); + cornerAnimator.playTogether(cornerAnimators); + + final AnimatorSet boundaryAnimator = new AnimatorSet(); + boundaryAnimator.playTogether(leftBoundaryAnimator, rightBoundaryAnimator); + + final AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playSequentially(boundaryAnimator, cornerAnimator); + + setUpAnimatorListener(animatorSet, onAnimationEnd); + + return animatorSet; + } + + private void setUpAnimatorListener(final Animator animator, final Runnable onAnimationEnd) { + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationEnd(Animator animator) { + mExistingRectangleList.setDisplayType(RectangleList.DisplayType.POLYGON); + mInvalidator.run(); + + onAnimationEnd.run(); + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + }); + } + + private ObjectAnimator createCornerAnimator( + final RoundedRectangleShape shape, + final ValueAnimator.AnimatorUpdateListener listener) { + final ObjectAnimator animator = ObjectAnimator.ofFloat( + shape, + RoundedRectangleShape.PROPERTY_ROUND_RATIO, + shape.getRoundRatio(), 0.0F); + animator.setDuration(CORNER_DURATION); + animator.addUpdateListener(listener); + animator.setInterpolator(mCornerInterpolator); + return animator; + } + + private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections( + final RectangleWithTextSelectionLayout centerRectangle, + final List<RectangleWithTextSelectionLayout> rectangles) { + final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()]; + + final int centerRectangleIndex = rectangles.indexOf(centerRectangle); + + for (int i = 0; i < centerRectangleIndex - 1; ++i) { + result[i] = RoundedRectangleShape.ExpansionDirection.LEFT; + } + + if (rectangles.size() == 1) { + result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER; + } else if (centerRectangleIndex == 0) { + result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.LEFT; + } else if (centerRectangleIndex == rectangles.size() - 1) { + result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.RIGHT; + } else { + result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER; + } + + for (int i = centerRectangleIndex + 1; i < result.length; ++i) { + result[i] = RoundedRectangleShape.ExpansionDirection.RIGHT; + } + + return result; + } + + private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes( + final int numberOfRectangles) { + final @RoundedRectangleShape.RectangleBorderType int[] result = new int[numberOfRectangles]; + + for (int i = 1; i < result.length - 1; ++i) { + result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT; + } + + result[0] = RoundedRectangleShape.RectangleBorderType.FIT; + result[result.length - 1] = RoundedRectangleShape.RectangleBorderType.FIT; + return result; + } + + private static float dpToPixel(final Context context, final float dp) { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp, + context.getResources().getDisplayMetrics()); + } + + @ColorInt + private static int getStrokeColor(final Context context) { + final TypedValue typedValue = new TypedValue(); + final TypedArray array = context.obtainStyledAttributes(typedValue.data, new int[]{ + android.R.attr.colorControlActivated}); + final int result = array.getColor(0, DEFAULT_STROKE_COLOR); + array.recycle(); + return result; + } + + /** + * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on + * the right boundary of the rectangle. + * + * @param rectangle the rectangle inside which the point should be to be considered "contained" + * @param point the point which will be tested + * @return whether the point is inside the rectangle (or on it's right boundary) + */ + private static boolean contains(final RectF rectangle, final PointF point) { + final float x = point.x; + final float y = point.y; + return x >= rectangle.left && x <= rectangle.right && y >= rectangle.top + && y <= rectangle.bottom; + } + + private void removeExistingDrawables() { + mExistingDrawable = null; + mExistingRectangleList = null; + mInvalidator.run(); + } + + /** + * Cancels any active Smart Select animation that might be in progress. + */ + public void cancelAnimation() { + if (mActiveAnimator != null) { + mActiveAnimator.cancel(); + mActiveAnimator = null; + removeExistingDrawables(); + } + } + + public void draw(Canvas canvas) { + if (mExistingDrawable != null) { + mExistingDrawable.draw(canvas); + } + } + +} diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index 40253a187177..604575fae463 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -33,6 +33,7 @@ import android.graphics.Rect; import android.graphics.Region.Op; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.os.Build.VERSION_CODES; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; @@ -111,6 +112,7 @@ public class Switch extends CompoundButton { private CharSequence mTextOn; private CharSequence mTextOff; private boolean mShowText; + private boolean mUseFallbackLineSpacing; private int mTouchMode; private int mTouchSlop; @@ -246,6 +248,8 @@ public class Switch extends CompoundButton { com.android.internal.R.styleable.Switch_switchPadding, 0); mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false); + mUseFallbackLineSpacing = context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.P; + ColorStateList thumbTintList = a.getColorStateList( com.android.internal.R.styleable.Switch_thumbTint); if (thumbTintList != null) { @@ -894,8 +898,9 @@ public class Switch extends CompoundButton { int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0, transformed.length(), mTextPaint, getTextDirectionHeuristic())); - return new StaticLayout(transformed, mTextPaint, width, - Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true); + return StaticLayout.Builder.obtain(transformed, 0, transformed.length(), mTextPaint, width) + .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing) + .build(); } /** diff --git a/core/java/android/widget/TextClock.java b/core/java/android/widget/TextClock.java index 127904039506..7156300e6e47 100644 --- a/core/java/android/widget/TextClock.java +++ b/core/java/android/widget/TextClock.java @@ -570,11 +570,12 @@ public class TextClock extends TextView { mFormatChangeObserver = new FormatChangeObserver(getHandler()); } final ContentResolver resolver = getContext().getContentResolver(); + Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24); if (mShowCurrentUserTime) { - resolver.registerContentObserver(Settings.System.CONTENT_URI, true, + resolver.registerContentObserver(uri, true, mFormatChangeObserver, UserHandle.USER_ALL); } else { - resolver.registerContentObserver(Settings.System.CONTENT_URI, true, + resolver.registerContentObserver(uri, true, mFormatChangeObserver); } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 9826fa0b94a1..f8083babef29 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -22,6 +22,7 @@ import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_C import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; import android.R; +import android.annotation.CheckResult; import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.FloatRange; @@ -76,6 +77,7 @@ import android.text.InputFilter; import android.text.InputType; import android.text.Layout; import android.text.ParcelableSpan; +import android.text.PremeasuredText; import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; @@ -118,6 +120,7 @@ import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.IntArray; import android.util.Log; +import android.util.SparseIntArray; import android.util.TypedValue; import android.view.AccessibilityIterators.TextSegmentIterator; import android.view.ActionMode; @@ -313,6 +316,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private static final int SERIF = 2; private static final int MONOSPACE = 3; + // Enum for the "ellipsize" XML parameter. + private static final int ELLIPSIZE_NOT_SET = -1; + private static final int ELLIPSIZE_NONE = 0; + private static final int ELLIPSIZE_START = 1; + private static final int ELLIPSIZE_MIDDLE = 2; + private static final int ELLIPSIZE_END = 3; + private static final int ELLIPSIZE_MARQUEE = 4; + // Bitfield for the "numeric" XML parameter. // TODO: How can we get this from the XML instead of hardcoding it here? private static final int SIGNED = 2; @@ -640,6 +651,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private boolean mListenerChanged = false; // True if internationalized input should be used for numbers and date and time. private final boolean mUseInternationalizedInput; + // True if fallback fonts that end up getting used should be allowed to affect line spacing. + /* package */ final boolean mUseFallbackLineSpacing; @ViewDebug.ExportedProperty(category = "text") private int mGravity = Gravity.TOP | Gravity.START; @@ -846,22 +859,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTransformation = null; - int textColorHighlight = 0; - ColorStateList textColor = null; - ColorStateList textColorHint = null; - ColorStateList textColorLink = null; - int textSize = 15; - String fontFamily = null; - Typeface fontTypeface = null; - boolean fontFamilyExplicit = false; - int typefaceIndex = -1; - int styleIndex = -1; - boolean allCaps = false; - int shadowcolor = 0; - float dx = 0, dy = 0, r = 0; - boolean elegant = false; - float letterSpacing = 0; - String fontFeatureSettings = null; + final TextAppearanceAttributes attributes = new TextAppearanceAttributes(); + attributes.mTextColor = ColorStateList.valueOf(0xFF000000); + attributes.mTextSize = 15; mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; @@ -885,87 +885,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ap, com.android.internal.R.styleable.TextAppearance); } if (appearance != null) { - int n = appearance.getIndexCount(); - for (int i = 0; i < n; i++) { - int attr = appearance.getIndex(i); - - switch (attr) { - case com.android.internal.R.styleable.TextAppearance_textColorHighlight: - textColorHighlight = appearance.getColor(attr, textColorHighlight); - break; - - case com.android.internal.R.styleable.TextAppearance_textColor: - textColor = appearance.getColorStateList(attr); - break; - - case com.android.internal.R.styleable.TextAppearance_textColorHint: - textColorHint = appearance.getColorStateList(attr); - break; - - case com.android.internal.R.styleable.TextAppearance_textColorLink: - textColorLink = appearance.getColorStateList(attr); - break; - - case com.android.internal.R.styleable.TextAppearance_textSize: - textSize = appearance.getDimensionPixelSize(attr, textSize); - break; - - case com.android.internal.R.styleable.TextAppearance_typeface: - typefaceIndex = appearance.getInt(attr, -1); - break; - - case com.android.internal.R.styleable.TextAppearance_fontFamily: - if (!context.isRestricted() && context.canLoadUnsafeResources()) { - try { - fontTypeface = appearance.getFont(attr); - } catch (UnsupportedOperationException - | Resources.NotFoundException e) { - // Expected if it is not a font resource. - } - } - if (fontTypeface == null) { - fontFamily = appearance.getString(attr); - } - break; - - case com.android.internal.R.styleable.TextAppearance_textStyle: - styleIndex = appearance.getInt(attr, -1); - break; - - case com.android.internal.R.styleable.TextAppearance_textAllCaps: - allCaps = appearance.getBoolean(attr, false); - break; - - case com.android.internal.R.styleable.TextAppearance_shadowColor: - shadowcolor = appearance.getInt(attr, 0); - break; - - case com.android.internal.R.styleable.TextAppearance_shadowDx: - dx = appearance.getFloat(attr, 0); - break; - - case com.android.internal.R.styleable.TextAppearance_shadowDy: - dy = appearance.getFloat(attr, 0); - break; - - case com.android.internal.R.styleable.TextAppearance_shadowRadius: - r = appearance.getFloat(attr, 0); - break; - - case com.android.internal.R.styleable.TextAppearance_elegantTextHeight: - elegant = appearance.getBoolean(attr, false); - break; - - case com.android.internal.R.styleable.TextAppearance_letterSpacing: - letterSpacing = appearance.getFloat(attr, 0); - break; - - case com.android.internal.R.styleable.TextAppearance_fontFeatureSettings: - fontFeatureSettings = appearance.getString(attr); - break; - } - } - + readTextAppearance(context, appearance, attributes, false /* styleArray */); + attributes.mFontFamilyExplicit = false; appearance.recycle(); } @@ -983,7 +904,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ColorStateList drawableTint = null; PorterDuff.Mode drawableTintMode = null; int drawablePadding = 0; - int ellipsize = -1; + int ellipsize = ELLIPSIZE_NOT_SET; boolean singleLine = false; int maxlength = -1; CharSequence text = ""; @@ -996,6 +917,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener a = theme.obtainStyledAttributes( attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes); + readTextAppearance(context, a, attributes, true /* styleArray */); + int n = a.getIndexCount(); boolean fromResourceId = false; @@ -1186,69 +1109,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mFreezesText = a.getBoolean(attr, false); break; - case com.android.internal.R.styleable.TextView_shadowColor: - shadowcolor = a.getInt(attr, 0); - break; - - case com.android.internal.R.styleable.TextView_shadowDx: - dx = a.getFloat(attr, 0); - break; - - case com.android.internal.R.styleable.TextView_shadowDy: - dy = a.getFloat(attr, 0); - break; - - case com.android.internal.R.styleable.TextView_shadowRadius: - r = a.getFloat(attr, 0); - break; - case com.android.internal.R.styleable.TextView_enabled: setEnabled(a.getBoolean(attr, isEnabled())); break; - case com.android.internal.R.styleable.TextView_textColorHighlight: - textColorHighlight = a.getColor(attr, textColorHighlight); - break; - - case com.android.internal.R.styleable.TextView_textColor: - textColor = a.getColorStateList(attr); - break; - - case com.android.internal.R.styleable.TextView_textColorHint: - textColorHint = a.getColorStateList(attr); - break; - - case com.android.internal.R.styleable.TextView_textColorLink: - textColorLink = a.getColorStateList(attr); - break; - - case com.android.internal.R.styleable.TextView_textSize: - textSize = a.getDimensionPixelSize(attr, textSize); - break; - - case com.android.internal.R.styleable.TextView_typeface: - typefaceIndex = a.getInt(attr, typefaceIndex); - break; - - case com.android.internal.R.styleable.TextView_textStyle: - styleIndex = a.getInt(attr, styleIndex); - break; - - case com.android.internal.R.styleable.TextView_fontFamily: - if (!context.isRestricted() && context.canLoadUnsafeResources()) { - try { - fontTypeface = a.getFont(attr); - } catch (UnsupportedOperationException | Resources.NotFoundException e) { - // Expected if it is not a resource reference or if it is a reference to - // another resource type. - } - } - if (fontTypeface == null) { - fontFamily = a.getString(attr); - } - fontFamilyExplicit = true; - break; - case com.android.internal.R.styleable.TextView_password: password = a.getBoolean(attr, password); break; @@ -1336,22 +1200,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setTextIsSelectable(a.getBoolean(attr, false)); break; - case com.android.internal.R.styleable.TextView_textAllCaps: - allCaps = a.getBoolean(attr, false); - break; - - case com.android.internal.R.styleable.TextView_elegantTextHeight: - elegant = a.getBoolean(attr, false); - break; - - case com.android.internal.R.styleable.TextView_letterSpacing: - letterSpacing = a.getFloat(attr, 0); - break; - - case com.android.internal.R.styleable.TextView_fontFeatureSettings: - fontFeatureSettings = a.getString(attr); - break; - case com.android.internal.R.styleable.TextView_breakStrategy: mBreakStrategy = a.getInt(attr, Layout.BREAK_STRATEGY_SIMPLE); break; @@ -1407,8 +1255,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final boolean numberPasswordInputType = variation == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD); - mUseInternationalizedInput = - context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.O; + final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; + mUseInternationalizedInput = targetSdkVersion >= VERSION_CODES.O; + mUseFallbackLineSpacing = targetSdkVersion >= VERSION_CODES.P; if (inputMethod != null) { Class<?> c; @@ -1557,21 +1406,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setInputTypeSingleLine(singleLine); applySingleLine(singleLine, singleLine, singleLine); - if (singleLine && getKeyListener() == null && ellipsize < 0) { - ellipsize = 3; // END + if (singleLine && getKeyListener() == null && ellipsize == ELLIPSIZE_NOT_SET) { + ellipsize = ELLIPSIZE_END; } switch (ellipsize) { - case 1: + case ELLIPSIZE_START: setEllipsize(TextUtils.TruncateAt.START); break; - case 2: + case ELLIPSIZE_MIDDLE: setEllipsize(TextUtils.TruncateAt.MIDDLE); break; - case 3: + case ELLIPSIZE_END: setEllipsize(TextUtils.TruncateAt.END); break; - case 4: + case ELLIPSIZE_MARQUEE: if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) { setHorizontalFadingEdgeEnabled(true); mMarqueeFadeMode = MARQUEE_FADE_NORMAL; @@ -1583,38 +1432,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; } - setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000)); - setHintTextColor(textColorHint); - setLinkTextColor(textColorLink); - if (textColorHighlight != 0) { - setHighlightColor(textColorHighlight); - } - setRawTextSize(textSize, true /* shouldRequestLayout */); - setElegantTextHeight(elegant); - setLetterSpacing(letterSpacing); - setFontFeatureSettings(fontFeatureSettings); - - if (allCaps) { - setTransformationMethod(new AllCapsTransformationMethod(getContext())); - } - - if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) { - setTransformationMethod(PasswordTransformationMethod.getInstance()); - typefaceIndex = MONOSPACE; - } else if (mEditor != null + final boolean isPassword = password || passwordInputType || webPasswordInputType + || numberPasswordInputType; + final boolean isMonospaceEnforced = isPassword || (mEditor != null && (mEditor.mInputType - & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION)) - == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { - typefaceIndex = MONOSPACE; + & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION)) + == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)); + if (isMonospaceEnforced) { + attributes.mTypefaceIndex = MONOSPACE; } - if (typefaceIndex != -1 && !fontFamilyExplicit) { - fontFamily = null; - } - setTypefaceFromAttrs(fontTypeface, fontFamily, typefaceIndex, styleIndex); + applyTextAppearance(attributes); - if (shadowcolor != 0) { - setShadowLayer(r, dx, dy, shadowcolor); + if (isPassword) { + setTransformationMethod(PasswordTransformationMethod.getInstance()); } if (maxlength >= 0) { @@ -3388,79 +3219,244 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Deprecated public void setTextAppearance(Context context, @StyleRes int resId) { final TypedArray ta = context.obtainStyledAttributes(resId, R.styleable.TextAppearance); + final TextAppearanceAttributes attributes = new TextAppearanceAttributes(); + readTextAppearance(context, ta, attributes, false /* styleArray */); + ta.recycle(); + applyTextAppearance(attributes); + } + + /** + * Set of attributes that can be defined in a Text Appearance. This is used to simplify the code + * that reads these attributes in the constructor and in {@link #setTextAppearance}. + */ + private static class TextAppearanceAttributes { + int mTextColorHighlight = 0; + ColorStateList mTextColor = null; + ColorStateList mTextColorHint = null; + ColorStateList mTextColorLink = null; + int mTextSize = 0; + String mFontFamily = null; + Typeface mFontTypeface = null; + boolean mFontFamilyExplicit = false; + int mTypefaceIndex = -1; + int mStyleIndex = -1; + boolean mAllCaps = false; + int mShadowColor = 0; + float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0; + boolean mHasElegant = false; + boolean mElegant = false; + boolean mHasLetterSpacing = false; + float mLetterSpacing = 0; + String mFontFeatureSettings = null; - final int textColorHighlight = ta.getColor( - R.styleable.TextAppearance_textColorHighlight, 0); - if (textColorHighlight != 0) { - setHighlightColor(textColorHighlight); + @Override + public String toString() { + return "TextAppearanceAttributes {\n" + + " mTextColorHighlight:" + mTextColorHighlight + "\n" + + " mTextColor:" + mTextColor + "\n" + + " mTextColorHint:" + mTextColorHint + "\n" + + " mTextColorLink:" + mTextColorLink + "\n" + + " mTextSize:" + mTextSize + "\n" + + " mFontFamily:" + mFontFamily + "\n" + + " mFontTypeface:" + mFontTypeface + "\n" + + " mFontFamilyExplicit:" + mFontFamilyExplicit + "\n" + + " mTypefaceIndex:" + mTypefaceIndex + "\n" + + " mStyleIndex:" + mStyleIndex + "\n" + + " mAllCaps:" + mAllCaps + "\n" + + " mShadowColor:" + mShadowColor + "\n" + + " mShadowDx:" + mShadowDx + "\n" + + " mShadowDy:" + mShadowDy + "\n" + + " mShadowRadius:" + mShadowRadius + "\n" + + " mHasElegant:" + mHasElegant + "\n" + + " mElegant:" + mElegant + "\n" + + " mHasLetterSpacing:" + mHasLetterSpacing + "\n" + + " mLetterSpacing:" + mLetterSpacing + "\n" + + " mFontFeatureSettings:" + mFontFeatureSettings + "\n" + + "}"; + } + } + + // Maps styleable attributes that exist both in TextView style and TextAppearance. + private static final SparseIntArray sAppearanceValues = new SparseIntArray(); + static { + sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHighlight, + com.android.internal.R.styleable.TextAppearance_textColorHighlight); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColor, + com.android.internal.R.styleable.TextAppearance_textColor); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHint, + com.android.internal.R.styleable.TextAppearance_textColorHint); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorLink, + com.android.internal.R.styleable.TextAppearance_textColorLink); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_textSize, + com.android.internal.R.styleable.TextAppearance_textSize); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_typeface, + com.android.internal.R.styleable.TextAppearance_typeface); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFamily, + com.android.internal.R.styleable.TextAppearance_fontFamily); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_textStyle, + com.android.internal.R.styleable.TextAppearance_textStyle); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_textAllCaps, + com.android.internal.R.styleable.TextAppearance_textAllCaps); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowColor, + com.android.internal.R.styleable.TextAppearance_shadowColor); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowDx, + com.android.internal.R.styleable.TextAppearance_shadowDx); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowDy, + com.android.internal.R.styleable.TextAppearance_shadowDy); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowRadius, + com.android.internal.R.styleable.TextAppearance_shadowRadius); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_elegantTextHeight, + com.android.internal.R.styleable.TextAppearance_elegantTextHeight); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_letterSpacing, + com.android.internal.R.styleable.TextAppearance_letterSpacing); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFeatureSettings, + com.android.internal.R.styleable.TextAppearance_fontFeatureSettings); + } + + /** + * Read the Text Appearance attributes from a given TypedArray and set its values to the given + * set. If the TypedArray contains a value that was already set in the given attributes, that + * will be overriden. + * + * @param context The Context to be used + * @param appearance The TypedArray to read properties from + * @param attributes the TextAppearanceAttributes to fill in + * @param styleArray Whether the given TypedArray is a style or a TextAppearance. This defines + * what attribute indexes will be used to read the properties. + */ + private void readTextAppearance(Context context, TypedArray appearance, + TextAppearanceAttributes attributes, boolean styleArray) { + final int n = appearance.getIndexCount(); + for (int i = 0; i < n; i++) { + final int attr = appearance.getIndex(i); + int index = attr; + // Translate style array index ids to TextAppearance ids. + if (styleArray) { + index = sAppearanceValues.get(attr, -1); + if (index == -1) { + // This value is not part of a Text Appearance and should be ignored. + continue; + } + } + switch (index) { + case com.android.internal.R.styleable.TextAppearance_textColorHighlight: + attributes.mTextColorHighlight = + appearance.getColor(attr, attributes.mTextColorHighlight); + break; + case com.android.internal.R.styleable.TextAppearance_textColor: + attributes.mTextColor = appearance.getColorStateList(attr); + break; + case com.android.internal.R.styleable.TextAppearance_textColorHint: + attributes.mTextColorHint = appearance.getColorStateList(attr); + break; + case com.android.internal.R.styleable.TextAppearance_textColorLink: + attributes.mTextColorLink = appearance.getColorStateList(attr); + break; + case com.android.internal.R.styleable.TextAppearance_textSize: + attributes.mTextSize = + appearance.getDimensionPixelSize(attr, attributes.mTextSize); + break; + case com.android.internal.R.styleable.TextAppearance_typeface: + attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex); + if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { + attributes.mFontFamily = null; + } + break; + case com.android.internal.R.styleable.TextAppearance_fontFamily: + if (!context.isRestricted() && context.canLoadUnsafeResources()) { + try { + attributes.mFontTypeface = appearance.getFont(attr); + } catch (UnsupportedOperationException | Resources.NotFoundException e) { + // Expected if it is not a font resource. + } + } + if (attributes.mFontTypeface == null) { + attributes.mFontFamily = appearance.getString(attr); + } + attributes.mFontFamilyExplicit = true; + break; + case com.android.internal.R.styleable.TextAppearance_textStyle: + attributes.mStyleIndex = appearance.getInt(attr, attributes.mStyleIndex); + break; + case com.android.internal.R.styleable.TextAppearance_textAllCaps: + attributes.mAllCaps = appearance.getBoolean(attr, attributes.mAllCaps); + break; + case com.android.internal.R.styleable.TextAppearance_shadowColor: + attributes.mShadowColor = appearance.getInt(attr, attributes.mShadowColor); + break; + case com.android.internal.R.styleable.TextAppearance_shadowDx: + attributes.mShadowDx = appearance.getFloat(attr, attributes.mShadowDx); + break; + case com.android.internal.R.styleable.TextAppearance_shadowDy: + attributes.mShadowDy = appearance.getFloat(attr, attributes.mShadowDy); + break; + case com.android.internal.R.styleable.TextAppearance_shadowRadius: + attributes.mShadowRadius = appearance.getFloat(attr, attributes.mShadowRadius); + break; + case com.android.internal.R.styleable.TextAppearance_elegantTextHeight: + attributes.mHasElegant = true; + attributes.mElegant = appearance.getBoolean(attr, attributes.mElegant); + break; + case com.android.internal.R.styleable.TextAppearance_letterSpacing: + attributes.mHasLetterSpacing = true; + attributes.mLetterSpacing = + appearance.getFloat(attr, attributes.mLetterSpacing); + break; + case com.android.internal.R.styleable.TextAppearance_fontFeatureSettings: + attributes.mFontFeatureSettings = appearance.getString(attr); + break; + default: + } } + } - final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor); - if (textColor != null) { - setTextColor(textColor); + private void applyTextAppearance(TextAppearanceAttributes attributes) { + if (attributes.mTextColor != null) { + setTextColor(attributes.mTextColor); } - final int textSize = ta.getDimensionPixelSize(R.styleable.TextAppearance_textSize, 0); - if (textSize != 0) { - setRawTextSize(textSize, true /* shouldRequestLayout */); + if (attributes.mTextColorHint != null) { + setHintTextColor(attributes.mTextColorHint); } - final ColorStateList textColorHint = ta.getColorStateList( - R.styleable.TextAppearance_textColorHint); - if (textColorHint != null) { - setHintTextColor(textColorHint); + if (attributes.mTextColorLink != null) { + setLinkTextColor(attributes.mTextColorLink); } - final ColorStateList textColorLink = ta.getColorStateList( - R.styleable.TextAppearance_textColorLink); - if (textColorLink != null) { - setLinkTextColor(textColorLink); + if (attributes.mTextColorHighlight != 0) { + setHighlightColor(attributes.mTextColorHighlight); } - Typeface fontTypeface = null; - String fontFamily = null; - if (!context.isRestricted() && context.canLoadUnsafeResources()) { - try { - fontTypeface = ta.getFont(R.styleable.TextAppearance_fontFamily); - } catch (UnsupportedOperationException | Resources.NotFoundException e) { - // Expected if it is not a font resource. - } + if (attributes.mTextSize != 0) { + setRawTextSize(attributes.mTextSize, true /* shouldRequestLayout */); } - if (fontTypeface == null) { - fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily); + + if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { + attributes.mFontFamily = null; } - final int typefaceIndex = ta.getInt(R.styleable.TextAppearance_typeface, -1); - final int styleIndex = ta.getInt(R.styleable.TextAppearance_textStyle, -1); - setTypefaceFromAttrs(fontTypeface, fontFamily, typefaceIndex, styleIndex); + setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily, + attributes.mTypefaceIndex, attributes.mStyleIndex); - final int shadowColor = ta.getInt(R.styleable.TextAppearance_shadowColor, 0); - if (shadowColor != 0) { - final float dx = ta.getFloat(R.styleable.TextAppearance_shadowDx, 0); - final float dy = ta.getFloat(R.styleable.TextAppearance_shadowDy, 0); - final float r = ta.getFloat(R.styleable.TextAppearance_shadowRadius, 0); - setShadowLayer(r, dx, dy, shadowColor); + if (attributes.mShadowColor != 0) { + setShadowLayer(attributes.mShadowRadius, attributes.mShadowDx, attributes.mShadowDy, + attributes.mShadowColor); } - if (ta.getBoolean(R.styleable.TextAppearance_textAllCaps, false)) { + if (attributes.mAllCaps) { setTransformationMethod(new AllCapsTransformationMethod(getContext())); } - if (ta.hasValue(R.styleable.TextAppearance_elegantTextHeight)) { - setElegantTextHeight(ta.getBoolean( - R.styleable.TextAppearance_elegantTextHeight, false)); + if (attributes.mHasElegant) { + setElegantTextHeight(attributes.mElegant); } - if (ta.hasValue(R.styleable.TextAppearance_letterSpacing)) { - setLetterSpacing(ta.getFloat( - R.styleable.TextAppearance_letterSpacing, 0)); + if (attributes.mHasLetterSpacing) { + setLetterSpacing(attributes.mLetterSpacing); } - if (ta.hasValue(R.styleable.TextAppearance_fontFeatureSettings)) { - setFontFeatureSettings(ta.getString( - R.styleable.TextAppearance_fontFeatureSettings)); + if (attributes.mFontFeatureSettings != null) { + setFontFeatureSettings(attributes.mFontFeatureSettings); } - - ta.recycle(); } /** @@ -3735,6 +3731,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @param elegant set the paint's elegant metrics flag. * + * @see Paint#isElegantTextHeight(boolean) + * * @attr ref android.R.styleable#TextView_elegantTextHeight */ public void setElegantTextHeight(boolean elegant) { @@ -3749,6 +3747,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Get the value of the TextView's elegant height metrics flag. This setting selects font + * variants that have not been compacted to fit Latin-based vertical + * metrics, and also increases top and bottom bounds to provide more space. + * @return {@code true} if the elegant height metrics flag is set. + * + * @see #setElegantTextHeight(boolean) + * @see Paint#setElegantTextHeight(boolean) + */ + public boolean isElegantTextHeight() { + return mTextPaint.isElegantTextHeight(); + } + + /** * Gets the text letter-space value, which determines the spacing between characters. * The value returned is in ems. Normally, this value is 0.0. * @return The text letter-space value in ems. @@ -4848,8 +4859,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Sets line spacing for this TextView. Each line will have its height - * multiplied by <code>mult</code> and have <code>add</code> added to it. + * Sets line spacing for this TextView. Each line other than the last line will have its height + * multiplied by {@code mult} and have {@code add} added to it. + * * * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier @@ -4936,20 +4948,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void updateTextColors() { boolean inval = false; - int color = mTextColor.getColorForState(getDrawableState(), 0); + final int[] drawableState = getDrawableState(); + int color = mTextColor.getColorForState(drawableState, 0); if (color != mCurTextColor) { mCurTextColor = color; inval = true; } if (mLinkTextColor != null) { - color = mLinkTextColor.getColorForState(getDrawableState(), 0); + color = mLinkTextColor.getColorForState(drawableState, 0); if (color != mTextPaint.linkColor) { mTextPaint.linkColor = color; inval = true; } } if (mHintTextColor != null) { - color = mHintTextColor.getColorForState(getDrawableState(), 0); + color = mHintTextColor.getColorForState(drawableState, 0); if (color != mCurHintTextColor) { mCurHintTextColor = color; if (mText.length() == 0) { @@ -5314,7 +5327,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (imm != null) imm.restartInput(this); } else if (type == BufferType.SPANNABLE || mMovement != null) { text = mSpannableFactory.newSpannable(text); - } else if (!(text instanceof CharWrapper)) { + } else if (!(text instanceof PremeasuredText || text instanceof CharWrapper)) { text = TextUtils.stringOrSpannedString(text); } @@ -5533,6 +5546,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @android.view.RemotableViewMethod public final void setHint(CharSequence hint) { + setHintInternal(hint); + + if (mEditor != null && isInputMethodTarget()) { + mEditor.reportExtractedText(); + } + } + + private void setHintInternal(CharSequence hint) { mHint = TextUtils.stringOrSpannedString(hint); if (mLayout != null) { @@ -5590,10 +5611,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener spannable = (Spannable) text; } else { spannable = mSpannableFactory.newSpannable(text); - text = spannable; } SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class); + if (spans.length == 0) { + return text; + } else { + text = spannable; + } + for (int i = 0; i < spans.length; i++) { spannable.removeSpan(spans[i]); } @@ -6261,7 +6287,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int horizontalPadding = getCompoundPaddingLeft(); final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true); - if (mEditor.mCursorCount == 0) { + if (mEditor.mDrawableForCursor == null) { synchronized (TEMP_RECTF) { /* * The reason for this concern about the thickness of the @@ -6288,11 +6314,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener (int) Math.ceil(verticalPadding + TEMP_RECTF.bottom + thick)); } } else { - for (int i = 0; i < mEditor.mCursorCount; i++) { - Rect bounds = mEditor.mCursorDrawable[i].getBounds(); - invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding, - bounds.right + horizontalPadding, bounds.bottom + verticalPadding); - } + final Rect bounds = mEditor.mDrawableForCursor.getBounds(); + invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding, + bounds.right + horizontalPadding, bounds.bottom + verticalPadding); } } } @@ -6342,12 +6366,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int bottom = mLayout.getLineBottom(lineEnd); // mEditor can be null in case selection is set programmatically. - if (invalidateCursor && mEditor != null) { - for (int i = 0; i < mEditor.mCursorCount; i++) { - Rect bounds = mEditor.mCursorDrawable[i].getBounds(); - top = Math.min(top, bounds.top); - bottom = Math.max(bottom, bounds.bottom); - } + if (invalidateCursor && mEditor != null && mEditor.mDrawableForCursor != null) { + final Rect bounds = mEditor.mDrawableForCursor.getBounds(); + top = Math.min(top, bounds.top); + bottom = Math.max(bottom, bounds.bottom); } final int compoundPaddingLeft = getCompoundPaddingLeft(); @@ -6692,7 +6714,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mHighlightPath == null) mHighlightPath = new Path(); mHighlightPath.reset(); mLayout.getCursorPath(selStart, mHighlightPath, mText); - mEditor.updateCursorsPositions(); + mEditor.updateCursorPosition(); mHighlightPathBogus = false; } @@ -7634,6 +7656,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { MetaKeyKeyListener.stopSelecting(this, sp); } + + setHintInternal(text.hint); } /** @@ -7909,6 +7933,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setTextDirection(mTextDir) .setLineSpacing(mSpacingAdd, mSpacingMult) .setIncludePad(mIncludePad) + .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing) .setBreakStrategy(mBreakStrategy) .setHyphenationFrequency(mHyphenationFrequency) .setJustificationMode(mJustificationMode) @@ -7951,10 +7976,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean useSaved) { Layout result = null; if (mText instanceof Spannable) { - result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth, - alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, - mBreakStrategy, mHyphenationFrequency, mJustificationMode, - getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth); + final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint, + wantWidth) + .setDisplayText(mTransformed) + .setAlignment(alignment) + .setTextDirection(mTextDir) + .setLineSpacing(mSpacingAdd, mSpacingMult) + .setIncludePad(mIncludePad) + .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing) + .setBreakStrategy(mBreakStrategy) + .setHyphenationFrequency(mHyphenationFrequency) + .setJustificationMode(mJustificationMode) + .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null) + .setEllipsizedWidth(ellipsisWidth); + result = builder.build(); } else { if (boring == UNKNOWN_BORING) { boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring); @@ -8001,6 +8036,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setTextDirection(mTextDir) .setLineSpacing(mSpacingAdd, mSpacingMult) .setIncludePad(mIncludePad) + .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing) .setBreakStrategy(mBreakStrategy) .setHyphenationFrequency(mHyphenationFrequency) .setJustificationMode(mJustificationMode) @@ -8009,7 +8045,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener builder.setEllipsize(effectiveEllipsize) .setEllipsizedWidth(ellipsisWidth); } - // TODO: explore always setting maxLines result = builder.build(); } return result; @@ -8112,6 +8147,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int des = -1; boolean fromexisting = false; + final float widthLimit = (widthMode == MeasureSpec.AT_MOST) + ? (float) widthSize : Float.MAX_VALUE; if (widthMode == MeasureSpec.EXACTLY) { // Parent has told us how big to be. So be it. @@ -8132,8 +8169,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (boring == null || boring == UNKNOWN_BORING) { if (des < 0) { - des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, 0, - mTransformed.length(), mTextPaint, mTextDir)); + des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0, + mTransformed.length(), mTextPaint, mTextDir, widthLimit)); } width = des; } else { @@ -8163,8 +8200,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (hintBoring == null || hintBoring == UNKNOWN_BORING) { if (hintDes < 0) { - hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, 0, mHint.length(), - mTextPaint, mTextDir)); + hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0, + mHint.length(), mTextPaint, mTextDir, widthLimit)); } hintWidth = hintDes; } else { @@ -8359,6 +8396,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener layoutBuilder.setAlignment(getLayoutAlignment()) .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier()) .setIncludePad(getIncludeFontPadding()) + .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing) .setBreakStrategy(getBreakStrategy()) .setHyphenationFrequency(getHyphenationFrequency()) .setJustificationMode(getJustificationMode()) @@ -9018,6 +9056,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * + * Checks whether the transformation method applied to this TextView is set to ALL CAPS. This + * settings is internally ignored if this field is editable or selectable. + * @return Whether the current transformation method is for ALL CAPS. + * + * @see #setAllCaps(boolean) + * @see #setTransformationMethod(TransformationMethod) + */ + public boolean isAllCaps() { + final TransformationMethod method = getTransformationMethod(); + return method != null && method instanceof AllCapsTransformationMethod; + } + + /** * If true, sets the properties of this field (number of lines, horizontally scrolling, * transformation method) to be for a single-line input; if false, restores these to the default * conditions. @@ -10292,6 +10344,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // of the View (and can be any drawable) or a BackgroundColorSpan inside the text. structure.setTextStyle(getTextSize(), getCurrentTextColor(), AssistStructure.ViewNode.TEXT_COLOR_UNDEFINED /* bgColor */, style); + } else { + structure.setMinTextEms(getMinEms()); + structure.setMaxTextEms(getMaxEms()); + int maxLength = -1; + for (InputFilter filter: getFilters()) { + if (filter instanceof InputFilter.LengthFilter) { + maxLength = ((InputFilter.LengthFilter) filter).getMax(); + break; + } + } + structure.setMaxTextLength(maxLength); } } structure.setHint(getHint()); @@ -10340,10 +10403,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return isTextEditable() ? AUTOFILL_TYPE_TEXT : AUTOFILL_TYPE_NONE; } + /** + * Gets the {@link TextView}'s current text for AutoFill. The value is trimmed to 100K + * {@code char}s if longer. + * + * @return current text, {@code null} if the text is not editable + * + * @see View#getAutofillValue() + */ @Override @Nullable public AutofillValue getAutofillValue() { - return isTextEditable() ? AutofillValue.forText(getText()) : null; + if (isTextEditable()) { + final CharSequence text = TextUtils.trimToParcelableSize(getText()); + return AutofillValue.forText(text); + } + return null; } /** @hide */ @@ -10730,13 +10805,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mEditor.mProcessTextIntentActionsHandler.initializeAccessibilityActions(); } + super.sendAccessibilityEventInternal(eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { // Do not send scroll events since first they are not interesting for // accessibility and second such events a generated too frequently. // For details see the implementation of bringTextIntoView(). - if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { return; } - super.sendAccessibilityEventInternal(eventType); + super.sendAccessibilityEventUnchecked(event); } /** @@ -10757,7 +10837,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // Otherwise, return whatever text is being displayed. - return mTransformed; + return TextUtils.trimToParcelableSize(mTransformed); } void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, @@ -10842,13 +10922,25 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; case ID_CUT: - setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max))); - deleteText_internal(min, max); + final ClipData cutData = ClipData.newPlainText(null, getTransformedText(min, max)); + if (setPrimaryClip(cutData)) { + deleteText_internal(min, max); + } else { + Toast.makeText(getContext(), + com.android.internal.R.string.failed_to_copy_to_clipboard, + Toast.LENGTH_SHORT).show(); + } return true; case ID_COPY: - setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max))); - stopTextActionMode(); + final ClipData copyData = ClipData.newPlainText(null, getTransformedText(min, max)); + if (setPrimaryClip(copyData)) { + stopTextActionMode(); + } else { + Toast.makeText(getContext(), + com.android.internal.R.string.failed_to_copy_to_clipboard, + Toast.LENGTH_SHORT).show(); + } return true; case ID_REPLACE: @@ -11208,17 +11300,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND); sharingIntent.setType("text/plain"); sharingIntent.removeExtra(android.content.Intent.EXTRA_TEXT); + selectedText = TextUtils.trimToParcelableSize(selectedText); sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText); getContext().startActivity(Intent.createChooser(sharingIntent, null)); Selection.setSelection((Spannable) mText, getSelectionEnd()); } } - private void setPrimaryClip(ClipData clip) { + @CheckResult + private boolean setPrimaryClip(ClipData clip) { ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setPrimaryClip(clip); + try { + clipboard.setPrimaryClip(clip); + } catch (Throwable t) { + return false; + } sLastCutCopyOrTextChangedTime = SystemClock.uptimeMillis(); + return true; } /** diff --git a/core/java/android/widget/TextViewMetrics.java b/core/java/android/widget/TextViewMetrics.java index 96d17943bf09..738a5742c6ba 100644 --- a/core/java/android/widget/TextViewMetrics.java +++ b/core/java/android/widget/TextViewMetrics.java @@ -37,29 +37,4 @@ public final class TextViewMetrics { * Long press on TextView - drag and drop started. */ public static final int SUBTYPE_LONG_PRESS_DRAG_AND_DROP = 2; - - /** - * Assist menu item (shown or clicked) - classification: other. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_OTHER = 0; - - /** - * Assist menu item (shown or clicked) - classification: email. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_EMAIL = 1; - - /** - * Assist menu item (shown or clicked) - classification: phone. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_PHONE = 2; - - /** - * Assist menu item (shown or clicked) - classification: address. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_ADDRESS = 3; - - /** - * Assist menu item (shown or clicked) - classification: url. - */ - public static final int SUBTYPE_ASSIST_MENU_ITEM_URL = 4; } diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java new file mode 100644 index 000000000000..293471c686e5 --- /dev/null +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -0,0 +1,403 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.internal.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.ActivityManager; +import android.app.ActivityThread; +import android.app.AlertDialog; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.media.AudioAttributes; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Handler; +import android.os.UserHandle; +import android.os.Vibrator; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.Window; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; + +import android.widget.Toast; +import com.android.internal.R; + +import java.util.Collections; +import java.util.Map; + +import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; + +import static com.android.internal.util.ArrayUtils.convertToLongArray; + +/** + * Class to help manage the accessibility shortcut + */ +public class AccessibilityShortcutController { + private static final String TAG = "AccessibilityShortcutController"; + + // Dummy component names for framework features + public static final ComponentName COLOR_INVERSION_COMPONENT_NAME = + new ComponentName("com.android.server.accessibility", "ColorInversion"); + public static final ComponentName DALTONIZER_COMPONENT_NAME = + new ComponentName("com.android.server.accessibility", "Daltonizer"); + + private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY) + .build(); + private static Map<ComponentName, ToggleableFrameworkFeatureInfo> sFrameworkShortcutFeaturesMap; + + private final Context mContext; + private AlertDialog mAlertDialog; + private boolean mIsShortcutEnabled; + private boolean mEnabledOnLockScreen; + private int mUserId; + + // Visible for testing + public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider(); + + /** + * Get the component name string for the service or feature currently assigned to the + * accessiblity shortcut + * + * @param context A valid context + * @param userId The user ID of interest + * @return The flattened component name string of the service selected by the user, or the + * string for the default service if the user has not made a selection + */ + public static String getTargetServiceComponentNameString( + Context context, int userId) { + final String currentShortcutServiceId = Settings.Secure.getStringForUser( + context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, + userId); + if (currentShortcutServiceId != null) { + return currentShortcutServiceId; + } + return context.getString(R.string.config_defaultAccessibilityService); + } + + /** + * @return An immutable map from dummy component names to feature info for toggling a framework + * feature + */ + public static Map<ComponentName, ToggleableFrameworkFeatureInfo> + getFrameworkShortcutFeaturesMap() { + if (sFrameworkShortcutFeaturesMap == null) { + Map<ComponentName, ToggleableFrameworkFeatureInfo> featuresMap = new ArrayMap<>(2); + featuresMap.put(COLOR_INVERSION_COMPONENT_NAME, + new ToggleableFrameworkFeatureInfo( + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, + "1" /* Value to enable */, "0" /* Value to disable */, + R.string.color_inversion_feature_name)); + featuresMap.put(DALTONIZER_COMPONENT_NAME, + new ToggleableFrameworkFeatureInfo( + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, + "1" /* Value to enable */, "0" /* Value to disable */, + R.string.color_correction_feature_name)); + sFrameworkShortcutFeaturesMap = Collections.unmodifiableMap(featuresMap); + } + return sFrameworkShortcutFeaturesMap; + } + + public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) { + mContext = context; + mUserId = initialUserId; + + // Keep track of state of shortcut settings + final ContentObserver co = new ContentObserver(handler) { + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + if (userId == mUserId) { + onSettingsChanged(); + } + } + }; + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE), + false, co, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED), + false, co, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN), + false, co, UserHandle.USER_ALL); + setCurrentUser(mUserId); + } + + public void setCurrentUser(int currentUserId) { + mUserId = currentUserId; + onSettingsChanged(); + } + + /** + * Check if the shortcut is available. + * + * @param onLockScreen Whether or not the phone is currently locked. + * + * @return {@code true} if the shortcut is available + */ + public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) { + return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen); + } + + public void onSettingsChanged() { + final boolean haveValidService = + !TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId)); + final ContentResolver cr = mContext.getContentResolver(); + final boolean enabled = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1; + mEnabledOnLockScreen = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN, 0, mUserId) == 1; + mIsShortcutEnabled = enabled && haveValidService; + } + + /** + * Called when the accessibility shortcut is activated + */ + public void performAccessibilityShortcut() { + Slog.d(TAG, "Accessibility shortcut activated"); + final ContentResolver cr = mContext.getContentResolver(); + final int userId = ActivityManager.getCurrentUser(); + final int dialogAlreadyShown = Settings.Secure.getIntForUser( + cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId); + // Use USAGE_ASSISTANCE_ACCESSIBILITY for TVs to ensure that TVs play the ringtone as they + // have less ways of providing feedback like vibration. + final int audioAttributesUsage = hasFeatureLeanback() + ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY + : AudioAttributes.USAGE_NOTIFICATION_EVENT; + + // Play a notification tone + final Ringtone tone = + RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI); + if (tone != null) { + tone.setAudioAttributes(new AudioAttributes.Builder() + .setUsage(audioAttributesUsage) + .build()); + tone.play(); + } + + // Play a notification vibration + Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + if ((vibrator != null) && vibrator.hasVibrator()) { + // Don't check if haptics are disabled, as we need to alert the user that their + // way of interacting with the phone may change if they activate the shortcut + long[] vibePattern = convertToLongArray( + mContext.getResources().getIntArray(R.array.config_longPressVibePattern)); + vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES); + } + + + if (dialogAlreadyShown == 0) { + // The first time, we show a warning rather than toggle the service to give the user a + // chance to turn off this feature before stuff gets enabled. + mAlertDialog = createShortcutWarningDialog(userId); + if (mAlertDialog == null) { + return; + } + Window w = mAlertDialog.getWindow(); + WindowManager.LayoutParams attr = w.getAttributes(); + attr.type = TYPE_KEYGUARD_DIALOG; + w.setAttributes(attr); + mAlertDialog.show(); + Settings.Secure.putIntForUser( + cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1, userId); + } else { + if (mAlertDialog != null) { + mAlertDialog.dismiss(); + mAlertDialog = null; + } + + // Show a toast alerting the user to what's happening + final String serviceName = getShortcutFeatureDescription(false /* no summary */); + if (serviceName == null) { + Slog.e(TAG, "Accessibility shortcut set to invalid service"); + return; + } + // For accessibility services, show a toast explaining what we're doing. + final AccessibilityServiceInfo serviceInfo = getInfoForTargetService(); + if (serviceInfo != null) { + String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo) + ? R.string.accessibility_shortcut_disabling_service + : R.string.accessibility_shortcut_enabling_service); + String toastMessage = String.format(toastMessageFormatString, serviceName); + Toast warningToast = mFrameworkObjectProvider.makeToastFromText( + mContext, toastMessage, Toast.LENGTH_LONG); + warningToast.getWindowParams().privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + warningToast.show(); + } + + mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext) + .performAccessibilityShortcut(); + } + } + + private AlertDialog createShortcutWarningDialog(int userId) { + final String serviceDescription = getShortcutFeatureDescription(true /* Include summary */); + + if (serviceDescription == null) { + return null; + } + + final String warningMessage = String.format( + mContext.getString(R.string.accessibility_shortcut_toogle_warning), + serviceDescription); + final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder( + // Use SystemUI context so we pick up any theme set in a vendor overlay + mFrameworkObjectProvider.getSystemUiContext()) + .setTitle(R.string.accessibility_shortcut_warning_dialog_title) + .setMessage(warningMessage) + .setCancelable(false) + .setPositiveButton(R.string.leave_accessibility_shortcut_on, null) + .setNegativeButton(R.string.disable_accessibility_shortcut, + (DialogInterface d, int which) -> { + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "", + userId); + }) + .setOnCancelListener((DialogInterface d) -> { + // If canceled, treat as if the dialog has never been shown + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId); + }) + .create(); + return alertDialog; + } + + private AccessibilityServiceInfo getInfoForTargetService() { + final String currentShortcutServiceString = getTargetServiceComponentNameString( + mContext, UserHandle.USER_CURRENT); + if (currentShortcutServiceString == null) { + return null; + } + AccessibilityManager accessibilityManager = + mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext); + return accessibilityManager.getInstalledServiceInfoWithComponentName( + ComponentName.unflattenFromString(currentShortcutServiceString)); + } + + private String getShortcutFeatureDescription(boolean includeSummary) { + final String currentShortcutServiceString = getTargetServiceComponentNameString( + mContext, UserHandle.USER_CURRENT); + if (currentShortcutServiceString == null) { + return null; + } + final ComponentName targetComponentName = + ComponentName.unflattenFromString(currentShortcutServiceString); + final ToggleableFrameworkFeatureInfo frameworkFeatureInfo = + getFrameworkShortcutFeaturesMap().get(targetComponentName); + if (frameworkFeatureInfo != null) { + return frameworkFeatureInfo.getLabel(mContext); + } + final AccessibilityServiceInfo serviceInfo = mFrameworkObjectProvider + .getAccessibilityManagerInstance(mContext).getInstalledServiceInfoWithComponentName( + targetComponentName); + if (serviceInfo == null) { + return null; + } + final PackageManager pm = mContext.getPackageManager(); + String label = serviceInfo.getResolveInfo().loadLabel(pm).toString(); + String summary = serviceInfo.loadSummary(pm).toString(); + if (!includeSummary || TextUtils.isEmpty(summary)) { + return label; + } + return String.format("%s\n%s", label, summary); + } + + private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) { + AccessibilityManager accessibilityManager = + mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext); + return accessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo); + } + + private boolean hasFeatureLeanback() { + return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); + } + + /** + * Immutable class to hold info about framework features that can be controlled by shortcut + */ + public static class ToggleableFrameworkFeatureInfo { + private final String mSettingKey; + private final String mSettingOnValue; + private final String mSettingOffValue; + private final int mLabelStringResourceId; + // These go to the settings wrapper + private int mIconDrawableId; + + ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue, + String settingOffValue, int labelStringResourceId) { + mSettingKey = settingKey; + mSettingOnValue = settingOnValue; + mSettingOffValue = settingOffValue; + mLabelStringResourceId = labelStringResourceId; + } + + /** + * @return The settings key to toggle between two values + */ + public String getSettingKey() { + return mSettingKey; + } + + /** + * @return The value to write to settings to turn the feature on + */ + public String getSettingOnValue() { + return mSettingOnValue; + } + + /** + * @return The value to write to settings to turn the feature off + */ + public String getSettingOffValue() { + return mSettingOffValue; + } + + public String getLabel(Context context) { + return context.getString(mLabelStringResourceId); + } + } + + // Class to allow mocking of static framework calls + public static class FrameworkObjectProvider { + public AccessibilityManager getAccessibilityManagerInstance(Context context) { + return AccessibilityManager.getInstance(context); + } + + public AlertDialog.Builder getAlertDialogBuilder(Context context) { + return new AlertDialog.Builder(context); + } + + public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) { + return Toast.makeText(context, charSequence, duration); + } + + public Context getSystemUiContext() { + return ActivityThread.currentActivityThread().getSystemUiContext(); + } + } +} diff --git a/core/java/com/android/internal/alsa/AlsaCardsParser.java b/core/java/com/android/internal/alsa/AlsaCardsParser.java index 5b92a1734d47..bb75bf6e6fb8 100644 --- a/core/java/com/android/internal/alsa/AlsaCardsParser.java +++ b/core/java/com/android/internal/alsa/AlsaCardsParser.java @@ -37,6 +37,12 @@ public class AlsaCardsParser { private ArrayList<AlsaCardRecord> mCardRecords = new ArrayList<AlsaCardRecord>(); + public static final int SCANSTATUS_NOTSCANNED = -1; + public static final int SCANSTATUS_SUCCESS = 0; + public static final int SCANSTATUS_FAIL = 1; + public static final int SCANSTATUS_EMPTY = 2; + private int mScanStatus = SCANSTATUS_NOTSCANNED; + public class AlsaCardRecord { private static final String TAG = "AlsaCardRecord"; private static final String kUsbCardKeyStr = "at usb-"; @@ -104,10 +110,11 @@ public class AlsaCardsParser { public AlsaCardsParser() {} - public void scan() { + public int scan() { if (DEBUG) { - Slog.i(TAG, "AlsaCardsParser.scan()"); + Slog.i(TAG, "AlsaCardsParser.scan()...."); } + mCardRecords = new ArrayList<AlsaCardRecord>(); File cardsFile = new File(kCardsFilePath); @@ -134,11 +141,26 @@ public class AlsaCardsParser { mCardRecords.add(cardRecord); } reader.close(); + if (mCardRecords.size() > 0) { + mScanStatus = SCANSTATUS_SUCCESS; + } else { + mScanStatus = SCANSTATUS_EMPTY; + } } catch (FileNotFoundException e) { e.printStackTrace(); + mScanStatus = SCANSTATUS_FAIL; } catch (IOException e) { e.printStackTrace(); + mScanStatus = SCANSTATUS_FAIL; + } + if (DEBUG) { + Slog.i(TAG, " status:" + mScanStatus); } + return mScanStatus; + } + + public int getScanStatus() { + return mScanStatus; } public ArrayList<AlsaCardRecord> getScanRecords() { @@ -182,7 +204,11 @@ public class AlsaCardsParser { } // get the new list of devices - scan(); + if (scan() != SCANSTATUS_SUCCESS) { + Slog.e(TAG, "Error scanning Alsa cards file."); + return -1; + } + if (DEBUG) { LogDevices("Current Devices:", mCardRecords); } diff --git a/core/java/com/android/internal/alsa/AlsaDevicesParser.java b/core/java/com/android/internal/alsa/AlsaDevicesParser.java index 7cdd89701d73..15261bafd299 100644 --- a/core/java/com/android/internal/alsa/AlsaDevicesParser.java +++ b/core/java/com/android/internal/alsa/AlsaDevicesParser.java @@ -46,6 +46,12 @@ public class AlsaDevicesParser { private boolean mHasPlaybackDevices = false; private boolean mHasMIDIDevices = false; + public static final int SCANSTATUS_NOTSCANNED = -1; + public static final int SCANSTATUS_SUCCESS = 0; + public static final int SCANSTATUS_FAIL = 1; + public static final int SCANSTATUS_EMPTY = 2; + private int mScanStatus = SCANSTATUS_NOTSCANNED; + public class AlsaDeviceRecord { public static final int kDeviceType_Unknown = -1; public static final int kDeviceType_Audio = 0; @@ -258,7 +264,11 @@ public class AlsaDevicesParser { return line.charAt(kIndex_CardDeviceField) == '['; } - public void scan() { + public int scan() { + if (DEBUG) { + Slog.i(TAG, "AlsaDevicesParser.scan()...."); + } + mDeviceRecords.clear(); File devicesFile = new File(kDevicesFilePath); @@ -274,11 +284,27 @@ public class AlsaDevicesParser { } } reader.close(); + // success if we add at least 1 record + if (mDeviceRecords.size() > 0) { + mScanStatus = SCANSTATUS_SUCCESS; + } else { + mScanStatus = SCANSTATUS_EMPTY; + } } catch (FileNotFoundException e) { e.printStackTrace(); + mScanStatus = SCANSTATUS_FAIL; } catch (IOException e) { e.printStackTrace(); + mScanStatus = SCANSTATUS_FAIL; } + if (DEBUG) { + Slog.i(TAG, " status:" + mScanStatus); + } + return mScanStatus; + } + + public int getScanStatus() { + return mScanStatus; } // diff --git a/core/java/com/android/internal/app/NightDisplayController.java b/core/java/com/android/internal/app/ColorDisplayController.java index e0e15c818a02..b8682a89eb95 100644 --- a/core/java/com/android/internal/app/NightDisplayController.java +++ b/core/java/com/android/internal/app/ColorDisplayController.java @@ -42,24 +42,20 @@ import java.time.ZoneId; import java.time.format.DateTimeParseException; /** - * Controller for managing Night display settings. + * Controller for managing night display and color mode settings. * <p/> * Night display tints your screen red at night. This makes it easier to look at your screen in * dim light and may help you fall asleep more easily. */ -public final class NightDisplayController { +public final class ColorDisplayController { - private static final String TAG = "NightDisplayController"; + private static final String TAG = "ColorDisplayController"; private static final boolean DEBUG = false; @Retention(RetentionPolicy.SOURCE) @IntDef({ AUTO_MODE_DISABLED, AUTO_MODE_CUSTOM, AUTO_MODE_TWILIGHT }) public @interface AutoMode {} - @Retention(RetentionPolicy.SOURCE) - @IntDef({ COLOR_MODE_NATURAL, COLOR_MODE_BOOSTED, COLOR_MODE_SATURATED }) - public @interface ColorMode {} - /** * Auto mode value to prevent Night display from being automatically activated. It can still * be activated manually via {@link #setActivated(boolean)}. @@ -82,6 +78,10 @@ public final class NightDisplayController { */ public static final int AUTO_MODE_TWILIGHT = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ COLOR_MODE_NATURAL, COLOR_MODE_BOOSTED, COLOR_MODE_SATURATED }) + public @interface ColorMode {} + /** * Color mode with natural colors. * @@ -114,11 +114,11 @@ public final class NightDisplayController { private Callback mCallback; - public NightDisplayController(@NonNull Context context) { + public ColorDisplayController(@NonNull Context context) { this(context, ActivityManager.getCurrentUser()); } - public NightDisplayController(@NonNull Context context, int userId) { + public ColorDisplayController(@NonNull Context context, int userId) { mContext = context.getApplicationContext(); mUserId = userId; @@ -361,7 +361,7 @@ public final class NightDisplayController { */ public void setColorMode(@ColorMode int colorMode) { if (colorMode < COLOR_MODE_NATURAL || colorMode > COLOR_MODE_SATURATED) { - return; + throw new IllegalArgumentException("Invalid colorMode: " + colorMode); } System.putIntForUser(mContext.getContentResolver(), System.DISPLAY_COLOR_MODE, colorMode, mUserId); diff --git a/core/java/com/android/internal/app/IAppOpsCallback.aidl b/core/java/com/android/internal/app/IAppOpsCallback.aidl index 5fdc92041c3c..15221b1f0fa7 100644 --- a/core/java/com/android/internal/app/IAppOpsCallback.aidl +++ b/core/java/com/android/internal/app/IAppOpsCallback.aidl @@ -17,7 +17,7 @@ package com.android.internal.app; // This interface is also used by native code, so must -// be kept in sync with frameworks/native/include/binder/IAppOpsCallback.h +// be kept in sync with frameworks/native/libs/binder/include/binder/IAppOpsCallback.h oneway interface IAppOpsCallback { void opChanged(int op, int uid, String packageName); } diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 7a119b4351c0..2b975fe03bf2 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -22,7 +22,7 @@ import com.android.internal.app.IAppOpsCallback; interface IAppOpsService { // These first methods are also called by native code, so must - // be kept in sync with frameworks/native/include/binder/IAppOpsService.h + // be kept in sync with frameworks/native/libs/binder/include/binder/IAppOpsService.h int checkOperation(int code, int uid, String packageName); int noteOperation(int code, int uid, String packageName); int startOperation(IBinder token, int code, int uid, String packageName); diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index 4275e0b43a4e..f40523162078 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -29,7 +29,7 @@ import android.telephony.SignalStrength; interface IBatteryStats { // These first methods are also called by native code, so must - // be kept in sync with frameworks/native/include/binder/IBatteryStats.h + // be kept in sync with frameworks/native/libs/binder/include/binder/IBatteryStats.h void noteStartSensor(int uid, int sensor); void noteStopSensor(int uid, int sensor); void noteStartVideo(int uid); diff --git a/core/java/com/android/internal/app/IMediaContainerService.aidl b/core/java/com/android/internal/app/IMediaContainerService.aidl index 36e4c1c61f0e..ef30cd184721 100644 --- a/core/java/com/android/internal/app/IMediaContainerService.aidl +++ b/core/java/com/android/internal/app/IMediaContainerService.aidl @@ -21,12 +21,10 @@ import android.content.pm.PackageInfoLite; import android.content.res.ObbInfo; interface IMediaContainerService { - String copyPackageToContainer(String packagePath, String containerId, String key, - boolean isExternal, boolean isForwardLocked, String abiOverride); int copyPackage(String packagePath, in IParcelFileDescriptorFactory target); PackageInfoLite getMinimalPackageInfo(String packagePath, int flags, String abiOverride); ObbInfo getObbInfo(String filename); void clearDirectory(String directory); - long calculateInstalledSize(String packagePath, boolean isForwardLocked, String abiOverride); + long calculateInstalledSize(String packagePath, String abiOverride); } diff --git a/core/java/com/android/internal/app/LocaleHelper.java b/core/java/com/android/internal/app/LocaleHelper.java index d26be9195825..0a230a90a735 100644 --- a/core/java/com/android/internal/app/LocaleHelper.java +++ b/core/java/com/android/internal/app/LocaleHelper.java @@ -136,7 +136,16 @@ public class LocaleHelper { * @return the localized country name. */ public static String getDisplayCountry(Locale locale, Locale displayLocale) { - return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.forLocale(displayLocale)); + final String languageTag = locale.toLanguageTag(); + final ULocale uDisplayLocale = ULocale.forLocale(displayLocale); + final String country = ULocale.getDisplayCountry(languageTag, uDisplayLocale); + final String numberingSystem = locale.getUnicodeLocaleType("nu"); + if (numberingSystem != null) { + return String.format("%s (%s)", country, + ULocale.getDisplayKeywordValue(languageTag, "numbers", uDisplayLocale)); + } else { + return country; + } } /** @@ -181,7 +190,7 @@ public class LocaleHelper { // Hong Kong Traditional Chinese (zh_Hant_HK) and Dzongkha (dz). But that has two // problems: it's expensive to extract it, and in case the output string becomes // automatically ellipsized, it can result in weird output. - localeNames[maxLocales] = TextUtils.ELLIPSIS_STRING; + localeNames[maxLocales] = TextUtils.getEllipsisString(TextUtils.TruncateAt.END); } ListFormatter lfn = ListFormatter.getInstance(dispLocale); diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java index 9936ed5c6491..c8c2fcf60d1f 100644 --- a/core/java/com/android/internal/app/LocalePicker.java +++ b/core/java/com/android/internal/app/LocalePicker.java @@ -93,10 +93,6 @@ public class LocalePicker extends ListFragment { return context.getResources().getStringArray(R.array.supported_locales); } - public static String[] getPseudoLocales() { - return pseudoLocales; - } - public static List<LocaleInfo> getAllAssetLocales(Context context, boolean isInDeveloperMode) { final Resources resources = context.getResources(); @@ -104,13 +100,6 @@ public class LocalePicker extends ListFragment { List<String> localeList = new ArrayList<String>(locales.length); Collections.addAll(localeList, locales); - // Don't show the pseudolocales unless we're in developer mode. http://b/17190407. - if (!isInDeveloperMode) { - for (String locale : pseudoLocales) { - localeList.remove(locale); - } - } - Collections.sort(localeList); final String[] specialLocaleCodes = resources.getStringArray(R.array.special_locale_codes); final String[] specialLocaleNames = resources.getStringArray(R.array.special_locale_names); @@ -122,6 +111,10 @@ public class LocalePicker extends ListFragment { || l.getLanguage().isEmpty() || l.getCountry().isEmpty()) { continue; } + // Don't show the pseudolocales unless we're in developer mode. http://b/17190407. + if (!isInDeveloperMode && LocaleList.isPseudoLocale(l)) { + continue; + } if (localeInfos.isEmpty()) { if (DEBUG) { diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java index 3d5cd0f5847b..d0719eeca04e 100644 --- a/core/java/com/android/internal/app/LocalePickerWithRegion.java +++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java @@ -238,7 +238,7 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O mSearchView.setOnQueryTextListener(this); // Restore previous search status - if (mPreviousSearch != null) { + if (!TextUtils.isEmpty(mPreviousSearch)) { searchMenuItem.expandActionView(); mSearchView.setIconified(false); mSearchView.setActivated(true); diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java index e3fce5197dca..2b0b5eec6c56 100644 --- a/core/java/com/android/internal/app/LocaleStore.java +++ b/core/java/com/android/internal/app/LocaleStore.java @@ -17,6 +17,7 @@ package com.android.internal.app; import android.content.Context; +import android.os.LocaleList; import android.provider.Settings; import android.telephony.TelephonyManager; @@ -68,7 +69,9 @@ public class LocaleStore { return null; } return new Locale.Builder() - .setLocale(locale).setRegion("") + .setLocale(locale) + .setRegion("") + .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "") .build(); } @@ -253,11 +256,25 @@ public class LocaleStore { Set<String> simCountries = getSimCountries(context); + final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; for (String localeId : LocalePicker.getSupportedLocales(context)) { if (localeId.isEmpty()) { throw new IllformedLocaleException("Bad locale entry in locale_config.xml"); } LocaleInfo li = new LocaleInfo(localeId); + + if (LocaleList.isPseudoLocale(li.getLocale())) { + if (isInDeveloperMode) { + li.setTranslated(true); + li.mIsPseudo = true; + li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; + } else { + // Do not display pseudolocales unless in development mode. + continue; + } + } + if (simCountries.contains(li.getLocale().getCountry())) { li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; } @@ -271,19 +288,6 @@ public class LocaleStore { } } - boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(), - Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; - for (String localeId : LocalePicker.getPseudoLocales()) { - LocaleInfo li = getLocaleInfo(Locale.forLanguageTag(localeId)); - if (isInDeveloperMode) { - li.setTranslated(true); - li.mIsPseudo = true; - li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; - } else { - sLocaleCache.remove(li.getId()); - } - } - // TODO: See if we can reuse what LocaleList.matchScore does final HashSet<String> localizedLocales = new HashSet<>(); for (String localeId : LocalePicker.getSystemAssetLocales()) { diff --git a/core/java/com/android/internal/app/ShutdownActivity.java b/core/java/com/android/internal/app/ShutdownActivity.java index 745d28f367a3..f81e83836e18 100644 --- a/core/java/com/android/internal/app/ShutdownActivity.java +++ b/core/java/com/android/internal/app/ShutdownActivity.java @@ -41,6 +41,9 @@ public class ShutdownActivity extends Activity { mReboot = Intent.ACTION_REBOOT.equals(intent.getAction()); mConfirm = intent.getBooleanExtra(Intent.EXTRA_KEY_CONFIRM, false); mUserRequested = intent.getBooleanExtra(Intent.EXTRA_USER_REQUESTED_SHUTDOWN, false); + final String reason = mUserRequested + ? PowerManager.SHUTDOWN_USER_REQUESTED + : intent.getStringExtra(Intent.EXTRA_REASON); Slog.i(TAG, "onCreate(): confirm=" + mConfirm); Thread thr = new Thread("ShutdownActivity") { @@ -52,9 +55,7 @@ public class ShutdownActivity extends Activity { if (mReboot) { pm.reboot(mConfirm, null, false); } else { - pm.shutdown(mConfirm, - mUserRequested ? PowerManager.SHUTDOWN_USER_REQUESTED : null, - false); + pm.shutdown(mConfirm, reason, false); } } catch (RemoteException e) { } diff --git a/core/java/com/android/internal/app/UnlaunchableAppActivity.java b/core/java/com/android/internal/app/UnlaunchableAppActivity.java index 0a539f19c48a..8016a6559bcc 100644 --- a/core/java/com/android/internal/app/UnlaunchableAppActivity.java +++ b/core/java/com/android/internal/app/UnlaunchableAppActivity.java @@ -111,14 +111,7 @@ public class UnlaunchableAppActivity extends Activity @Override public void onClick(DialogInterface dialog, int which) { if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE && which == DialogInterface.BUTTON_POSITIVE) { - if (UserManager.get(this).trySetQuietModeDisabled(mUserId, mTarget) - && mTarget != null) { - try { - startIntentSenderForResult(mTarget, -1, null, 0, 0, 0); - } catch (IntentSender.SendIntentException e) { - /* ignore */ - } - } + UserManager.get(this).trySetQuietModeDisabled(mUserId, mTarget); } } diff --git a/core/java/com/android/internal/app/procstats/DumpUtils.java b/core/java/com/android/internal/app/procstats/DumpUtils.java index ebedc89cab1e..0bc8c483313d 100644 --- a/core/java/com/android/internal/app/procstats/DumpUtils.java +++ b/core/java/com/android/internal/app/procstats/DumpUtils.java @@ -29,6 +29,7 @@ import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; import static com.android.internal.app.procstats.ProcessStats.*; @@ -66,6 +67,8 @@ public final class DumpUtils { "cch-activity", "cch-aclient", "cch-empty" }; + // State enum is defined in frameworks/base/core/proto/android/service/procstats.proto + // Update states must sync enum definition as well, the ordering must not be changed. static final String[] ADJ_SCREEN_TAGS = new String[] { "0", "1" }; @@ -177,6 +180,13 @@ public final class DumpUtils { printArrayEntry(pw, STATE_TAGS, state, 1); } + public static void printProcStateTagProto(ProtoOutputStream proto, long screenId, long memId, + long stateId, int state) { + state = printProto(proto, screenId, ADJ_SCREEN_TAGS, state, ADJ_SCREEN_MOD * STATE_COUNT); + state = printProto(proto, memId, ADJ_MEM_TAGS, state, STATE_COUNT); + printProto(proto, stateId, STATE_TAGS, state, 1); + } + public static void printAdjTag(PrintWriter pw, int state) { state = printArrayEntry(pw, ADJ_SCREEN_TAGS, state, ADJ_SCREEN_MOD); printArrayEntry(pw, ADJ_MEM_TAGS, state, 1); @@ -352,6 +362,15 @@ public final class DumpUtils { return value - index*mod; } + public static int printProto(ProtoOutputStream proto, long fieldId, String[] array, int value, int mod) { + int index = value/mod; + if (index >= 0 && index < array.length) { + // Valid state enum number starts at 1, 0 stands for unknown. + proto.write(fieldId, index + 1); + } // else enum default is always zero in proto3 + return value - index*mod; + } + public static String collapseString(String pkgName, String itemName) { if (itemName.startsWith(pkgName)) { final int ITEMLEN = itemName.length(); diff --git a/core/java/com/android/internal/app/procstats/ProcessState.java b/core/java/com/android/internal/app/procstats/ProcessState.java index e0a40536f255..6fb02b162309 100644 --- a/core/java/com/android/internal/app/procstats/ProcessState.java +++ b/core/java/com/android/internal/app/procstats/ProcessState.java @@ -21,14 +21,19 @@ import android.os.Parcelable; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; +import android.service.pm.PackageProto; +import android.service.procstats.ProcessStatsProto; import android.text.format.DateFormat; import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.util.Log; +import android.util.LongSparseArray; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; +import android.util.proto.ProtoUtils; import com.android.internal.app.procstats.ProcessStats; import com.android.internal.app.procstats.ProcessStats.PackageState; @@ -69,6 +74,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; public final class ProcessState { @@ -95,6 +103,7 @@ public final class ProcessState { STATE_LAST_ACTIVITY, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY STATE_CACHED_ACTIVITY, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY STATE_CACHED_ACTIVITY_CLIENT, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT + STATE_CACHED_ACTIVITY, // ActivityManager.PROCESS_STATE_CACHED_RECENT STATE_CACHED_EMPTY, // ActivityManager.PROCESS_STATE_CACHED_EMPTY }; @@ -129,7 +138,7 @@ public final class ProcessState { private final String mName; private final String mPackage; private final int mUid; - private final int mVersion; + private final long mVersion; private final DurationsTable mDurations; private final PssTable mPssTable; @@ -162,7 +171,7 @@ public final class ProcessState { * Create a new top-level process state, for the initial case where there is only * a single package running in a process. The initial state is not running. */ - public ProcessState(ProcessStats processStats, String pkg, int uid, int vers, String name) { + public ProcessState(ProcessStats processStats, String pkg, int uid, long vers, String name) { mStats = processStats; mName = name; mCommonProcess = this; @@ -178,7 +187,7 @@ public final class ProcessState { * state. The current running state of the top-level process is also copied, * marked as started running at 'now'. */ - public ProcessState(ProcessState commonProcess, String pkg, int uid, int vers, String name, + public ProcessState(ProcessState commonProcess, String pkg, int uid, long vers, String name, long now) { mStats = commonProcess.mStats; mName = name; @@ -230,7 +239,7 @@ public final class ProcessState { return mUid; } - public int getVersion() { + public long getVersion() { return mVersion; } @@ -538,7 +547,7 @@ public final class ProcessState { // The array map is still pointing to a common process state // that is now shared across packages. Update it to point to // the new per-package state. - SparseArray<PackageState> vpkg = mStats.mPackages.get(pkgName, mUid); + LongSparseArray<PackageState> vpkg = mStats.mPackages.get(pkgName, mUid); if (vpkg == null) { throw new IllegalStateException("Didn't find package " + pkgName + " / " + mUid); @@ -576,7 +585,7 @@ public final class ProcessState { // The array map is still pointing to a common process state // that is now shared across packages. Update it to point to // the new per-package state. - SparseArray<PackageState> vpkg = mStats.mPackages.get(pkgList.keyAt(index), + LongSparseArray<PackageState> vpkg = mStats.mPackages.get(pkgList.keyAt(index), proc.mUid); if (vpkg == null) { throw new IllegalStateException("No existing package " @@ -1029,7 +1038,7 @@ public final class ProcessState { } } - public void dumpPackageProcCheckin(PrintWriter pw, String pkgName, int uid, int vers, + public void dumpPackageProcCheckin(PrintWriter pw, String pkgName, int uid, long vers, String itemName, long now) { pw.print("pkgproc,"); pw.print(pkgName); @@ -1157,6 +1166,7 @@ public final class ProcessState { } } + @Override public String toString() { StringBuilder sb = new StringBuilder(128); sb.append("ProcessState{").append(Integer.toHexString(System.identityHashCode(this))) @@ -1167,4 +1177,76 @@ public final class ProcessState { sb.append("}"); return sb.toString(); } + + public void toProto(ProtoOutputStream proto, String procName, int uid, long now) { + proto.write(ProcessStatsProto.PROCESS, procName); + proto.write(ProcessStatsProto.UID, uid); + if (mNumExcessiveCpu > 0 || mNumCachedKill > 0 ) { + final long killToken = proto.start(ProcessStatsProto.KILL); + proto.write(ProcessStatsProto.Kill.CPU, mNumExcessiveCpu); + proto.write(ProcessStatsProto.Kill.CACHED, mNumCachedKill); + ProtoUtils.toAggStatsProto(proto, ProcessStatsProto.Kill.CACHED_PSS, + mMinCachedKillPss, mAvgCachedKillPss, mMaxCachedKillPss); + proto.end(killToken); + } + + // Group proc stats by type (screen state + mem state + process state) + Map<Integer, Long> durationByState = new HashMap<>(); + boolean didCurState = false; + for (int i=0; i<mDurations.getKeyCount(); i++) { + final int key = mDurations.getKeyAt(i); + final int type = SparseMappingTable.getIdFromKey(key); + long time = mDurations.getValue(key); + if (mCurState == type) { + didCurState = true; + time += now - mStartTime; + } + durationByState.put(type, time); + } + if (!didCurState && mCurState != STATE_NOTHING) { + durationByState.put(mCurState, now - mStartTime); + } + + for (int i=0; i<mPssTable.getKeyCount(); i++) { + final int key = mPssTable.getKeyAt(i); + final int type = SparseMappingTable.getIdFromKey(key); + if (!durationByState.containsKey(type)) { + // state without duration should not have stats! + continue; + } + final long stateToken = proto.start(ProcessStatsProto.STATES); + DumpUtils.printProcStateTagProto(proto, + ProcessStatsProto.State.SCREEN_STATE, + ProcessStatsProto.State.MEMORY_STATE, + ProcessStatsProto.State.PROCESS_STATE, + type); + + long duration = durationByState.get(type); + durationByState.remove(type); // remove the key since it is already being dumped. + proto.write(ProcessStatsProto.State.DURATION_MS, duration); + + proto.write(ProcessStatsProto.State.SAMPLE_SIZE, mPssTable.getValue(key, PSS_SAMPLE_COUNT)); + ProtoUtils.toAggStatsProto(proto, ProcessStatsProto.State.PSS, + mPssTable.getValue(key, PSS_MINIMUM), + mPssTable.getValue(key, PSS_AVERAGE), + mPssTable.getValue(key, PSS_MAXIMUM)); + ProtoUtils.toAggStatsProto(proto, ProcessStatsProto.State.USS, + mPssTable.getValue(key, PSS_USS_MINIMUM), + mPssTable.getValue(key, PSS_USS_AVERAGE), + mPssTable.getValue(key, PSS_USS_MAXIMUM)); + + proto.end(stateToken); + } + + for (Map.Entry<Integer, Long> entry : durationByState.entrySet()) { + final long stateToken = proto.start(ProcessStatsProto.STATES); + DumpUtils.printProcStateTagProto(proto, + ProcessStatsProto.State.SCREEN_STATE, + ProcessStatsProto.State.MEMORY_STATE, + ProcessStatsProto.State.PROCESS_STATE, + entry.getKey()); + proto.write(ProcessStatsProto.State.DURATION_MS, entry.getValue()); + proto.end(stateToken); + } + } } diff --git a/core/java/com/android/internal/app/procstats/ProcessStats.java b/core/java/com/android/internal/app/procstats/ProcessStats.java index 35b53c2298bb..2ce7936d157b 100644 --- a/core/java/com/android/internal/app/procstats/ProcessStats.java +++ b/core/java/com/android/internal/app/procstats/ProcessStats.java @@ -22,14 +22,17 @@ import android.os.Parcelable; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; +import android.service.procstats.ProcessStatsSectionProto; import android.text.format.DateFormat; import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.util.Log; +import android.util.LongSparseArray; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; import com.android.internal.app.ProcessMap; import com.android.internal.app.procstats.DurationsTable; @@ -155,7 +158,7 @@ public final class ProcessStats implements Parcelable { }; // Current version of the parcel format. - private static final int PARCEL_VERSION = 21; + private static final int PARCEL_VERSION = 22; // In-memory Parcel magic number, used to detect attempts to unmarshall bad data private static final int MAGIC = 0x50535454; @@ -163,9 +166,8 @@ public final class ProcessStats implements Parcelable { public String mTimePeriodStartClockStr; public int mFlags; - public final ProcessMap<SparseArray<PackageState>> mPackages - = new ProcessMap<SparseArray<PackageState>>(); - public final ProcessMap<ProcessState> mProcesses = new ProcessMap<ProcessState>(); + public final ProcessMap<LongSparseArray<PackageState>> mPackages = new ProcessMap<>(); + public final ProcessMap<ProcessState> mProcesses = new ProcessMap<>(); public final long[] mMemFactorDurations = new long[ADJ_COUNT]; public int mMemFactor = STATE_NOTHING; @@ -216,15 +218,16 @@ public final class ProcessStats implements Parcelable { } public void add(ProcessStats other) { - ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = other.mPackages.getMap(); + ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = + other.mPackages.getMap(); for (int ip=0; ip<pkgMap.size(); ip++) { final String pkgName = pkgMap.keyAt(ip); - final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); + final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip); for (int iu=0; iu<uids.size(); iu++) { final int uid = uids.keyAt(iu); - final SparseArray<PackageState> versions = uids.valueAt(iu); + final LongSparseArray<PackageState> versions = uids.valueAt(iu); for (int iv=0; iv<versions.size(); iv++) { - final int vers = versions.keyAt(iv); + final long vers = versions.keyAt(iv); final PackageState otherState = versions.valueAt(iv); final int NPROCS = otherState.mProcesses.size(); final int NSRVS = otherState.mServices.size(); @@ -267,7 +270,7 @@ public final class ProcessStats implements Parcelable { ProcessState otherProc = uids.valueAt(iu); final String name = otherProc.getName(); final String pkg = otherProc.getPackage(); - final int vers = otherProc.getVersion(); + final long vers = otherProc.getVersion(); ProcessState thisProc = mProcesses.get(name, uid); if (DEBUG) Slog.d(TAG, "Adding uid " + uid + " proc " + name); if (thisProc == null) { @@ -418,11 +421,12 @@ public final class ProcessStats implements Parcelable { // Next reset or prune all per-package processes, and for the ones that are reset // track this back to the common processes. - final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); + final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = + mPackages.getMap(); for (int ip=pkgMap.size()-1; ip>=0; ip--) { - final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); + final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip); for (int iu=uids.size()-1; iu>=0; iu--) { - final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu); for (int iv=vpkgs.size()-1; iv>=0; iv--) { final PackageState pkgState = vpkgs.valueAt(iv); for (int iproc=pkgState.mProcesses.size()-1; iproc>=0; iproc--) { @@ -725,13 +729,14 @@ public final class ProcessStats implements Parcelable { uids.valueAt(iu).commitStateTime(now); } } - final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); + final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = + mPackages.getMap(); final int NPKG = pkgMap.size(); for (int ip=0; ip<NPKG; ip++) { - final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); + final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip); final int NUID = uids.size(); for (int iu=0; iu<NUID; iu++) { - final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu); final int NVERS = vpkgs.size(); for (int iv=0; iv<NVERS; iv++) { PackageState pkgState = vpkgs.valueAt(iv); @@ -779,23 +784,23 @@ public final class ProcessStats implements Parcelable { out.writeInt(uids.keyAt(iu)); final ProcessState proc = uids.valueAt(iu); writeCommonString(out, proc.getPackage()); - out.writeInt(proc.getVersion()); + out.writeLong(proc.getVersion()); proc.writeToParcel(out, now); } } out.writeInt(NPKG); for (int ip=0; ip<NPKG; ip++) { writeCommonString(out, pkgMap.keyAt(ip)); - final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); + final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip); final int NUID = uids.size(); out.writeInt(NUID); for (int iu=0; iu<NUID; iu++) { out.writeInt(uids.keyAt(iu)); - final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu); final int NVERS = vpkgs.size(); out.writeInt(NVERS); for (int iv=0; iv<NVERS; iv++) { - out.writeInt(vpkgs.keyAt(iv)); + out.writeLong(vpkgs.keyAt(iv)); final PackageState pkgState = vpkgs.valueAt(iv); final int NPROCS = pkgState.mProcesses.size(); out.writeInt(NPROCS); @@ -961,7 +966,7 @@ public final class ProcessStats implements Parcelable { mReadError = "bad process package name"; return; } - final int vers = in.readInt(); + final long vers = in.readLong(); ProcessState proc = hadData ? mProcesses.get(procName, uid) : null; if (proc != null) { if (!proc.readFromParcel(in, false)) { @@ -1012,11 +1017,11 @@ public final class ProcessStats implements Parcelable { } while (NVERS > 0) { NVERS--; - final int vers = in.readInt(); + final long vers = in.readLong(); PackageState pkgState = new PackageState(pkgName, uid); - SparseArray<PackageState> vpkg = mPackages.get(pkgName, uid); + LongSparseArray<PackageState> vpkg = mPackages.get(pkgName, uid); if (vpkg == null) { - vpkg = new SparseArray<PackageState>(); + vpkg = new LongSparseArray<>(); mPackages.put(pkgName, uid, vpkg); } vpkg.put(vers, pkgState); @@ -1115,10 +1120,10 @@ public final class ProcessStats implements Parcelable { if (DEBUG_PARCEL) Slog.d(TAG, "Successfully read procstats!"); } - public PackageState getPackageStateLocked(String packageName, int uid, int vers) { - SparseArray<PackageState> vpkg = mPackages.get(packageName, uid); + public PackageState getPackageStateLocked(String packageName, int uid, long vers) { + LongSparseArray<PackageState> vpkg = mPackages.get(packageName, uid); if (vpkg == null) { - vpkg = new SparseArray<PackageState>(); + vpkg = new LongSparseArray<PackageState>(); mPackages.put(packageName, uid, vpkg); } PackageState as = vpkg.get(vers); @@ -1130,7 +1135,7 @@ public final class ProcessStats implements Parcelable { return as; } - public ProcessState getProcessStateLocked(String packageName, int uid, int vers, + public ProcessState getProcessStateLocked(String packageName, int uid, long vers, String processName) { final PackageState pkgState = getPackageStateLocked(packageName, uid, vers); ProcessState ps = pkgState.mProcesses.get(processName); @@ -1200,7 +1205,7 @@ public final class ProcessStats implements Parcelable { return ps; } - public ServiceState getServiceStateLocked(String packageName, int uid, int vers, + public ServiceState getServiceStateLocked(String packageName, int uid, long vers, String processName, String className) { final ProcessStats.PackageState as = getPackageStateLocked(packageName, uid, vers); ServiceState ss = as.mServices.get(className); @@ -1226,16 +1231,16 @@ public final class ProcessStats implements Parcelable { mSysMemUsage.dump(pw, " ", ALL_SCREEN_ADJ, ALL_MEM_ADJ); sepNeeded = true; } - ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); + ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = mPackages.getMap(); boolean printedHeader = false; for (int ip=0; ip<pkgMap.size(); ip++) { final String pkgName = pkgMap.keyAt(ip); - final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); + final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip); for (int iu=0; iu<uids.size(); iu++) { final int uid = uids.keyAt(iu); - final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu); for (int iv=0; iv<vpkgs.size(); iv++) { - final int vers = vpkgs.keyAt(iv); + final long vers = vpkgs.keyAt(iv); final PackageState pkgState = vpkgs.valueAt(iv); final int NPROCS = pkgState.mProcesses.size(); final int NSRVS = pkgState.mServices.size(); @@ -1529,12 +1534,13 @@ public final class ProcessStats implements Parcelable { int[] procStates, int sortProcStates[], long now, String reqPackage, boolean activeOnly) { final ArraySet<ProcessState> foundProcs = new ArraySet<ProcessState>(); - final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); + final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = + mPackages.getMap(); for (int ip=0; ip<pkgMap.size(); ip++) { final String pkgName = pkgMap.keyAt(ip); - final SparseArray<SparseArray<PackageState>> procs = pkgMap.valueAt(ip); + final SparseArray<LongSparseArray<PackageState>> procs = pkgMap.valueAt(ip); for (int iu=0; iu<procs.size(); iu++) { - final SparseArray<PackageState> vpkgs = procs.valueAt(iu); + final LongSparseArray<PackageState> vpkgs = procs.valueAt(iu); final int NVERS = vpkgs.size(); for (int iv=0; iv<NVERS; iv++) { final PackageState state = vpkgs.valueAt(iv); @@ -1569,7 +1575,8 @@ public final class ProcessStats implements Parcelable { public void dumpCheckinLocked(PrintWriter pw, String reqPackage) { final long now = SystemClock.uptimeMillis(); - final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); + final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = + mPackages.getMap(); pw.println("vers,5"); pw.print("period,"); pw.print(mTimePeriodStartClockStr); pw.print(","); pw.print(mTimePeriodStartRealtime); pw.print(","); @@ -1600,12 +1607,12 @@ public final class ProcessStats implements Parcelable { if (reqPackage != null && !reqPackage.equals(pkgName)) { continue; } - final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); + final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip); for (int iu=0; iu<uids.size(); iu++) { final int uid = uids.keyAt(iu); - final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu); for (int iv=0; iv<vpkgs.size(); iv++) { - final int vers = vpkgs.keyAt(iv); + final long vers = vpkgs.keyAt(iv); final PackageState pkgState = vpkgs.valueAt(iv); final int NPROCS = pkgState.mProcesses.size(); final int NSRVS = pkgState.mServices.size(); @@ -1706,12 +1713,53 @@ public final class ProcessStats implements Parcelable { } } + public void toProto(ProtoOutputStream proto, long now) { + final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = + mPackages.getMap(); + + proto.write(ProcessStatsSectionProto.START_REALTIME_MS, mTimePeriodStartRealtime); + proto.write(ProcessStatsSectionProto.END_REALTIME_MS, + mRunning ? SystemClock.elapsedRealtime() : mTimePeriodEndRealtime); + proto.write(ProcessStatsSectionProto.START_UPTIME_MS, mTimePeriodStartUptime); + proto.write(ProcessStatsSectionProto.END_UPTIME_MS, mTimePeriodEndUptime); + proto.write(ProcessStatsSectionProto.RUNTIME, mRuntime); + proto.write(ProcessStatsSectionProto.HAS_SWAPPED_PSS, mHasSwappedOutPss); + boolean partial = true; + if ((mFlags&FLAG_SHUTDOWN) != 0) { + proto.write(ProcessStatsSectionProto.STATUS, ProcessStatsSectionProto.STATUS_SHUTDOWN); + partial = false; + } + if ((mFlags&FLAG_SYSPROPS) != 0) { + proto.write(ProcessStatsSectionProto.STATUS, ProcessStatsSectionProto.STATUS_SYSPROPS); + partial = false; + } + if ((mFlags&FLAG_COMPLETE) != 0) { + proto.write(ProcessStatsSectionProto.STATUS, ProcessStatsSectionProto.STATUS_COMPLETE); + partial = false; + } + if (partial) { + proto.write(ProcessStatsSectionProto.STATUS, ProcessStatsSectionProto.STATUS_PARTIAL); + } + + ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap(); + for (int ip=0; ip<procMap.size(); ip++) { + String procName = procMap.keyAt(ip); + SparseArray<ProcessState> uids = procMap.valueAt(ip); + for (int iu=0; iu<uids.size(); iu++) { + final int uid = uids.keyAt(iu); + final ProcessState procState = uids.valueAt(iu); + final long processStateToken = proto.start(ProcessStatsSectionProto.PROCESS_STATS); + procState.toProto(proto, procName, uid, now); + proto.end(processStateToken); + } + } + } final public static class ProcessStateHolder { - public final int appVersion; + public final long appVersion; public ProcessState state; - public ProcessStateHolder(int _appVersion) { + public ProcessStateHolder(long _appVersion) { appVersion = _appVersion; } } diff --git a/core/java/com/android/internal/app/procstats/ServiceState.java b/core/java/com/android/internal/app/procstats/ServiceState.java index 2e11c43872f6..650de2ea2b68 100644 --- a/core/java/com/android/internal/app/procstats/ServiceState.java +++ b/core/java/com/android/internal/app/procstats/ServiceState.java @@ -441,7 +441,7 @@ public final class ServiceState { return totalTime; } - public void dumpTimesCheckin(PrintWriter pw, String pkgName, int uid, int vers, + public void dumpTimesCheckin(PrintWriter pw, String pkgName, int uid, long vers, String serviceName, long now) { dumpTimeCheckin(pw, "pkgsvc-run", pkgName, uid, vers, serviceName, ServiceState.SERVICE_RUN, mRunCount, mRunState, mRunStartTime, now); @@ -454,7 +454,7 @@ public final class ServiceState { } private void dumpTimeCheckin(PrintWriter pw, String label, String packageName, - int uid, int vers, String serviceName, int serviceType, int opCount, + int uid, long vers, String serviceName, int serviceType, int opCount, int curState, long curStartTime, long now) { if (opCount <= 0) { return; diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl index caf35b39b324..a4da6b9cea18 100644 --- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl +++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl @@ -26,6 +26,8 @@ import com.android.internal.appwidget.IAppWidgetHost; import android.os.Bundle; import android.os.IBinder; import android.widget.RemoteViews; +import android.app.IApplicationThread; +import android.app.IServiceConnection; /** {@hide} */ interface IAppWidgetService { @@ -62,9 +64,9 @@ interface IAppWidgetService { void setBindAppWidgetPermission(in String packageName, int userId, in boolean permission); boolean bindAppWidgetId(in String callingPackage, int appWidgetId, int providerProfileId, in ComponentName providerComponent, in Bundle options); - void bindRemoteViewsService(String callingPackage, int appWidgetId, in Intent intent, - in IBinder connection); - void unbindRemoteViewsService(String callingPackage, int appWidgetId, in Intent intent); + boolean bindRemoteViewsService(String callingPackage, int appWidgetId, in Intent intent, + IApplicationThread caller, IBinder token, IServiceConnection connection, int flags); + int[] getAppWidgetIds(in ComponentName providerComponent); boolean isBoundWidgetPackage(String packageName, int userId); boolean requestPinAppWidget(String packageName, in ComponentName providerComponent, diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl index 98f5bea4a869..147438cf47a5 100644 --- a/core/java/com/android/internal/backup/IBackupTransport.aidl +++ b/core/java/com/android/internal/backup/IBackupTransport.aidl @@ -94,7 +94,7 @@ interface IBackupTransport { * "live" backup services without interfering with the live bookkeeping. The * returned string should be a name that is expected to be unambiguous among all * available backup transports; the name of the class implementing the transport - * is a good choice. + * is a good choice. This MUST be constant. * * @return A unique name, suitable for use as a file or directory name, that the * Backup Manager could use to disambiguate state files associated with diff --git a/core/java/com/android/internal/colorextraction/types/Tonal.java b/core/java/com/android/internal/colorextraction/types/Tonal.java index e6ef10b3b7c6..71baaf177eac 100644 --- a/core/java/com/android/internal/colorextraction/types/Tonal.java +++ b/core/java/com/android/internal/colorextraction/types/Tonal.java @@ -51,9 +51,11 @@ public class Tonal implements ExtractionType { private static final boolean DEBUG = true; + public static final int THRESHOLD_COLOR_LIGHT = 0xffe0e0e0; public static final int MAIN_COLOR_LIGHT = 0xffe0e0e0; public static final int SECONDARY_COLOR_LIGHT = 0xff9e9e9e; - public static final int MAIN_COLOR_DARK = 0xff212121; + public static final int THRESHOLD_COLOR_DARK = 0xff212121; + public static final int MAIN_COLOR_DARK = 0xff000000; public static final int SECONDARY_COLOR_DARK = 0xff000000; private final TonalPalette mGreyPalette; @@ -197,12 +199,12 @@ public class Tonal implements ExtractionType { // light fallback or darker than our dark fallback. ColorUtils.colorToHSL(mainColor, mTmpHSL); final float mainLuminosity = mTmpHSL[2]; - ColorUtils.colorToHSL(MAIN_COLOR_LIGHT, mTmpHSL); + ColorUtils.colorToHSL(THRESHOLD_COLOR_LIGHT, mTmpHSL); final float lightLuminosity = mTmpHSL[2]; if (mainLuminosity > lightLuminosity) { return false; } - ColorUtils.colorToHSL(MAIN_COLOR_DARK, mTmpHSL); + ColorUtils.colorToHSL(THRESHOLD_COLOR_DARK, mTmpHSL); final float darkLuminosity = mTmpHSL[2]; if (mainLuminosity < darkLuminosity) { return false; diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java index 4b80a5ff03de..d49d572310d7 100644 --- a/core/java/com/android/internal/content/FileSystemProvider.java +++ b/core/java/com/android/internal/content/FileSystemProvider.java @@ -52,6 +52,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -73,12 +74,9 @@ public abstract class FileSystemProvider extends DocumentsProvider { private Handler mHandler; - private static final String MIMETYPE_JPEG = "image/jpeg"; - private static final String MIMETYPE_JPG = "image/jpg"; - - + private static final String MIMETYPE_OCTET_STREAM = "application/octet-stream"; protected abstract File getFileForDocId(String docId, boolean visible) throws FileNotFoundException; @@ -112,27 +110,41 @@ public abstract class FileSystemProvider extends DocumentsProvider { } @Override - public @Nullable Bundle getDocumentMetadata(String documentId, @Nullable String[] tags) + public @Nullable Bundle getDocumentMetadata(String documentId) throws FileNotFoundException { File file = getFileForDocId(documentId); - if (!(file.exists() && file.isFile() && file.canRead())) { - return Bundle.EMPTY; - } - String filePath = file.getAbsolutePath(); - Bundle metadata = new Bundle(); - if (getTypeForFile(file).equals(MIMETYPE_JPEG) - || getTypeForFile(file).equals(MIMETYPE_JPG)) { - FileInputStream stream = new FileInputStream(filePath); - try { - MetadataReader.getMetadata(metadata, stream, getTypeForFile(file), tags); - return metadata; - } catch (IOException e) { - Log.e(TAG, "An error occurred retrieving the metadata", e); - } finally { - IoUtils.closeQuietly(stream); - } + + if (!file.exists()) { + throw new FileNotFoundException("Can't find the file for documentId: " + documentId); + } + + if (!file.isFile()) { + Log.w(TAG, "Can't stream non-regular file. Returning empty metadata."); + return null; + } + + if (!file.canRead()) { + Log.w(TAG, "Can't stream non-readable file. Returning empty metadata."); + return null; + } + + String mimeType = getTypeForFile(file); + if (!MetadataReader.isSupportedMimeType(mimeType)) { + return null; + } + + InputStream stream = null; + try { + Bundle metadata = new Bundle(); + stream = new FileInputStream(file.getAbsolutePath()); + MetadataReader.getMetadata(metadata, stream, mimeType, null); + return metadata; + } catch (IOException e) { + Log.e(TAG, "An error occurred retrieving the metadata", e); + return null; + } finally { + IoUtils.closeQuietly(stream); } - return null; } protected final List<String> findDocumentPath(File parent, File doc) @@ -456,6 +468,10 @@ public abstract class FileSystemProvider extends DocumentsProvider { flags |= Document.FLAG_SUPPORTS_THUMBNAIL; } + if (typeSupportsMetadata(mimeType)) { + flags |= Document.FLAG_SUPPORTS_METADATA; + } + final RowBuilder row = result.newRow(); row.add(Document.COLUMN_DOCUMENT_ID, docId); row.add(Document.COLUMN_DISPLAY_NAME, displayName); @@ -481,6 +497,10 @@ public abstract class FileSystemProvider extends DocumentsProvider { } } + protected boolean typeSupportsMetadata(String mimeType) { + return MetadataReader.isSupportedMimeType(mimeType); + } + private static String getTypeForName(String name) { final int lastDot = name.lastIndexOf('.'); if (lastDot >= 0) { @@ -491,7 +511,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { } } - return "application/octet-stream"; + return MIMETYPE_OCTET_STREAM; } protected final File getFileForDocId(String docId) throws FileNotFoundException { diff --git a/core/java/com/android/internal/content/NativeLibraryHelper.java b/core/java/com/android/internal/content/NativeLibraryHelper.java index 83b7d2f948f8..a1e6fd8e22f9 100644 --- a/core/java/com/android/internal/content/NativeLibraryHelper.java +++ b/core/java/com/android/internal/content/NativeLibraryHelper.java @@ -43,6 +43,7 @@ import dalvik.system.VMRuntime; import java.io.Closeable; import java.io.File; +import java.io.FileDescriptor; import java.io.IOException; import java.util.List; @@ -118,6 +119,17 @@ public class NativeLibraryHelper { return new Handle(apkHandles, multiArch, extractNativeLibs, debuggable); } + public static Handle createFd(PackageLite lite, FileDescriptor fd) throws IOException { + final long[] apkHandles = new long[1]; + final String path = lite.baseCodePath; + apkHandles[0] = nativeOpenApkFd(fd, path); + if (apkHandles[0] == 0) { + throw new IOException("Unable to open APK " + path + " from fd " + fd); + } + + return new Handle(apkHandles, lite.multiArch, lite.extractNativeLibs, lite.debuggable); + } + Handle(long[] apkHandles, boolean multiArch, boolean extractNativeLibs, boolean debuggable) { this.apkHandles = apkHandles; @@ -152,6 +164,7 @@ public class NativeLibraryHelper { } private static native long nativeOpenApk(String path); + private static native long nativeOpenApkFd(FileDescriptor fd, String debugPath); private static native void nativeClose(long handle); private static native long nativeSumNativeBinaries(long handle, String cpuAbi, diff --git a/core/java/com/android/internal/content/PackageHelper.java b/core/java/com/android/internal/content/PackageHelper.java index e923223de81d..e765ab1eae2f 100644 --- a/core/java/com/android/internal/content/PackageHelper.java +++ b/core/java/com/android/internal/content/PackageHelper.java @@ -16,7 +16,6 @@ package com.android.internal.content; -import static android.net.TrafficStats.MB_IN_BYTES; import static android.os.storage.VolumeInfo.ID_PRIVATE_INTERNAL; import android.content.Context; @@ -27,13 +26,11 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageParser.PackageLite; import android.os.Environment; -import android.os.FileUtils; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.os.storage.IStorageManager; import android.os.storage.StorageManager; -import android.os.storage.StorageResultCode; import android.os.storage.StorageVolume; import android.os.storage.VolumeInfo; import android.provider.Settings; @@ -45,15 +42,10 @@ import com.android.internal.annotations.VisibleForTesting; import libcore.io.IoUtils; import java.io.File; -import java.io.FileOutputStream; +import java.io.FileDescriptor; import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; import java.util.Objects; import java.util.UUID; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; /** * Constants used internally between the PackageManager @@ -72,7 +64,6 @@ public class PackageHelper { public static final int RECOMMEND_FAILED_INVALID_URI = -6; public static final int RECOMMEND_FAILED_VERSION_DOWNGRADE = -7; - private static final boolean localLOGV = false; private static final String TAG = "PackageHelper"; // App installation location settings values public static final int APP_INSTALL_AUTO = 0; @@ -91,259 +82,6 @@ public class PackageHelper { } } - public static String createSdDir(long sizeBytes, String cid, String sdEncKey, int uid, - boolean isExternal) { - // Round up to nearest MB, plus another MB for filesystem overhead - final int sizeMb = (int) ((sizeBytes + MB_IN_BYTES) / MB_IN_BYTES) + 1; - try { - IStorageManager storageManager = getStorageManager(); - - if (localLOGV) - Log.i(TAG, "Size of container " + sizeMb + " MB"); - - int rc = storageManager.createSecureContainer(cid, sizeMb, "ext4", sdEncKey, uid, - isExternal); - if (rc != StorageResultCode.OperationSucceeded) { - Log.e(TAG, "Failed to create secure container " + cid); - return null; - } - String cachePath = storageManager.getSecureContainerPath(cid); - if (localLOGV) Log.i(TAG, "Created secure container " + cid + - " at " + cachePath); - return cachePath; - } catch (RemoteException e) { - Log.e(TAG, "StorageManagerService running?"); - } - return null; - } - - public static boolean resizeSdDir(long sizeBytes, String cid, String sdEncKey) { - // Round up to nearest MB, plus another MB for filesystem overhead - final int sizeMb = (int) ((sizeBytes + MB_IN_BYTES) / MB_IN_BYTES) + 1; - try { - IStorageManager storageManager = getStorageManager(); - int rc = storageManager.resizeSecureContainer(cid, sizeMb, sdEncKey); - if (rc == StorageResultCode.OperationSucceeded) { - return true; - } - } catch (RemoteException e) { - Log.e(TAG, "StorageManagerService running?"); - } - Log.e(TAG, "Failed to create secure container " + cid); - return false; - } - - public static String mountSdDir(String cid, String key, int ownerUid) { - return mountSdDir(cid, key, ownerUid, true); - } - - public static String mountSdDir(String cid, String key, int ownerUid, boolean readOnly) { - try { - int rc = getStorageManager().mountSecureContainer(cid, key, ownerUid, readOnly); - if (rc != StorageResultCode.OperationSucceeded) { - Log.i(TAG, "Failed to mount container " + cid + " rc : " + rc); - return null; - } - return getStorageManager().getSecureContainerPath(cid); - } catch (RemoteException e) { - Log.e(TAG, "StorageManagerService running?"); - } - return null; - } - - public static boolean unMountSdDir(String cid) { - try { - int rc = getStorageManager().unmountSecureContainer(cid, true); - if (rc != StorageResultCode.OperationSucceeded) { - Log.e(TAG, "Failed to unmount " + cid + " with rc " + rc); - return false; - } - return true; - } catch (RemoteException e) { - Log.e(TAG, "StorageManagerService running?"); - } - return false; - } - - public static boolean renameSdDir(String oldId, String newId) { - try { - int rc = getStorageManager().renameSecureContainer(oldId, newId); - if (rc != StorageResultCode.OperationSucceeded) { - Log.e(TAG, "Failed to rename " + oldId + " to " + - newId + "with rc " + rc); - return false; - } - return true; - } catch (RemoteException e) { - Log.i(TAG, "Failed ot rename " + oldId + " to " + newId + - " with exception : " + e); - } - return false; - } - - public static String getSdDir(String cid) { - try { - return getStorageManager().getSecureContainerPath(cid); - } catch (RemoteException e) { - Log.e(TAG, "Failed to get container path for " + cid + - " with exception " + e); - } - return null; - } - - public static String getSdFilesystem(String cid) { - try { - return getStorageManager().getSecureContainerFilesystemPath(cid); - } catch (RemoteException e) { - Log.e(TAG, "Failed to get container path for " + cid + - " with exception " + e); - } - return null; - } - - public static boolean finalizeSdDir(String cid) { - try { - int rc = getStorageManager().finalizeSecureContainer(cid); - if (rc != StorageResultCode.OperationSucceeded) { - Log.i(TAG, "Failed to finalize container " + cid); - return false; - } - return true; - } catch (RemoteException e) { - Log.e(TAG, "Failed to finalize container " + cid + - " with exception " + e); - } - return false; - } - - public static boolean destroySdDir(String cid) { - try { - if (localLOGV) Log.i(TAG, "Forcibly destroying container " + cid); - int rc = getStorageManager().destroySecureContainer(cid, true); - if (rc != StorageResultCode.OperationSucceeded) { - Log.i(TAG, "Failed to destroy container " + cid); - return false; - } - return true; - } catch (RemoteException e) { - Log.e(TAG, "Failed to destroy container " + cid + - " with exception " + e); - } - return false; - } - - public static String[] getSecureContainerList() { - try { - return getStorageManager().getSecureContainerList(); - } catch (RemoteException e) { - Log.e(TAG, "Failed to get secure container list with exception" + - e); - } - return null; - } - - public static boolean isContainerMounted(String cid) { - try { - return getStorageManager().isSecureContainerMounted(cid); - } catch (RemoteException e) { - Log.e(TAG, "Failed to find out if container " + cid + " mounted"); - } - return false; - } - - /** - * Extract public files for the single given APK. - */ - public static long extractPublicFiles(File apkFile, File publicZipFile) - throws IOException { - final FileOutputStream fstr; - final ZipOutputStream publicZipOutStream; - - if (publicZipFile == null) { - fstr = null; - publicZipOutStream = null; - } else { - fstr = new FileOutputStream(publicZipFile); - publicZipOutStream = new ZipOutputStream(fstr); - Log.d(TAG, "Extracting " + apkFile + " to " + publicZipFile); - } - - long size = 0L; - - try { - final ZipFile privateZip = new ZipFile(apkFile.getAbsolutePath()); - try { - // Copy manifest, resources.arsc and res directory to public zip - for (final ZipEntry zipEntry : Collections.list(privateZip.entries())) { - final String zipEntryName = zipEntry.getName(); - if ("AndroidManifest.xml".equals(zipEntryName) - || "resources.arsc".equals(zipEntryName) - || zipEntryName.startsWith("res/")) { - size += zipEntry.getSize(); - if (publicZipFile != null) { - copyZipEntry(zipEntry, privateZip, publicZipOutStream); - } - } - } - } finally { - try { privateZip.close(); } catch (IOException e) {} - } - - if (publicZipFile != null) { - publicZipOutStream.finish(); - publicZipOutStream.flush(); - FileUtils.sync(fstr); - publicZipOutStream.close(); - FileUtils.setPermissions(publicZipFile.getAbsolutePath(), FileUtils.S_IRUSR - | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IROTH, -1, -1); - } - } finally { - IoUtils.closeQuietly(publicZipOutStream); - } - - return size; - } - - private static void copyZipEntry(ZipEntry zipEntry, ZipFile inZipFile, - ZipOutputStream outZipStream) throws IOException { - byte[] buffer = new byte[4096]; - int num; - - ZipEntry newEntry; - if (zipEntry.getMethod() == ZipEntry.STORED) { - // Preserve the STORED method of the input entry. - newEntry = new ZipEntry(zipEntry); - } else { - // Create a new entry so that the compressed len is recomputed. - newEntry = new ZipEntry(zipEntry.getName()); - } - outZipStream.putNextEntry(newEntry); - - final InputStream data = inZipFile.getInputStream(zipEntry); - try { - while ((num = data.read(buffer)) > 0) { - outZipStream.write(buffer, 0, num); - } - outZipStream.flush(); - } finally { - IoUtils.closeQuietly(data); - } - } - - public static boolean fixSdPermissions(String cid, int gid, String filename) { - try { - int rc = getStorageManager().fixPermissionsSecureContainer(cid, gid, filename); - if (rc != StorageResultCode.OperationSucceeded) { - Log.i(TAG, "Failed to fixperms container " + cid); - return false; - } - return true; - } catch (RemoteException e) { - Log.e(TAG, "Failed to fixperms container " + cid + " with exception " + e); - } - return false; - } - /** * A group of external dependencies used in * {@link #resolveInstallVolume(Context, String, int, long)}. It can be backed by real values @@ -638,29 +376,43 @@ public class PackageHelper { return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE; } + @Deprecated public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, String abiOverride) throws IOException { + return calculateInstalledSize(pkg, abiOverride); + } + + public static long calculateInstalledSize(PackageLite pkg, String abiOverride) + throws IOException { + return calculateInstalledSize(pkg, abiOverride, null); + } + + public static long calculateInstalledSize(PackageLite pkg, String abiOverride, + FileDescriptor fd) throws IOException { NativeLibraryHelper.Handle handle = null; try { - handle = NativeLibraryHelper.Handle.create(pkg); - return calculateInstalledSize(pkg, handle, isForwardLocked, abiOverride); + handle = fd != null ? NativeLibraryHelper.Handle.createFd(pkg, fd) + : NativeLibraryHelper.Handle.create(pkg); + return calculateInstalledSize(pkg, handle, abiOverride); } finally { IoUtils.closeQuietly(handle); } } + @Deprecated + public static long calculateInstalledSize(PackageLite pkg, boolean isForwardLocked, + NativeLibraryHelper.Handle handle, String abiOverride) throws IOException { + return calculateInstalledSize(pkg, handle, abiOverride); + } + public static long calculateInstalledSize(PackageLite pkg, NativeLibraryHelper.Handle handle, - boolean isForwardLocked, String abiOverride) throws IOException { + String abiOverride) throws IOException { long sizeBytes = 0; // Include raw APKs, and possibly unpacked resources for (String codePath : pkg.getAllCodePaths()) { final File codeFile = new File(codePath); sizeBytes += codeFile.length(); - - if (isForwardLocked) { - sizeBytes += PackageHelper.extractPublicFiles(codeFile, null); - } } // Include all relevant native code diff --git a/core/java/com/android/internal/content/PdfUtils.java b/core/java/com/android/internal/content/PdfUtils.java deleted file mode 100644 index 1716d426b362..000000000000 --- a/core/java/com/android/internal/content/PdfUtils.java +++ /dev/null @@ -1,96 +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 com.android.internal.content; - -import android.annotation.Nullable; -import android.content.res.AssetFileDescriptor; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.pdf.PdfRenderer; -import android.os.AsyncTask; -import android.os.ParcelFileDescriptor; - -import libcore.io.IoUtils; -import libcore.io.Streams; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; - -/** - * Utils class for extracting PDF Thumbnails - */ -public final class PdfUtils { - - private PdfUtils() { - } - - /** - * Returns the front page of the pdf as a thumbnail - * @param file Given PDF File - * @param size Cropping of the front page. - * @return AssetFileDescriptor containing the thumbnail as a bitmap. - * @throws IOException if the file isn't a pdf or if the file doesn't exist. - */ - public static @Nullable AssetFileDescriptor openPdfThumbnail(File file, Point size) - throws IOException { - // Create the bitmap of the PDF's first page - ParcelFileDescriptor pdfDescriptor = - ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); - PdfRenderer renderer = new PdfRenderer(pdfDescriptor); - PdfRenderer.Page frontPage = renderer.openPage(0); - Bitmap thumbnail = Bitmap.createBitmap(size.x, size.y, - Bitmap.Config.ARGB_8888); - frontPage.render(thumbnail, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); - - // Create an AssetFileDescriptor that contains the Bitmap's information - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - // Quality is an integer that determines how much compression is used. - // However, this integer is ignored when using the PNG format - int quality = 100; - // The use of Bitmap.CompressFormat.JPEG leads to a black PDF background on the thumbnail - thumbnail.compress(Bitmap.CompressFormat.PNG, quality, out); - - final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - - final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createReliablePipe(); - new AsyncTask<Object, Object, Object>() { - @Override - protected Object doInBackground(Object... params) { - final FileOutputStream fos = new FileOutputStream(fds[1].getFileDescriptor()); - try { - Streams.copy(in, fos); - } catch (IOException e) { - throw new RuntimeException(e); - } - IoUtils.closeQuietly(fds[1]); - try { - pdfDescriptor.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - pdfDescriptor.close(); - return new AssetFileDescriptor(fds[0], 0, AssetFileDescriptor.UNKNOWN_LENGTH); - } - -} diff --git a/core/java/com/android/internal/content/ReferrerIntent.java b/core/java/com/android/internal/content/ReferrerIntent.java index 8d9a1cf9eee5..76dcc9bba91c 100644 --- a/core/java/com/android/internal/content/ReferrerIntent.java +++ b/core/java/com/android/internal/content/ReferrerIntent.java @@ -19,6 +19,8 @@ package com.android.internal.content; import android.content.Intent; import android.os.Parcel; +import java.util.Objects; + /** * Subclass of Intent that also contains referrer (as a package name) information. */ @@ -48,4 +50,21 @@ public class ReferrerIntent extends Intent { return new ReferrerIntent[size]; } }; + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof ReferrerIntent)) { + return false; + } + final ReferrerIntent other = (ReferrerIntent) obj; + return filterEquals(other) && Objects.equals(mReferrer, other.mReferrer); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + filterHashCode(); + result = 31 * result + Objects.hashCode(mReferrer); + return result; + } } diff --git a/core/java/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java b/core/java/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java index 931eb9901288..45555bf98071 100644 --- a/core/java/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java +++ b/core/java/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java @@ -26,7 +26,15 @@ import android.view.Choreographer; */ public final class SfVsyncFrameCallbackProvider implements AnimationFrameCallbackProvider { - private final Choreographer mChoreographer = Choreographer.getSfInstance(); + private final Choreographer mChoreographer; + + public SfVsyncFrameCallbackProvider() { + mChoreographer = Choreographer.getSfInstance(); + } + + public SfVsyncFrameCallbackProvider(Choreographer choreographer) { + mChoreographer = choreographer; + } @Override public void postFrameCallback(Choreographer.FrameCallback callback) { diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java index 3e231d0aaa8b..57efae61a9c6 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodUtils.java +++ b/core/java/com/android/internal/inputmethod/InputMethodUtils.java @@ -836,7 +836,6 @@ public class InputMethodUtils { private final Resources mRes; private final ContentResolver mResolver; private final HashMap<String, InputMethodInfo> mMethodMap; - private final ArrayList<InputMethodInfo> mMethodList; /** * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}. @@ -906,7 +905,6 @@ public class InputMethodUtils { mRes = res; mResolver = resolver; mMethodMap = methodMap; - mMethodList = methodList; switchCurrentUser(userId, copyOnWrite); } @@ -1087,7 +1085,7 @@ public class InputMethodUtils { final ArrayList<InputMethodInfo> res = new ArrayList<>(); for (Pair<String, ArrayList<String>> ims: imsList) { InputMethodInfo info = mMethodMap.get(ims.first); - if (info != null) { + if (info != null && !info.isVrOnly()) { res.add(info); } } diff --git a/core/java/com/android/internal/logging/EventLogTags.logtags b/core/java/com/android/internal/logging/EventLogTags.logtags index 93d5a0373d2d..a440ee402294 100644 --- a/core/java/com/android/internal/logging/EventLogTags.logtags +++ b/core/java/com/android/internal/logging/EventLogTags.logtags @@ -8,3 +8,8 @@ option java_package com.android.internal.logging; 524292 sysui_multi_action (content|4) 524290 sysui_count (name|3),(increment|1) 524291 sysui_histogram (name|3),(bucket|1) + +# --------------------------- +# LatencyTracker.java +# --------------------------- +36070 sysui_latency (action|1|6),(latency|1|3) diff --git a/core/java/com/android/internal/net/INetworkWatchlistManager.aidl b/core/java/com/android/internal/net/INetworkWatchlistManager.aidl new file mode 100644 index 000000000000..7e88369055b8 --- /dev/null +++ b/core/java/com/android/internal/net/INetworkWatchlistManager.aidl @@ -0,0 +1,26 @@ +/* + * 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 com.android.internal.net; + +import android.os.SharedMemory; + +/** {@hide} */ +interface INetworkWatchlistManager { + boolean startWatchlistLogging(); + boolean stopWatchlistLogging(); + void reportWatchlistIfNecessary(); +} diff --git a/core/java/com/android/internal/net/NetworkStatsFactory.java b/core/java/com/android/internal/net/NetworkStatsFactory.java index 3d3e148fb3c2..5eda81baa364 100644 --- a/core/java/com/android/internal/net/NetworkStatsFactory.java +++ b/core/java/com/android/internal/net/NetworkStatsFactory.java @@ -43,6 +43,8 @@ import java.util.Objects; /** * Creates {@link NetworkStats} instances by parsing various {@code /proc/} * files as needed. + * + * @hide */ public class NetworkStatsFactory { private static final String TAG = "NetworkStatsFactory"; diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java index d64c9a1d813b..4a181b27b2e3 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -20,6 +20,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.content.pm.ParceledListSlice; +import android.media.AudioAttributes; import android.os.RemoteException; import android.provider.Settings; @@ -47,6 +48,7 @@ public class SystemNotificationChannels { public static String RETAIL_MODE = "RETAIL_MODE"; public static String USB = "USB"; public static String FOREGROUND_SERVICE = "FOREGROUND_SERVICE"; + public static String HEAVY_WEIGHT_APP = "HEAVY_WEIGHT_APP"; public static void createAll(Context context) { final NotificationManager nm = context.getSystemService(NotificationManager.class); @@ -139,6 +141,17 @@ public class SystemNotificationChannels { foregroundChannel.setBlockableSystem(true); channelsList.add(foregroundChannel); + NotificationChannel heavyWeightChannel = new NotificationChannel( + HEAVY_WEIGHT_APP, + context.getString(R.string.notification_channel_heavy_weight_app), + NotificationManager.IMPORTANCE_DEFAULT); + heavyWeightChannel.setShowBadge(false); + heavyWeightChannel.setSound(null, new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT) + .build()); + channelsList.add(heavyWeightChannel); + nm.createNotificationChannels(channelsList); } diff --git a/core/java/com/android/internal/os/BackgroundThread.java b/core/java/com/android/internal/os/BackgroundThread.java index cffba0177d14..7558f8cee233 100644 --- a/core/java/com/android/internal/os/BackgroundThread.java +++ b/core/java/com/android/internal/os/BackgroundThread.java @@ -35,7 +35,7 @@ public final class BackgroundThread extends HandlerThread { if (sInstance == null) { sInstance = new BackgroundThread(); sInstance.start(); - sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_ACTIVITY_MANAGER); + sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER); sHandler = new Handler(sInstance.getLooper()); } } diff --git a/core/java/com/android/internal/os/BaseCommand.java b/core/java/com/android/internal/os/BaseCommand.java index 3baccee049b0..05ec9e90513e 100644 --- a/core/java/com/android/internal/os/BaseCommand.java +++ b/core/java/com/android/internal/os/BaseCommand.java @@ -106,6 +106,14 @@ public abstract class BaseCommand { } /** + * Peek the next argument on the command line, whatever it is; if there are + * no arguments left, return null. + */ + public String peekNextArg() { + return mArgs.peekNextArg(); + } + + /** * Return the next argument on the command line, whatever it is; if there are * no arguments left, throws an IllegalArgumentException to report this to the user. */ diff --git a/core/java/com/android/internal/os/BatteryStatsHelper.java b/core/java/com/android/internal/os/BatteryStatsHelper.java index f085e290222f..15dc6f507093 100644 --- a/core/java/com/android/internal/os/BatteryStatsHelper.java +++ b/core/java/com/android/internal/os/BatteryStatsHelper.java @@ -143,6 +143,9 @@ public class BatteryStatsHelper { public static boolean checkWifiOnly(Context context) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService( Context.CONNECTIVITY_SERVICE); + if (cm == null) { + return false; + } return !cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE); } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index f26c0cdfe958..a050a3ce0cf2 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -63,6 +63,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.SparseLongArray; +import android.util.StatsLog; import android.util.TimeUtils; import android.util.Xml; import android.view.Display; @@ -119,7 +120,7 @@ public class BatteryStatsImpl extends BatteryStats { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version - private static final int VERSION = 167 + (USE_OLD_HISTORY ? 1000 : 0); + private static final int VERSION = 170 + (USE_OLD_HISTORY ? 1000 : 0); // Maximum number of items we will record in the history. private static final int MAX_HISTORY_ITEMS; @@ -157,6 +158,15 @@ public class BatteryStatsImpl extends BatteryStats { // Number of transmit power states the Bluetooth controller can be in. private static final int NUM_BT_TX_LEVELS = 1; + /** + * Holding a wakelock costs more than just using the cpu. + * Currently, we assign only half the cpu time to an app that is running but + * not holding a wakelock. The apps holding wakelocks get the rest of the blame. + * If no app is holding a wakelock, then the distribution is normal. + */ + @VisibleForTesting + public static final int WAKE_LOCK_WEIGHT = 50; + protected Clocks mClocks; private final JournaledFile mFile; @@ -171,9 +181,12 @@ public class BatteryStatsImpl extends BatteryStats { private final KernelWakelockReader mKernelWakelockReader = new KernelWakelockReader(); private final KernelWakelockStats mTmpWakelockStats = new KernelWakelockStats(); - private final KernelUidCpuTimeReader mKernelUidCpuTimeReader = new KernelUidCpuTimeReader(); - private KernelCpuSpeedReader[] mKernelCpuSpeedReaders; - private final KernelUidCpuFreqTimeReader mKernelUidCpuFreqTimeReader = + @VisibleForTesting + protected KernelUidCpuTimeReader mKernelUidCpuTimeReader = new KernelUidCpuTimeReader(); + @VisibleForTesting + protected KernelCpuSpeedReader[] mKernelCpuSpeedReaders; + @VisibleForTesting + protected KernelUidCpuFreqTimeReader mKernelUidCpuFreqTimeReader = new KernelUidCpuFreqTimeReader(); private final KernelMemoryBandwidthStats mKernelMemoryBandwidthStats @@ -205,10 +218,12 @@ public class BatteryStatsImpl extends BatteryStats { public static abstract class UserInfoProvider { private int[] userIds; protected abstract @Nullable int[] getUserIds(); - private final void refreshUserIds() { + @VisibleForTesting + public final void refreshUserIds() { userIds = getUserIds(); } - private final boolean exists(int userId) { + @VisibleForTesting + public boolean exists(int userId) { return userIds != null ? ArrayUtils.contains(userIds, userId) : true; } } @@ -226,7 +241,7 @@ public class BatteryStatsImpl extends BatteryStats { switch (msg.what) { case MSG_UPDATE_WAKELOCKS: synchronized (BatteryStatsImpl.this) { - updateCpuTimeLocked(false /* updateCpuFreqData */); + updateCpuTimeLocked(); } if (cb != null) { cb.batteryNeedsCpuUpdate(); @@ -282,7 +297,8 @@ public class BatteryStatsImpl extends BatteryStats { public final MyHandler mHandler; private ExternalStatsSync mExternalSync = null; - private UserInfoProvider mUserInfoProvider = null; + @VisibleForTesting + protected UserInfoProvider mUserInfoProvider = null; private BatteryCallback mCallback; @@ -300,7 +316,8 @@ public class BatteryStatsImpl extends BatteryStats { // elapsed time by the number of active timers to arrive at that timer's share of the time. // In order to do this, we must refresh each timer whenever the number of active timers // changes. - final ArrayList<StopwatchTimer> mPartialTimers = new ArrayList<>(); + @VisibleForTesting + protected ArrayList<StopwatchTimer> mPartialTimers = new ArrayList<>(); final ArrayList<StopwatchTimer> mFullTimers = new ArrayList<>(); final ArrayList<StopwatchTimer> mWindowTimers = new ArrayList<>(); final ArrayList<StopwatchTimer> mDrawTimers = new ArrayList<>(); @@ -317,7 +334,8 @@ public class BatteryStatsImpl extends BatteryStats { final ArrayList<StopwatchTimer> mBluetoothScanOnTimers = new ArrayList<>(); // Last partial timers we use for distributing CPU usage. - final ArrayList<StopwatchTimer> mLastPartialTimers = new ArrayList<>(); + @VisibleForTesting + protected ArrayList<StopwatchTimer> mLastPartialTimers = new ArrayList<>(); // These are the objects that will want to do something when the device // is unplugged from power. @@ -549,7 +567,8 @@ public class BatteryStatsImpl extends BatteryStats { * in to power. */ boolean mOnBattery; - boolean mOnBatteryInternal; + @VisibleForTesting + protected boolean mOnBatteryInternal; /** * External reporting of whether the device is actually charging. @@ -580,6 +599,8 @@ public class BatteryStatsImpl extends BatteryStats { private LongSamplingCounter mDischargeScreenOffCounter; private LongSamplingCounter mDischargeScreenDozeCounter; private LongSamplingCounter mDischargeCounter; + private LongSamplingCounter mDischargeLightDozeCounter; + private LongSamplingCounter mDischargeDeepDozeCounter; static final int MAX_LEVEL_STEPS = 200; @@ -623,7 +644,8 @@ public class BatteryStatsImpl extends BatteryStats { private long[] mCpuFreqs; - private PowerProfile mPowerProfile; + @VisibleForTesting + protected PowerProfile mPowerProfile; /* * Holds a SamplingTimer associated with each Resource Power Manager state and voter, @@ -662,21 +684,31 @@ public class BatteryStatsImpl extends BatteryStats { } @Override - public long getMahDischarge(int which) { + public long getUahDischarge(int which) { return mDischargeCounter.getCountLocked(which); } @Override - public long getMahDischargeScreenOff(int which) { + public long getUahDischargeScreenOff(int which) { return mDischargeScreenOffCounter.getCountLocked(which); } @Override - public long getMahDischargeScreenDoze(int which) { + public long getUahDischargeScreenDoze(int which) { return mDischargeScreenDozeCounter.getCountLocked(which); } @Override + public long getUahDischargeLightDoze(int which) { + return mDischargeLightDozeCounter.getCountLocked(which); + } + + @Override + public long getUahDischargeDeepDoze(int which) { + return mDischargeDeepDozeCounter.getCountLocked(which); + } + + @Override public int getEstimatedBatteryCapacity() { return mEstimatedBatteryCapacity; } @@ -2071,7 +2103,8 @@ public class BatteryStatsImpl extends BatteryStats { * For partial wake locks, keep track of whether we are in the list * to consume CPU cycles. */ - boolean mInList; + @VisibleForTesting + public boolean mInList; public StopwatchTimer(Clocks clocks, Uid uid, int type, ArrayList<StopwatchTimer> timerPool, TimeBase timeBase, Parcel in) { @@ -3568,7 +3601,7 @@ public class BatteryStatsImpl extends BatteryStats { public void updateTimeBasesLocked(boolean unplugged, int screenState, long uptime, long realtime) { - final boolean screenOff = isScreenOff(screenState) || isScreenDoze(screenState); + final boolean screenOff = !isScreenOn(screenState); final boolean updateOnBatteryTimeBase = unplugged != mOnBatteryTimeBase.isRunning(); final boolean updateOnBatteryScreenOffTimeBase = (unplugged && screenOff) != mOnBatteryScreenOffTimeBase.isRunning(); @@ -3589,7 +3622,8 @@ public class BatteryStatsImpl extends BatteryStats { + Display.stateToString(screenState) + " and battery is " + (unplugged ? "on" : "off")); } - updateCpuTimeLocked(true /* updateCpuFreqData */); + updateCpuTimeLocked(); + mOnBatteryTimeBase.setRunning(unplugged, uptime, realtime); if (updateOnBatteryTimeBase) { for (int i = mUidStats.size() - 1; i >= 0; --i) { @@ -3617,6 +3651,7 @@ public class BatteryStatsImpl extends BatteryStats { public void addIsolatedUidLocked(int isolatedUid, int appUid) { mIsolatedUids.put(isolatedUid, appUid); + StatsLog.write(StatsLog.ISOLATED_UID_CHANGED, appUid, isolatedUid, 1); } /** @@ -3637,9 +3672,11 @@ public class BatteryStatsImpl extends BatteryStats { * @see #scheduleRemoveIsolatedUidLocked(int, int) */ public void removeIsolatedUidLocked(int isolatedUid) { - mIsolatedUids.delete(isolatedUid); - mKernelUidCpuTimeReader.removeUid(isolatedUid); - mKernelUidCpuFreqTimeReader.removeUid(isolatedUid); + StatsLog.write( + StatsLog.ISOLATED_UID_CHANGED, mIsolatedUids.get(isolatedUid, -1), isolatedUid, 0); + mIsolatedUids.delete(isolatedUid); + mKernelUidCpuTimeReader.removeUid(isolatedUid); + mKernelUidCpuFreqTimeReader.removeUid(isolatedUid); } public int mapUid(int uid) { @@ -4009,6 +4046,7 @@ public class BatteryStatsImpl extends BatteryStats { } addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_LONG_WAKE_LOCK_START, historyName, uid); + StatsLog.write(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED, uid, name, historyName, 1); } public void noteLongPartialWakelockFinish(String name, String historyName, int uid) { @@ -4024,6 +4062,7 @@ public class BatteryStatsImpl extends BatteryStats { } addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH, historyName, uid); + StatsLog.write(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED, uid, name, historyName, 0); } void aggregateLastWakeupUptimeLocked(long uptimeMs) { @@ -4031,6 +4070,7 @@ public class BatteryStatsImpl extends BatteryStats { long deltaUptime = uptimeMs - mLastWakeupUptimeMs; SamplingTimer timer = getWakeupReasonTimerLocked(mLastWakeupReason); timer.add(deltaUptime * 1000, 1); // time in in microseconds + StatsLog.write(StatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason, deltaUptime * 1000); mLastWakeupReason = null; } } @@ -4349,6 +4389,7 @@ public class BatteryStatsImpl extends BatteryStats { + Integer.toHexString(mHistoryCur.states)); addHistoryRecordLocked(elapsedRealtime, uptime); mMobileRadioPowerState = powerState; + StatsLog.write(StatsLog.MOBILE_RADIO_POWER_STATE_CHANGED, uid, powerState); if (active) { mMobileRadioActiveTimer.startRunningLocked(elapsedRealtime); mMobileRadioActivePerAppTimer.startRunningLocked(elapsedRealtime); @@ -4382,10 +4423,11 @@ public class BatteryStatsImpl extends BatteryStats { mPowerSaveModeEnabledTimer.stopRunningLocked(elapsedRealtime); } addHistoryRecordLocked(elapsedRealtime, uptime); + StatsLog.write(StatsLog.BATTERY_SAVER_MODE_STATE_CHANGED, enabled ? 1 : 0); } } - public void noteDeviceIdleModeLocked(int mode, String activeReason, int activeUid) { + public void noteDeviceIdleModeLocked(final int mode, String activeReason, int activeUid) { final long elapsedRealtime = mClocks.elapsedRealtime(); final long uptime = mClocks.uptimeMillis(); boolean nowIdling = mode == DEVICE_IDLE_MODE_DEEP; @@ -4404,6 +4446,13 @@ public class BatteryStatsImpl extends BatteryStats { addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_ACTIVE, activeReason, activeUid); } + if (mDeviceIdling != nowIdling || mDeviceLightIdling != nowLightIdling) { + int statsmode; + if (nowIdling) statsmode = DEVICE_IDLE_MODE_DEEP; + else if (nowLightIdling) statsmode = DEVICE_IDLE_MODE_LIGHT; + else statsmode = DEVICE_IDLE_MODE_OFF; + StatsLog.write(StatsLog.DEVICE_IDLING_MODE_STATE_CHANGED, statsmode); + } if (mDeviceIdling != nowIdling) { mDeviceIdling = nowIdling; int stepState = nowIdling ? STEP_LEVEL_MODE_DEVICE_IDLE : 0; @@ -4448,14 +4497,16 @@ public class BatteryStatsImpl extends BatteryStats { mDeviceIdleModeFullTimer.startRunningLocked(elapsedRealtime); } mDeviceIdleMode = mode; + StatsLog.write(StatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode); } } - public void notePackageInstalledLocked(String pkgName, int versionCode) { + public void notePackageInstalledLocked(String pkgName, long versionCode) { final long elapsedRealtime = mClocks.elapsedRealtime(); final long uptime = mClocks.uptimeMillis(); + // XXX need to figure out what to do with long version codes. addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_PACKAGE_INSTALLED, - pkgName, versionCode); + pkgName, (int)versionCode); PackageChange pc = new PackageChange(); pc.mPackageName = pkgName; pc.mUpdate = true; @@ -4608,6 +4659,7 @@ public class BatteryStatsImpl extends BatteryStats { if (DEBUG_HISTORY) Slog.v(TAG, "Signal strength " + strengthBin + " to: " + Integer.toHexString(mHistoryCur.states)); newHistory = true; + StatsLog.write(StatsLog.PHONE_SIGNAL_STRENGTH_CHANGED, strengthBin); } else { stopAllPhoneSignalStrengthTimersLocked(-1); } @@ -5043,6 +5095,7 @@ public class BatteryStatsImpl extends BatteryStats { + Integer.toHexString(mHistoryCur.states)); addHistoryRecordLocked(elapsedRealtime, uptime); mWifiRadioPowerState = powerState; + StatsLog.write(StatsLog.WIFI_RADIO_POWER_STATE_CHANGED, uid, powerState); } } @@ -5163,6 +5216,7 @@ public class BatteryStatsImpl extends BatteryStats { if (strengthBin >= 0) { if (!mWifiSignalStrengthsTimer[strengthBin].isRunningLocked()) { mWifiSignalStrengthsTimer[strengthBin].startRunningLocked(elapsedRealtime); + StatsLog.write(StatsLog.WIFI_SIGNAL_STRENGTH_CHANGED, strengthBin); } mHistoryCur.states2 = (mHistoryCur.states2&~HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK) @@ -5384,6 +5438,18 @@ public class BatteryStatsImpl extends BatteryStats { } } + public String[] getWifiIfaces() { + synchronized (mWifiNetworkLock) { + return mWifiIfaces; + } + } + + public String[] getMobileIfaces() { + synchronized (mModemNetworkLock) { + return mModemIfaces; + } + } + @Override public long getScreenOnTime(long elapsedRealtimeUs, int which) { return mScreenOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @@ -5406,6 +5472,10 @@ public class BatteryStatsImpl extends BatteryStats { elapsedRealtimeUs, which); } + @Override public Timer getScreenBrightnessTimer(int brightnessBin) { + return mScreenBrightnessTimer[brightnessBin]; + } + @Override public long getInteractiveTime(long elapsedRealtimeUs, int which) { return mInteractiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @@ -5499,10 +5569,18 @@ public class BatteryStatsImpl extends BatteryStats { elapsedRealtimeUs, which); } + @Override public Timer getPhoneSignalScanningTimer() { + return mPhoneSignalScanningTimer; + } + @Override public int getPhoneSignalStrengthCount(int strengthBin, int which) { return mPhoneSignalStrengthsTimer[strengthBin].getCountLocked(which); } + @Override public Timer getPhoneSignalStrengthTimer(int strengthBin) { + return mPhoneSignalStrengthsTimer[strengthBin]; + } + @Override public long getPhoneDataConnectionTime(int dataType, long elapsedRealtimeUs, int which) { return mPhoneDataConnectionsTimer[dataType].getTotalTimeLocked( @@ -5513,6 +5591,10 @@ public class BatteryStatsImpl extends BatteryStats { return mPhoneDataConnectionsTimer[dataType].getCountLocked(which); } + @Override public Timer getPhoneDataConnectionTimer(int dataType) { + return mPhoneDataConnectionsTimer[dataType]; + } + @Override public long getMobileRadioActiveTime(long elapsedRealtimeUs, int which) { return mMobileRadioActiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @@ -5551,6 +5633,10 @@ public class BatteryStatsImpl extends BatteryStats { return mWifiStateTimer[wifiState].getCountLocked(which); } + @Override public Timer getWifiStateTimer(int wifiState) { + return mWifiStateTimer[wifiState]; + } + @Override public long getWifiSupplStateTime(int state, long elapsedRealtimeUs, int which) { return mWifiSupplStateTimer[state].getTotalTimeLocked( @@ -5561,6 +5647,10 @@ public class BatteryStatsImpl extends BatteryStats { return mWifiSupplStateTimer[state].getCountLocked(which); } + @Override public Timer getWifiSupplStateTimer(int state) { + return mWifiSupplStateTimer[state]; + } + @Override public long getWifiSignalStrengthTime(int strengthBin, long elapsedRealtimeUs, int which) { return mWifiSignalStrengthsTimer[strengthBin].getTotalTimeLocked( @@ -5571,6 +5661,10 @@ public class BatteryStatsImpl extends BatteryStats { return mWifiSignalStrengthsTimer[strengthBin].getCountLocked(which); } + @Override public Timer getWifiSignalStrengthTimer(int strengthBin) { + return mWifiSignalStrengthsTimer[strengthBin]; + } + @Override public ControllerActivityCounter getBluetoothControllerActivity() { return mBluetoothActivity; @@ -5937,6 +6031,11 @@ public class BatteryStatsImpl extends BatteryStats { } @Override + public Timer getMulticastWakelockStats() { + return mWifiMulticastTimer; + } + + @Override public ArrayMap<String, ? extends BatteryStats.Timer> getSyncStats() { return mSyncStats.getMap(); } @@ -6000,6 +6099,8 @@ public class BatteryStatsImpl extends BatteryStats { mBsi.mFullWifiLockTimers, mBsi.mOnBatteryTimeBase); } mFullWifiLockTimer.startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.WIFI_LOCK_STATE_CHANGED, getUid(), 1); } } @@ -6008,6 +6109,10 @@ public class BatteryStatsImpl extends BatteryStats { if (mFullWifiLockOut) { mFullWifiLockOut = false; mFullWifiLockTimer.stopRunningLocked(elapsedRealtimeMs); + if (!mFullWifiLockTimer.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.WIFI_LOCK_STATE_CHANGED, getUid(), 0); + } } } @@ -6021,6 +6126,8 @@ public class BatteryStatsImpl extends BatteryStats { mOnBatteryBackgroundTimeBase); } mWifiScanTimer.startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.WIFI_SCAN_STATE_CHANGED, getUid(), 1); } } @@ -6029,6 +6136,10 @@ public class BatteryStatsImpl extends BatteryStats { if (mWifiScanStarted) { mWifiScanStarted = false; mWifiScanTimer.stopRunningLocked(elapsedRealtimeMs); + if (!mWifiScanTimer.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.WIFI_SCAN_STATE_CHANGED, getUid(), 0); + } } } @@ -6131,17 +6242,25 @@ public class BatteryStatsImpl extends BatteryStats { public void noteAudioTurnedOnLocked(long elapsedRealtimeMs) { createAudioTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, getUid(), 1); } public void noteAudioTurnedOffLocked(long elapsedRealtimeMs) { if (mAudioTurnedOnTimer != null) { mAudioTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs); + if (!mAudioTurnedOnTimer.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, getUid(), 0); + } } } public void noteResetAudioLocked(long elapsedRealtimeMs) { if (mAudioTurnedOnTimer != null) { mAudioTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, getUid(), 0); } } @@ -6155,17 +6274,25 @@ public class BatteryStatsImpl extends BatteryStats { public void noteVideoTurnedOnLocked(long elapsedRealtimeMs) { createVideoTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(), 1); } public void noteVideoTurnedOffLocked(long elapsedRealtimeMs) { if (mVideoTurnedOnTimer != null) { mVideoTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs); + if (!mVideoTurnedOnTimer.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(), 0); + } } } public void noteResetVideoLocked(long elapsedRealtimeMs) { if (mVideoTurnedOnTimer != null) { mVideoTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(), 0); } } @@ -6179,17 +6306,25 @@ public class BatteryStatsImpl extends BatteryStats { public void noteFlashlightTurnedOnLocked(long elapsedRealtimeMs) { createFlashlightTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), 1); } public void noteFlashlightTurnedOffLocked(long elapsedRealtimeMs) { if (mFlashlightTurnedOnTimer != null) { mFlashlightTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs); + if (!mFlashlightTurnedOnTimer.isRunningLocked()) { + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), 0); + } } } public void noteResetFlashlightLocked(long elapsedRealtimeMs) { if (mFlashlightTurnedOnTimer != null) { mFlashlightTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), 0); } } @@ -6203,17 +6338,25 @@ public class BatteryStatsImpl extends BatteryStats { public void noteCameraTurnedOnLocked(long elapsedRealtimeMs) { createCameraTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.CAMERA_STATE_CHANGED, getUid(), 1); } public void noteCameraTurnedOffLocked(long elapsedRealtimeMs) { if (mCameraTurnedOnTimer != null) { mCameraTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs); + if (!mCameraTurnedOnTimer.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.CAMERA_STATE_CHANGED, getUid(), 0); + } } } public void noteResetCameraLocked(long elapsedRealtimeMs) { if (mCameraTurnedOnTimer != null) { mCameraTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.CAMERA_STATE_CHANGED, getUid(), 0); } } @@ -6262,26 +6405,42 @@ public class BatteryStatsImpl extends BatteryStats { public void noteBluetoothScanStartedLocked(long elapsedRealtimeMs, boolean isUnoptimized) { createBluetoothScanTimerLocked().startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, getUid(), 1); if (isUnoptimized) { createBluetoothUnoptimizedScanTimerLocked().startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED, getUid(), 1); } } public void noteBluetoothScanStoppedLocked(long elapsedRealtimeMs, boolean isUnoptimized) { if (mBluetoothScanTimer != null) { mBluetoothScanTimer.stopRunningLocked(elapsedRealtimeMs); + if (!mBluetoothScanTimer.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, getUid(), 0); + } } if (isUnoptimized && mBluetoothUnoptimizedScanTimer != null) { mBluetoothUnoptimizedScanTimer.stopRunningLocked(elapsedRealtimeMs); + if (!mBluetoothUnoptimizedScanTimer.isRunningLocked()) { + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED, getUid(), 0); + } } } public void noteResetBluetoothScanLocked(long elapsedRealtimeMs) { if (mBluetoothScanTimer != null) { mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, getUid(), 0); } if (mBluetoothUnoptimizedScanTimer != null) { mBluetoothUnoptimizedScanTimer.stopAllRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED, getUid(), 0); } } @@ -6303,6 +6462,9 @@ public class BatteryStatsImpl extends BatteryStats { createBluetoothScanResultCounterLocked().addAtomic(numNewResults); // Uses background timebase, so the count will only be incremented if uid in background. createBluetoothScanResultBgCounterLocked().addAtomic(numNewResults); + // TODO(statsd): Possibly use a worksource instead of a uid. + // TODO(statsd): This could be in AppScanStats instead, if desired. + StatsLog.write(StatsLog.BLE_SCAN_RESULT_RECEIVED, getUid(), numNewResults); } @Override @@ -6379,6 +6541,11 @@ public class BatteryStatsImpl extends BatteryStats { } @Override + public Timer getWifiScanTimer() { + return mWifiScanTimer; + } + + @Override public int getWifiScanBackgroundCount(int which) { if (mWifiScanTimer == null || mWifiScanTimer.getSubTimer() == null) { return 0; @@ -6405,6 +6572,14 @@ public class BatteryStatsImpl extends BatteryStats { } @Override + public Timer getWifiScanBackgroundTimer() { + if (mWifiScanTimer == null) { + return null; + } + return mWifiScanTimer.getSubTimer(); + } + + @Override public long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which) { if (csphBin < 0 || csphBin >= NUM_WIFI_BATCHED_SCAN_BINS) return 0; if (mWifiBatchedScanTimer[csphBin] == null) { @@ -8663,6 +8838,8 @@ public class BatteryStatsImpl extends BatteryStats { DualTimer t = mSyncStats.startObject(name); if (t != null) { t.startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.SYNC_STATE_CHANGED, getUid(), name, 1); } } @@ -8670,6 +8847,10 @@ public class BatteryStatsImpl extends BatteryStats { DualTimer t = mSyncStats.stopObject(name); if (t != null) { t.stopRunningLocked(elapsedRealtimeMs); + if (!t.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.SYNC_STATE_CHANGED, getUid(), name, 0); + } } } @@ -8677,6 +8858,8 @@ public class BatteryStatsImpl extends BatteryStats { DualTimer t = mJobStats.startObject(name); if (t != null) { t.startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.SCHEDULED_JOB_STATE_CHANGED, getUid(), name, 1); } } @@ -8684,6 +8867,10 @@ public class BatteryStatsImpl extends BatteryStats { DualTimer t = mJobStats.stopObject(name); if (t != null) { t.stopRunningLocked(elapsedRealtimeMs); + if (!t.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.SCHEDULED_JOB_STATE_CHANGED, getUid(), name, 0); + } } if (mBsi.mOnBatteryTimeBase.isRunning()) { SparseIntArray types = mJobCompletions.get(name); @@ -8747,6 +8934,8 @@ public class BatteryStatsImpl extends BatteryStats { Wakelock wl = mWakelockStats.startObject(name); if (wl != null) { getWakelockTimerLocked(wl, type).startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Hopefully use a worksource instead of a uid (so move elsewhere) + StatsLog.write(StatsLog.WAKELOCK_STATE_CHANGED, getUid(), type, name, 1); } if (type == WAKE_TYPE_PARTIAL) { createAggregatedPartialWakelockTimerLocked().startRunningLocked(elapsedRealtimeMs); @@ -8762,7 +8951,12 @@ public class BatteryStatsImpl extends BatteryStats { public void noteStopWakeLocked(int pid, String name, int type, long elapsedRealtimeMs) { Wakelock wl = mWakelockStats.stopObject(name); if (wl != null) { - getWakelockTimerLocked(wl, type).stopRunningLocked(elapsedRealtimeMs); + StopwatchTimer wlt = getWakelockTimerLocked(wl, type); + wlt.stopRunningLocked(elapsedRealtimeMs); + if (!wlt.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + StatsLog.write(StatsLog.WAKELOCK_STATE_CHANGED, getUid(), type, name, 0); + } } if (type == WAKE_TYPE_PARTIAL) { if (mAggregatedPartialWakelockTimer != null) { @@ -8790,6 +8984,12 @@ public class BatteryStatsImpl extends BatteryStats { public void noteStartSensor(int sensor, long elapsedRealtimeMs) { DualTimer t = getSensorTimerLocked(sensor, /* create= */ true); t.startRunningLocked(elapsedRealtimeMs); + // TODO(statsd): Possibly use a worksource instead of a uid. + if (sensor == Sensor.GPS) { + StatsLog.write(StatsLog.GPS_SCAN_STATE_CHANGED, getUid(), 1); + } else { + StatsLog.write(StatsLog.SENSOR_STATE_CHANGED, getUid(), sensor, 1); + } } public void noteStopSensor(int sensor, long elapsedRealtimeMs) { @@ -8797,6 +8997,14 @@ public class BatteryStatsImpl extends BatteryStats { DualTimer t = getSensorTimerLocked(sensor, false); if (t != null) { t.stopRunningLocked(elapsedRealtimeMs); + if (!t.isRunningLocked()) { // only tell statsd if truly stopped + // TODO(statsd): Possibly use a worksource instead of a uid. + if (sensor == Sensor.GPS) { + StatsLog.write(StatsLog.GPS_SCAN_STATE_CHANGED, getUid(), 0); + } else { + StatsLog.write(StatsLog.SENSOR_STATE_CHANGED, getUid(), sensor, 0); + } + } } } @@ -8898,6 +9106,8 @@ public class BatteryStatsImpl extends BatteryStats { mBluetoothScanTimer = new StopwatchTimer(mClocks, null, -14, null, mOnBatteryTimeBase); mDischargeScreenOffCounter = new LongSamplingCounter(mOnBatteryScreenOffTimeBase); mDischargeScreenDozeCounter = new LongSamplingCounter(mOnBatteryTimeBase); + mDischargeLightDozeCounter = new LongSamplingCounter(mOnBatteryTimeBase); + mDischargeDeepDozeCounter = new LongSamplingCounter(mOnBatteryTimeBase); mDischargeCounter = new LongSamplingCounter(mOnBatteryTimeBase); mOnBattery = mOnBatteryInternal = false; long uptime = mClocks.uptimeMillis() * 1000; @@ -9074,7 +9284,7 @@ public class BatteryStatsImpl extends BatteryStats { if (pc.mUpdate) { out.startTag(null, "upd"); out.attribute(null, "pkg", pc.mPackageName); - out.attribute(null, "ver", Integer.toString(pc.mVersionCode)); + out.attribute(null, "ver", Long.toString(pc.mVersionCode)); out.endTag(null, "upd"); } else { out.startTag(null, "rem"); @@ -9203,7 +9413,7 @@ public class BatteryStatsImpl extends BatteryStats { pc.mUpdate = true; pc.mPackageName = parser.getAttributeValue(null, "pkg"); String verStr = parser.getAttributeValue(null, "ver"); - pc.mVersionCode = verStr != null ? Integer.parseInt(verStr) : 0; + pc.mVersionCode = verStr != null ? Long.parseLong(verStr) : 0; dit.mPackageChanges.add(pc); XmlUtils.skipCurrentTag(parser); } else if (tagName.equals("rem")) { @@ -9442,7 +9652,8 @@ public class BatteryStatsImpl extends BatteryStats { } public boolean isScreenOn(int state) { - return state == Display.STATE_ON; + return state == Display.STATE_ON || state == Display.STATE_VR + || state == Display.STATE_ON_SUSPEND; } public boolean isScreenOff(int state) { @@ -9476,6 +9687,8 @@ public class BatteryStatsImpl extends BatteryStats { mChargeStepTracker.init(); mDischargeScreenOffCounter.reset(false); mDischargeScreenDozeCounter.reset(false); + mDischargeLightDozeCounter.reset(false); + mDischargeDeepDozeCounter.reset(false); mDischargeCounter.reset(false); } @@ -10427,6 +10640,7 @@ public class BatteryStatsImpl extends BatteryStats { // Used in updateCpuTimeLocked(). long mTempTotalCpuUserTimeUs; long mTempTotalCpuSystemTimeUs; + long[][] mWakeLockAllocationsUs; /** * Reads the newest memory stats from the kernel. @@ -10460,7 +10674,7 @@ public class BatteryStatsImpl extends BatteryStats { * and we are on battery with screen off, we give more of the cpu time to those apps holding * wakelocks. If the screen is on, we just assign the actual cpu time an app used. */ - public void updateCpuTimeLocked(boolean updateCpuFreqData) { + public void updateCpuTimeLocked() { if (mPowerProfile == null) { return; } @@ -10469,169 +10683,90 @@ public class BatteryStatsImpl extends BatteryStats { Slog.d(TAG, "!Cpu updating!"); } - // Holding a wakelock costs more than just using the cpu. - // Currently, we assign only half the cpu time to an app that is running but - // not holding a wakelock. The apps holding wakelocks get the rest of the blame. - // If no app is holding a wakelock, then the distribution is normal. - final int wakelockWeight = 50; - - int numWakelocks = 0; + if (mCpuFreqs == null) { + mCpuFreqs = mKernelUidCpuFreqTimeReader.readFreqs(mPowerProfile); + } - // Calculate how many wakelocks we have to distribute amongst. The system is excluded. - // Only distribute cpu power to wakelocks if the screen is off and we're on battery. - final int numPartialTimers = mPartialTimers.size(); + // Calculate the wakelocks we have to distribute amongst. The system is excluded as it is + // usually holding the wakelock on behalf of an app. + // And Only distribute cpu power to wakelocks if the screen is off and we're on battery. + ArrayList<StopwatchTimer> partialTimersToConsider = null; if (mOnBatteryScreenOffTimeBase.isRunning()) { - for (int i = 0; i < numPartialTimers; i++) { + partialTimersToConsider = new ArrayList<>(); + for (int i = mPartialTimers.size() - 1; i >= 0; --i) { final StopwatchTimer timer = mPartialTimers.get(i); + // Since the collection and blaming of wakelocks can be scheduled to run after + // some delay, the mPartialTimers list may have new entries. We can't blame + // the newly added timer for past cpu time, so we only consider timers that + // were present for one round of collection. Once a timer has gone through + // a round of collection, its mInList field is set to true. if (timer.mInList && timer.mUid != null && timer.mUid.mUid != Process.SYSTEM_UID) { - // Since the collection and blaming of wakelocks can be scheduled to run after - // some delay, the mPartialTimers list may have new entries. We can't blame - // the newly added timer for past cpu time, so we only consider timers that - // were present for one round of collection. Once a timer has gone through - // a round of collection, its mInList field is set to true. - numWakelocks++; + partialTimersToConsider.add(timer); } } } + markPartialTimersAsEligible(); - final int numWakelocksF = numWakelocks; - mTempTotalCpuUserTimeUs = 0; - mTempTotalCpuSystemTimeUs = 0; - - final SparseLongArray updatedUids = new SparseLongArray(); - - // Read the CPU data for each UID. This will internally generate a snapshot so next time - // we read, we get a delta. If we are to distribute the cpu time, then do so. Otherwise - // we just ignore the data. - final long startTimeMs = mClocks.uptimeMillis(); - mUserInfoProvider.refreshUserIds(); - mKernelUidCpuTimeReader.readDelta(!mOnBatteryInternal ? null : - new KernelUidCpuTimeReader.Callback() { - @Override - public void onUidCpuTime(int uid, long userTimeUs, long systemTimeUs) { - uid = mapUid(uid); - if (Process.isIsolated(uid)) { - // This could happen if the isolated uid mapping was removed before - // that process was actually killed. - mKernelUidCpuTimeReader.removeUid(uid); - Slog.d(TAG, "Got readings for an isolated uid with" - + " no mapping to owning uid: " + uid); - return; - } - if (!mUserInfoProvider.exists(UserHandle.getUserId(uid))) { - Slog.d(TAG, "Got readings for an invalid user's uid " + uid); - mKernelUidCpuTimeReader.removeUid(uid); - return; - } - final Uid u = getUidStatsLocked(uid); - - // Accumulate the total system and user time. - mTempTotalCpuUserTimeUs += userTimeUs; - mTempTotalCpuSystemTimeUs += systemTimeUs; - - StringBuilder sb = null; - if (DEBUG_ENERGY_CPU) { - sb = new StringBuilder(); - sb.append(" got time for uid=").append(u.mUid).append(": u="); - TimeUtils.formatDuration(userTimeUs / 1000, sb); - sb.append(" s="); - TimeUtils.formatDuration(systemTimeUs / 1000, sb); - sb.append("\n"); - } - - if (numWakelocksF > 0) { - // We have wakelocks being held, so only give a portion of the - // time to the process. The rest will be distributed among wakelock - // holders. - userTimeUs = (userTimeUs * wakelockWeight) / 100; - systemTimeUs = (systemTimeUs * wakelockWeight) / 100; - } - - if (sb != null) { - sb.append(" adding to uid=").append(u.mUid).append(": u="); - TimeUtils.formatDuration(userTimeUs / 1000, sb); - sb.append(" s="); - TimeUtils.formatDuration(systemTimeUs / 1000, sb); - Slog.d(TAG, sb.toString()); - } - - u.mUserCpuTime.addCountLocked(userTimeUs); - u.mSystemCpuTime.addCountLocked(systemTimeUs); - updatedUids.put(u.getUid(), userTimeUs + systemTimeUs); - } - }); - - if (updateCpuFreqData) { - readKernelUidCpuFreqTimesLocked(); + // When the battery is not on, we don't attribute the cpu times to any timers but we still + // need to take the snapshots. + if (!mOnBatteryInternal) { + mKernelUidCpuTimeReader.readDelta(null); + mKernelUidCpuFreqTimeReader.readDelta(null); + for (int cluster = mKernelCpuSpeedReaders.length - 1; cluster >= 0; --cluster) { + mKernelCpuSpeedReaders[cluster].readDelta(); + } + return; } - final long elapse = (mClocks.uptimeMillis() - startTimeMs); - if (DEBUG_ENERGY_CPU || (elapse >= 100)) { - Slog.d(TAG, "Reading cpu stats took " + elapse + " ms"); + mUserInfoProvider.refreshUserIds(); + final SparseLongArray updatedUids = mKernelUidCpuFreqTimeReader.perClusterTimesAvailable() + ? null : new SparseLongArray(); + readKernelUidCpuTimesLocked(partialTimersToConsider, updatedUids); + // updatedUids=null means /proc/uid_time_in_state provides snapshots of per-cluster cpu + // freqs, so no need to approximate these values. + if (updatedUids != null) { + updateClusterSpeedTimes(updatedUids); } + readKernelUidCpuFreqTimesLocked(partialTimersToConsider); + } - if (mOnBatteryInternal && numWakelocks > 0) { - // Distribute a portion of the total cpu time to wakelock holders. - mTempTotalCpuUserTimeUs = (mTempTotalCpuUserTimeUs * (100 - wakelockWeight)) / 100; - mTempTotalCpuSystemTimeUs = - (mTempTotalCpuSystemTimeUs * (100 - wakelockWeight)) / 100; - - for (int i = 0; i < numPartialTimers; i++) { - final StopwatchTimer timer = mPartialTimers.get(i); - - // The system does not share any blame, as it is usually holding the wakelock - // on behalf of an app. - if (timer.mInList && timer.mUid != null && timer.mUid.mUid != Process.SYSTEM_UID) { - int userTimeUs = (int) (mTempTotalCpuUserTimeUs / numWakelocks); - int systemTimeUs = (int) (mTempTotalCpuSystemTimeUs / numWakelocks); - - if (DEBUG_ENERGY_CPU) { - StringBuilder sb = new StringBuilder(); - sb.append(" Distributing wakelock uid=").append(timer.mUid.mUid) - .append(": u="); - TimeUtils.formatDuration(userTimeUs / 1000, sb); - sb.append(" s="); - TimeUtils.formatDuration(systemTimeUs / 1000, sb); - Slog.d(TAG, sb.toString()); - } - - timer.mUid.mUserCpuTime.addCountLocked(userTimeUs); - timer.mUid.mSystemCpuTime.addCountLocked(systemTimeUs); - final int uid = timer.mUid.getUid(); - updatedUids.put(uid, updatedUids.get(uid, 0) + userTimeUs + systemTimeUs); - - final Uid.Proc proc = timer.mUid.getProcessStatsLocked("*wakelock*"); - proc.addCpuTimeLocked(userTimeUs / 1000, systemTimeUs / 1000); - - mTempTotalCpuUserTimeUs -= userTimeUs; - mTempTotalCpuSystemTimeUs -= systemTimeUs; - numWakelocks--; - } + /** + * Mark the current partial timers as gone through a collection so that they will be + * considered in the next cpu times distribution to wakelock holders. + */ + @VisibleForTesting + public void markPartialTimersAsEligible() { + if (ArrayUtils.referenceEquals(mPartialTimers, mLastPartialTimers)) { + // No difference, so each timer is now considered for the next collection. + for (int i = mPartialTimers.size() - 1; i >= 0; --i) { + mPartialTimers.get(i).mInList = true; } + } else { + // The lists are different, meaning we added (or removed a timer) since the last + // collection. + for (int i = mLastPartialTimers.size() - 1; i >= 0; --i) { + mLastPartialTimers.get(i).mInList = false; + } + mLastPartialTimers.clear(); - if (mTempTotalCpuUserTimeUs > 0 || mTempTotalCpuSystemTimeUs > 0) { - // Anything left over is given to the system. - if (DEBUG_ENERGY_CPU) { - StringBuilder sb = new StringBuilder(); - sb.append(" Distributing lost time to system: u="); - TimeUtils.formatDuration(mTempTotalCpuUserTimeUs / 1000, sb); - sb.append(" s="); - TimeUtils.formatDuration(mTempTotalCpuSystemTimeUs / 1000, sb); - Slog.d(TAG, sb.toString()); - } - - final Uid u = getUidStatsLocked(Process.SYSTEM_UID); - u.mUserCpuTime.addCountLocked(mTempTotalCpuUserTimeUs); - u.mSystemCpuTime.addCountLocked(mTempTotalCpuSystemTimeUs); - updatedUids.put(Process.SYSTEM_UID, updatedUids.get(Process.SYSTEM_UID, 0) - + mTempTotalCpuUserTimeUs + mTempTotalCpuSystemTimeUs); - - final Uid.Proc proc = u.getProcessStatsLocked("*lost*"); - proc.addCpuTimeLocked((int) mTempTotalCpuUserTimeUs / 1000, - (int) mTempTotalCpuSystemTimeUs / 1000); + // Mark the current timers as gone through a collection. + final int numPartialTimers = mPartialTimers.size(); + for (int i = 0; i < numPartialTimers; ++i) { + final StopwatchTimer timer = mPartialTimers.get(i); + timer.mInList = true; + mLastPartialTimers.add(timer); } } + } + /** + * Take snapshot of cpu times (aggregated over all uids) at different frequencies and + * calculate cpu times spent by each uid at different frequencies. + * + * @param updatedUids The uids for which times spent at different frequencies are calculated. + */ + @VisibleForTesting + public void updateClusterSpeedTimes(@NonNull SparseLongArray updatedUids) { long totalCpuClustersTimeMs = 0; // Read the time spent for each cluster at various cpu frequencies. final long[][] clusterSpeedTimesMs = new long[mKernelCpuSpeedReaders.length][]; @@ -10653,8 +10788,8 @@ public class BatteryStatsImpl extends BatteryStats { final long appCpuTimeUs = updatedUids.valueAt(i); // Add the cpu speeds to this UID. final int numClusters = mPowerProfile.getNumCpuClusters(); - if (u.mCpuClusterSpeedTimesUs == null || u.mCpuClusterSpeedTimesUs.length != - numClusters) { + if (u.mCpuClusterSpeedTimesUs == null || + u.mCpuClusterSpeedTimesUs.length != numClusters) { u.mCpuClusterSpeedTimesUs = new LongSamplingCounter[numClusters][]; } @@ -10678,68 +10813,224 @@ public class BatteryStatsImpl extends BatteryStats { } } } + } - // See if there is a difference in wakelocks between this collection and the last - // collection. - if (ArrayUtils.referenceEquals(mPartialTimers, mLastPartialTimers)) { - // No difference, so each timer is now considered for the next collection. - for (int i = 0; i < numPartialTimers; i++) { - mPartialTimers.get(i).mInList = true; + /** + * Take a snapshot of the cpu times spent by each uid and update the corresponding counters. + * If {@param partialTimers} is not null and empty, then we assign a portion of cpu times to + * wakelock holders. + * + * @param partialTimers The wakelock holders among which the cpu times will be distributed. + * @param updatedUids If not null, then the uids found in the snapshot will be added to this. + */ + @VisibleForTesting + public void readKernelUidCpuTimesLocked(@Nullable ArrayList<StopwatchTimer> partialTimers, + @Nullable SparseLongArray updatedUids) { + mTempTotalCpuUserTimeUs = mTempTotalCpuSystemTimeUs = 0; + final int numWakelocks = partialTimers == null ? 0 : partialTimers.size(); + final long startTimeMs = mClocks.uptimeMillis(); + + mKernelUidCpuTimeReader.readDelta((uid, userTimeUs, systemTimeUs) -> { + uid = mapUid(uid); + if (Process.isIsolated(uid)) { + // This could happen if the isolated uid mapping was removed before that process + // was actually killed. + mKernelUidCpuTimeReader.removeUid(uid); + Slog.d(TAG, "Got readings for an isolated uid with no mapping: " + uid); + return; } - } else { - // The lists are different, meaning we added (or removed a timer) since the last - // collection. - final int numLastPartialTimers = mLastPartialTimers.size(); - for (int i = 0; i < numLastPartialTimers; i++) { - mLastPartialTimers.get(i).mInList = false; + if (!mUserInfoProvider.exists(UserHandle.getUserId(uid))) { + Slog.d(TAG, "Got readings for an invalid user's uid " + uid); + mKernelUidCpuTimeReader.removeUid(uid); + return; } - mLastPartialTimers.clear(); + final Uid u = getUidStatsLocked(uid); - // Mark the current timers as gone through a collection. - for (int i = 0; i < numPartialTimers; i++) { - final StopwatchTimer timer = mPartialTimers.get(i); - timer.mInList = true; - mLastPartialTimers.add(timer); + // Accumulate the total system and user time. + mTempTotalCpuUserTimeUs += userTimeUs; + mTempTotalCpuSystemTimeUs += systemTimeUs; + + StringBuilder sb = null; + if (DEBUG_ENERGY_CPU) { + sb = new StringBuilder(); + sb.append(" got time for uid=").append(u.mUid).append(": u="); + TimeUtils.formatDuration(userTimeUs / 1000, sb); + sb.append(" s="); + TimeUtils.formatDuration(systemTimeUs / 1000, sb); + sb.append("\n"); + } + + if (numWakelocks > 0) { + // We have wakelocks being held, so only give a portion of the + // time to the process. The rest will be distributed among wakelock + // holders. + userTimeUs = (userTimeUs * WAKE_LOCK_WEIGHT) / 100; + systemTimeUs = (systemTimeUs * WAKE_LOCK_WEIGHT) / 100; + } + + if (sb != null) { + sb.append(" adding to uid=").append(u.mUid).append(": u="); + TimeUtils.formatDuration(userTimeUs / 1000, sb); + sb.append(" s="); + TimeUtils.formatDuration(systemTimeUs / 1000, sb); + Slog.d(TAG, sb.toString()); + } + + u.mUserCpuTime.addCountLocked(userTimeUs); + u.mSystemCpuTime.addCountLocked(systemTimeUs); + if (updatedUids != null) { + updatedUids.put(u.getUid(), userTimeUs + systemTimeUs); + } + }); + + final long elapsedTimeMs = mClocks.uptimeMillis() - startTimeMs; + if (DEBUG_ENERGY_CPU || elapsedTimeMs >= 100) { + Slog.d(TAG, "Reading cpu stats took " + elapsedTimeMs + "ms"); + } + + if (numWakelocks > 0) { + // Distribute a portion of the total cpu time to wakelock holders. + mTempTotalCpuUserTimeUs = (mTempTotalCpuUserTimeUs * (100 - WAKE_LOCK_WEIGHT)) / 100; + mTempTotalCpuSystemTimeUs = + (mTempTotalCpuSystemTimeUs * (100 - WAKE_LOCK_WEIGHT)) / 100; + + for (int i = 0; i < numWakelocks; ++i) { + final StopwatchTimer timer = partialTimers.get(i); + final int userTimeUs = (int) (mTempTotalCpuUserTimeUs / (numWakelocks - i)); + final int systemTimeUs = (int) (mTempTotalCpuSystemTimeUs / (numWakelocks - i)); + + if (DEBUG_ENERGY_CPU) { + final StringBuilder sb = new StringBuilder(); + sb.append(" Distributing wakelock uid=").append(timer.mUid.mUid) + .append(": u="); + TimeUtils.formatDuration(userTimeUs / 1000, sb); + sb.append(" s="); + TimeUtils.formatDuration(systemTimeUs / 1000, sb); + Slog.d(TAG, sb.toString()); + } + + timer.mUid.mUserCpuTime.addCountLocked(userTimeUs); + timer.mUid.mSystemCpuTime.addCountLocked(systemTimeUs); + if (updatedUids != null) { + final int uid = timer.mUid.getUid(); + updatedUids.put(uid, updatedUids.get(uid, 0) + userTimeUs + systemTimeUs); + } + + final Uid.Proc proc = timer.mUid.getProcessStatsLocked("*wakelock*"); + proc.addCpuTimeLocked(userTimeUs / 1000, systemTimeUs / 1000); + + mTempTotalCpuUserTimeUs -= userTimeUs; + mTempTotalCpuSystemTimeUs -= systemTimeUs; } } } - void readKernelUidCpuFreqTimesLocked() { - mKernelUidCpuFreqTimeReader.readDelta(!mOnBatteryInternal ? null : - new KernelUidCpuFreqTimeReader.Callback() { - @Override - public void onCpuFreqs(long[] cpuFreqs) { - mCpuFreqs = cpuFreqs; - } + /** + * Take a snapshot of the cpu times spent by each uid in each freq and update the + * corresponding counters. + * + * @param partialTimers The wakelock holders among which the cpu freq times will be distributed. + */ + @VisibleForTesting + public void readKernelUidCpuFreqTimesLocked(@Nullable ArrayList<StopwatchTimer> partialTimers) { + final boolean perClusterTimesAvailable = + mKernelUidCpuFreqTimeReader.perClusterTimesAvailable(); + final int numWakelocks = partialTimers == null ? 0 : partialTimers.size(); + final int numClusters = mPowerProfile.getNumCpuClusters(); + mWakeLockAllocationsUs = null; + mKernelUidCpuFreqTimeReader.readDelta((uid, cpuFreqTimeMs) -> { + uid = mapUid(uid); + if (Process.isIsolated(uid)) { + mKernelUidCpuFreqTimeReader.removeUid(uid); + Slog.d(TAG, "Got freq readings for an isolated uid with no mapping: " + uid); + return; + } + if (!mUserInfoProvider.exists(UserHandle.getUserId(uid))) { + Slog.d(TAG, "Got freq readings for an invalid user's uid " + uid); + mKernelUidCpuFreqTimeReader.removeUid(uid); + return; + } + final Uid u = getUidStatsLocked(uid); + if (u.mCpuFreqTimeMs == null || u.mCpuFreqTimeMs.getSize() != cpuFreqTimeMs.length) { + u.mCpuFreqTimeMs = new LongSamplingCounterArray(mOnBatteryTimeBase); + } + u.mCpuFreqTimeMs.addCountLocked(cpuFreqTimeMs); + if (u.mScreenOffCpuFreqTimeMs == null || + u.mScreenOffCpuFreqTimeMs.getSize() != cpuFreqTimeMs.length) { + u.mScreenOffCpuFreqTimeMs = new LongSamplingCounterArray( + mOnBatteryScreenOffTimeBase); + } + u.mScreenOffCpuFreqTimeMs.addCountLocked(cpuFreqTimeMs); - @Override - public void onUidCpuFreqTime(int uid, long[] cpuFreqTimeMs) { - uid = mapUid(uid); - if (Process.isIsolated(uid)) { - mKernelUidCpuFreqTimeReader.removeUid(uid); - Slog.d(TAG, "Got freq readings for an isolated uid with" - + " no mapping to owning uid: " + uid); - return; - } - if (!mUserInfoProvider.exists(UserHandle.getUserId(uid))) { - Slog.d(TAG, "Got readings for an invalid user's uid " + uid); - mKernelUidCpuFreqTimeReader.removeUid(uid); - return; + if (perClusterTimesAvailable) { + if (u.mCpuClusterSpeedTimesUs == null || + u.mCpuClusterSpeedTimesUs.length != numClusters) { + u.mCpuClusterSpeedTimesUs = new LongSamplingCounter[numClusters][]; + } + if (numWakelocks > 0 && mWakeLockAllocationsUs == null) { + mWakeLockAllocationsUs = new long[numClusters][]; + } + + int freqIndex = 0; + for (int cluster = 0; cluster < numClusters; ++cluster) { + final int speedsInCluster = mPowerProfile.getNumSpeedStepsInCpuCluster(cluster); + if (u.mCpuClusterSpeedTimesUs[cluster] == null || + u.mCpuClusterSpeedTimesUs[cluster].length != speedsInCluster) { + u.mCpuClusterSpeedTimesUs[cluster] + = new LongSamplingCounter[speedsInCluster]; + } + if (numWakelocks > 0 && mWakeLockAllocationsUs[cluster] == null) { + mWakeLockAllocationsUs[cluster] = new long[speedsInCluster]; + } + final LongSamplingCounter[] cpuTimesUs = u.mCpuClusterSpeedTimesUs[cluster]; + for (int speed = 0; speed < speedsInCluster; ++speed) { + if (cpuTimesUs[speed] == null) { + cpuTimesUs[speed] = new LongSamplingCounter(mOnBatteryTimeBase); } - final Uid u = getUidStatsLocked(uid); - if (u.mCpuFreqTimeMs == null - || u.mCpuFreqTimeMs.getSize() != cpuFreqTimeMs.length) { - u.mCpuFreqTimeMs = new LongSamplingCounterArray(mOnBatteryTimeBase); + final long appAllocationUs; + if (mWakeLockAllocationsUs != null) { + appAllocationUs = + (cpuFreqTimeMs[freqIndex] * 1000 * WAKE_LOCK_WEIGHT) / 100; + mWakeLockAllocationsUs[cluster][speed] += + (cpuFreqTimeMs[freqIndex] * 1000 - appAllocationUs); + } else { + appAllocationUs = cpuFreqTimeMs[freqIndex] * 1000; } - u.mCpuFreqTimeMs.addCountLocked(cpuFreqTimeMs); - if (u.mScreenOffCpuFreqTimeMs == null - || u.mScreenOffCpuFreqTimeMs.getSize() != cpuFreqTimeMs.length) { - u.mScreenOffCpuFreqTimeMs = new LongSamplingCounterArray( - mOnBatteryScreenOffTimeBase); + cpuTimesUs[speed].addCountLocked(appAllocationUs); + freqIndex++; + } + } + } + }); + + if (mWakeLockAllocationsUs != null) { + for (int i = 0; i < numWakelocks; ++i) { + final Uid u = partialTimers.get(i).mUid; + if (u.mCpuClusterSpeedTimesUs == null || + u.mCpuClusterSpeedTimesUs.length != numClusters) { + u.mCpuClusterSpeedTimesUs = new LongSamplingCounter[numClusters][]; + } + + for (int cluster = 0; cluster < numClusters; ++cluster) { + final int speedsInCluster = mPowerProfile.getNumSpeedStepsInCpuCluster(cluster); + if (u.mCpuClusterSpeedTimesUs[cluster] == null || + u.mCpuClusterSpeedTimesUs[cluster].length != speedsInCluster) { + u.mCpuClusterSpeedTimesUs[cluster] + = new LongSamplingCounter[speedsInCluster]; + } + final LongSamplingCounter[] cpuTimeUs = u.mCpuClusterSpeedTimesUs[cluster]; + for (int speed = 0; speed < speedsInCluster; ++speed) { + if (cpuTimeUs[speed] == null) { + cpuTimeUs[speed] = new LongSamplingCounter(mOnBatteryTimeBase); } - u.mScreenOffCpuFreqTimeMs.addCountLocked(cpuFreqTimeMs); + final long allocationUs = + mWakeLockAllocationsUs[cluster][speed] / (numWakelocks - i); + cpuTimeUs[speed].addCountLocked(allocationUs); + mWakeLockAllocationsUs[cluster][speed] -= allocationUs; } - }); + } + } + } } boolean setChargingLocked(boolean charging) { @@ -10928,11 +11219,15 @@ public class BatteryStatsImpl extends BatteryStats { // This should probably be exposed in the API, though it's not critical public static final int BATTERY_PLUGGED_NONE = 0; - public void setBatteryStateLocked(int status, int health, int plugType, int level, - int temp, int volt, int chargeUAh, int chargeFullUAh) { + public void setBatteryStateLocked(final int status, final int health, final int plugType, + final int level, /* not final */ int temp, final int volt, final int chargeUAh, + final int chargeFullUAh) { // Temperature is encoded without the signed bit, so clamp any negative temperatures to 0. temp = Math.max(0, temp); + reportChangesToStatsLog(mHaveBatteryLevel ? mHistoryCur : null, + status, plugType, level, temp); + final boolean onBattery = plugType == BATTERY_PLUGGED_NONE; final long uptime = mClocks.uptimeMillis(); final long elapsedRealtime = mClocks.elapsedRealtime(); @@ -10993,6 +11288,11 @@ public class BatteryStatsImpl extends BatteryStats { if (isScreenDoze(mScreenState)) { mDischargeScreenDozeCounter.addCountLocked(chargeDiff); } + if (mDeviceIdleMode == DEVICE_IDLE_MODE_LIGHT) { + mDischargeLightDozeCounter.addCountLocked(chargeDiff); + } else if (mDeviceIdleMode == DEVICE_IDLE_MODE_DEEP) { + mDischargeDeepDozeCounter.addCountLocked(chargeDiff); + } } mHistoryCur.batteryChargeUAh = chargeUAh; setOnBatteryLocked(elapsedRealtime, uptime, onBattery, oldStatus, level, chargeUAh); @@ -11038,6 +11338,11 @@ public class BatteryStatsImpl extends BatteryStats { if (isScreenDoze(mScreenState)) { mDischargeScreenDozeCounter.addCountLocked(chargeDiff); } + if (mDeviceIdleMode == DEVICE_IDLE_MODE_LIGHT) { + mDischargeLightDozeCounter.addCountLocked(chargeDiff); + } else if (mDeviceIdleMode == DEVICE_IDLE_MODE_DEEP) { + mDischargeDeepDozeCounter.addCountLocked(chargeDiff); + } } mHistoryCur.batteryChargeUAh = chargeUAh; changed = true; @@ -11109,6 +11414,24 @@ public class BatteryStatsImpl extends BatteryStats { mMaxLearnedBatteryCapacity = Math.max(mMaxLearnedBatteryCapacity, chargeFullUAh); } + // Inform StatsLog of setBatteryState changes. + // If this is the first reporting, pass in recentPast == null. + private void reportChangesToStatsLog(HistoryItem recentPast, + final int status, final int plugType, final int level, final int temp) { + + if (recentPast == null || recentPast.batteryStatus != status) { + StatsLog.write(StatsLog.CHARGING_STATE_CHANGED, status); + } + if (recentPast == null || recentPast.batteryPlugType != plugType) { + StatsLog.write(StatsLog.PLUGGED_STATE_CHANGED, plugType); + } + if (recentPast == null || recentPast.batteryLevel != level) { + StatsLog.write(StatsLog.BATTERY_LEVEL_CHANGED, level); + } + // Let's just always print the temperature, regardless of whether it changed. + StatsLog.write(StatsLog.DEVICE_TEMPERATURE_REPORTED, temp); + } + public long getAwakeTimeBattery() { return computeBatteryUptime(getBatteryUptimeLocked(), STATS_CURRENT); } @@ -11781,6 +12104,8 @@ public class BatteryStatsImpl extends BatteryStats { mDischargeCounter.readSummaryFromParcelLocked(in); mDischargeScreenOffCounter.readSummaryFromParcelLocked(in); mDischargeScreenDozeCounter.readSummaryFromParcelLocked(in); + mDischargeLightDozeCounter.readSummaryFromParcelLocked(in); + mDischargeDeepDozeCounter.readSummaryFromParcelLocked(in); int NPKG = in.readInt(); if (NPKG > 0) { mDailyPackageChanges = new ArrayList<>(NPKG); @@ -11789,7 +12114,7 @@ public class BatteryStatsImpl extends BatteryStats { PackageChange pc = new PackageChange(); pc.mPackageName = in.readString(); pc.mUpdate = in.readInt() != 0; - pc.mVersionCode = in.readInt(); + pc.mVersionCode = in.readLong(); mDailyPackageChanges.add(pc); } } else { @@ -11915,8 +12240,6 @@ public class BatteryStatsImpl extends BatteryStats { } } - mCpuFreqs = in.createLongArray(); - final int NU = in.readInt(); if (NU > 10000) { throw new ParcelFormatException("File corrupt: too many uids " + NU); @@ -12207,6 +12530,8 @@ public class BatteryStatsImpl extends BatteryStats { mDischargeCounter.writeSummaryFromParcelLocked(out); mDischargeScreenOffCounter.writeSummaryFromParcelLocked(out); mDischargeScreenDozeCounter.writeSummaryFromParcelLocked(out); + mDischargeLightDozeCounter.writeSummaryFromParcelLocked(out); + mDischargeDeepDozeCounter.writeSummaryFromParcelLocked(out); if (mDailyPackageChanges != null) { final int NPKG = mDailyPackageChanges.size(); out.writeInt(NPKG); @@ -12214,7 +12539,7 @@ public class BatteryStatsImpl extends BatteryStats { PackageChange pc = mDailyPackageChanges.get(i); out.writeString(pc.mPackageName); out.writeInt(pc.mUpdate ? 1 : 0); - out.writeInt(pc.mVersionCode); + out.writeLong(pc.mVersionCode); } } else { out.writeInt(0); @@ -12335,8 +12660,6 @@ public class BatteryStatsImpl extends BatteryStats { } } - out.writeLongArray(mCpuFreqs); - final int NU = mUidStats.size(); out.writeInt(NU); for (int iu = 0; iu < NU; iu++) { @@ -12696,7 +13019,7 @@ public class BatteryStatsImpl extends BatteryStats { mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW; mMobileRadioActiveTimer = new StopwatchTimer(mClocks, null, -400, null, mOnBatteryTimeBase, in); - mMobileRadioActivePerAppTimer = new StopwatchTimer(mClocks, null, -401, null, + mMobileRadioActivePerAppTimer = new StopwatchTimer(mClocks, null, -401, null, mOnBatteryTimeBase, in); mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase, in); mMobileRadioActiveUnknownTime = new LongSamplingCounter(mOnBatteryTimeBase, in); @@ -12760,6 +13083,8 @@ public class BatteryStatsImpl extends BatteryStats { mDischargeCounter = new LongSamplingCounter(mOnBatteryTimeBase, in); mDischargeScreenOffCounter = new LongSamplingCounter(mOnBatteryScreenOffTimeBase, in); mDischargeScreenDozeCounter = new LongSamplingCounter(mOnBatteryTimeBase, in); + mDischargeLightDozeCounter = new LongSamplingCounter(mOnBatteryTimeBase, in); + mDischargeDeepDozeCounter = new LongSamplingCounter(mOnBatteryTimeBase, in); mLastWriteTime = in.readLong(); mRpmStats.clear(); @@ -12824,8 +13149,6 @@ public class BatteryStatsImpl extends BatteryStats { mFlashlightTurnedOnTimers.clear(); mCameraTurnedOnTimers.clear(); - mCpuFreqs = in.createLongArray(); - int numUids = in.readInt(); mUidStats.clear(); for (int i = 0; i < numUids; i++) { @@ -12948,6 +13271,8 @@ public class BatteryStatsImpl extends BatteryStats { mDischargeCounter.writeToParcel(out); mDischargeScreenOffCounter.writeToParcel(out); mDischargeScreenDozeCounter.writeToParcel(out); + mDischargeLightDozeCounter.writeToParcel(out); + mDischargeDeepDozeCounter.writeToParcel(out); out.writeLong(mLastWriteTime); out.writeInt(mRpmStats.size()); @@ -12997,7 +13322,7 @@ public class BatteryStatsImpl extends BatteryStats { } } } else { - // TODO: There should be two 0's printed here, not just one. + out.writeInt(0); out.writeInt(0); } @@ -13013,8 +13338,6 @@ public class BatteryStatsImpl extends BatteryStats { } } - out.writeLongArray(mCpuFreqs); - if (inclUids) { int size = mUidStats.size(); out.writeInt(size); diff --git a/core/java/com/android/internal/os/BinderInternal.java b/core/java/com/android/internal/os/BinderInternal.java index ea4575aba9c0..5bddd2f98983 100644 --- a/core/java/com/android/internal/os/BinderInternal.java +++ b/core/java/com/android/internal/os/BinderInternal.java @@ -16,9 +16,15 @@ package com.android.internal.os; +import android.annotation.NonNull; +import android.os.Handler; import android.os.IBinder; import android.os.SystemClock; import android.util.EventLog; +import android.util.Log; +import android.util.SparseIntArray; + +import com.android.internal.util.Preconditions; import dalvik.system.VMRuntime; @@ -31,11 +37,14 @@ import java.util.ArrayList; * @see IBinder */ public class BinderInternal { + private static final String TAG = "BinderInternal"; static WeakReference<GcWatcher> sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher()); static ArrayList<Runnable> sGcWatchers = new ArrayList<>(); static Runnable[] sTmpWatchers = new Runnable[1]; static long sLastGcTime; + static final BinderProxyLimitListenerDelegate sBinderProxyLimitListenerDelegate = + new BinderProxyLimitListenerDelegate(); static final class GcWatcher { @Override @@ -106,4 +115,96 @@ public class BinderInternal { static void forceBinderGc() { forceGc("Binder"); } + + /** + * Enable/disable Binder Proxy Instance Counting by Uid. While enabled, the set callback will + * be called if this process holds too many Binder Proxies on behalf of a Uid. + * @param enabled true to enable counting, false to disable + */ + public static final native void nSetBinderProxyCountEnabled(boolean enabled); + + /** + * Get the current number of Binder Proxies held for each uid. + * @return SparseIntArray mapping uids to the number of Binder Proxies currently held + */ + public static final native SparseIntArray nGetBinderProxyPerUidCounts(); + + /** + * Get the current number of Binder Proxies held for an individual uid. + * @param uid Requested uid for Binder Proxy count + * @return int with the number of Binder proxies held for a uid + */ + public static final native int nGetBinderProxyCount(int uid); + + /** + * Set the Binder Proxy watermarks. Default high watermark = 2500. Default low watermark = 2000 + * @param high The limit at which the BinderProxyListener callback will be called. + * @param low The threshold a binder count must drop below before the callback + * can be called again. (This is to avoid many repeated calls to the + * callback in a brief period of time) + */ + public static final native void nSetBinderProxyCountWatermarks(int high, int low); + + /** + * Interface for callback invocation when the Binder Proxy limit is reached. onLimitReached will + * be called with the uid of the app causing too many Binder Proxies + */ + public interface BinderProxyLimitListener { + public void onLimitReached(int uid); + } + + /** + * Callback used by native code to trigger a callback in java code. The callback will be + * triggered when too many binder proxies from a uid hits the allowed limit. + * @param uid The uid of the bad behaving app sending too many binders + */ + public static void binderProxyLimitCallbackFromNative(int uid) { + sBinderProxyLimitListenerDelegate.notifyClient(uid); + } + + /** + * Set a callback to be triggered when a uid's Binder Proxy limit is reached for this process. + * @param listener OnLimitReached of listener will be called in the thread provided by handler + * @param handler must not be null, callback will be posted through the handler; + * + */ + public static void setBinderProxyCountCallback(BinderProxyLimitListener listener, + @NonNull Handler handler) { + Preconditions.checkNotNull(handler, + "Must provide NonNull Handler to setBinderProxyCountCallback when setting " + + "BinderProxyLimitListener"); + sBinderProxyLimitListenerDelegate.setListener(listener, handler); + } + + /** + * Clear the Binder Proxy callback + */ + public static void clearBinderProxyCountCallback() { + sBinderProxyLimitListenerDelegate.setListener(null, null); + } + + static private class BinderProxyLimitListenerDelegate { + private BinderProxyLimitListener mBinderProxyLimitListener; + private Handler mHandler; + + void setListener(BinderProxyLimitListener listener, Handler handler) { + synchronized (this) { + mBinderProxyLimitListener = listener; + mHandler = handler; + } + } + + void notifyClient(final int uid) { + synchronized (this) { + if (mBinderProxyLimitListener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + mBinderProxyLimitListener.onLimitReached(uid); + } + }); + } + } + } + } } diff --git a/core/java/com/android/internal/os/IShellCallback.aidl b/core/java/com/android/internal/os/IShellCallback.aidl index 57d67890d840..57043424fc47 100644 --- a/core/java/com/android/internal/os/IShellCallback.aidl +++ b/core/java/com/android/internal/os/IShellCallback.aidl @@ -20,5 +20,5 @@ import android.os.ParcelFileDescriptor; /** @hide */ interface IShellCallback { - ParcelFileDescriptor openOutputFile(String path, String seLinuxContext); + ParcelFileDescriptor openFile(String path, String seLinuxContext, String mode); } diff --git a/core/java/com/android/internal/os/KernelUidCpuFreqTimeReader.java b/core/java/com/android/internal/os/KernelUidCpuFreqTimeReader.java index 19e2b8633781..a39997d3dce1 100644 --- a/core/java/com/android/internal/os/KernelUidCpuFreqTimeReader.java +++ b/core/java/com/android/internal/os/KernelUidCpuFreqTimeReader.java @@ -16,8 +16,13 @@ package com.android.internal.os; +import static com.android.internal.util.Preconditions.checkNotNull; + +import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.StrictMode; import android.os.SystemClock; +import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; @@ -47,7 +52,6 @@ public class KernelUidCpuFreqTimeReader { private static final String UID_TIMES_PROC_FILE = "/proc/uid_time_in_state"; public interface Callback { - void onCpuFreqs(long[] cpuFreqs); void onUidCpuFreqTime(int uid, long[] cpuFreqTimeMs); } @@ -63,19 +67,58 @@ public class KernelUidCpuFreqTimeReader { private static final int TOTAL_READ_ERROR_COUNT = 5; private int mReadErrorCounter; private boolean mProcFileAvailable; + private boolean mPerClusterTimesAvailable; - public void readDelta(@Nullable Callback callback) { + public boolean perClusterTimesAvailable() { + return mPerClusterTimesAvailable; + } + + public long[] readFreqs(@NonNull PowerProfile powerProfile) { + checkNotNull(powerProfile); + + if (mCpuFreqs != null) { + // No need to read cpu freqs more than once. + return mCpuFreqs; + } if (!mProcFileAvailable && mReadErrorCounter >= TOTAL_READ_ERROR_COUNT) { + return null; + } + final int oldMask = StrictMode.allowThreadDiskReadsMask(); + try (BufferedReader reader = new BufferedReader(new FileReader(UID_TIMES_PROC_FILE))) { + mProcFileAvailable = true; + return readFreqs(reader, powerProfile); + } catch (IOException e) { + mReadErrorCounter++; + Slog.e(TAG, "Failed to read " + UID_TIMES_PROC_FILE + ": " + e); + return null; + } finally { + StrictMode.setThreadPolicyMask(oldMask); + } + } + + @VisibleForTesting + public long[] readFreqs(BufferedReader reader, PowerProfile powerProfile) + throws IOException { + final String line = reader.readLine(); + if (line == null) { + return null; + } + return readCpuFreqs(line, powerProfile); + } + + public void readDelta(@Nullable Callback callback) { + if (!mProcFileAvailable) { return; } + final int oldMask = StrictMode.allowThreadDiskReadsMask(); try (BufferedReader reader = new BufferedReader(new FileReader(UID_TIMES_PROC_FILE))) { mNowTimeMs = SystemClock.elapsedRealtime(); readDelta(reader, callback); mLastTimeReadMs = mNowTimeMs; - mProcFileAvailable = true; } catch (IOException e) { - mReadErrorCounter++; Slog.e(TAG, "Failed to read " + UID_TIMES_PROC_FILE + ": " + e); + } finally { + StrictMode.setThreadPolicyMask(oldMask); } } @@ -100,7 +143,6 @@ public class KernelUidCpuFreqTimeReader { if (line == null) { return; } - readCpuFreqs(line, callback); while ((line = reader.readLine()) != null) { final int index = line.indexOf(' '); final int uid = Integer.parseInt(line.substring(0, index - 1), 10); @@ -156,18 +198,54 @@ public class KernelUidCpuFreqTimeReader { } } - private void readCpuFreqs(String line, Callback callback) { - if (mCpuFreqs == null) { - final String[] freqStr = line.split(" "); - // First item would be "uid:" which needs to be ignored - mCpuFreqsCount = freqStr.length - 1; - mCpuFreqs = new long[mCpuFreqsCount]; - for (int i = 0; i < mCpuFreqsCount; ++i) { - mCpuFreqs[i] = Long.parseLong(freqStr[i + 1], 10); + private long[] readCpuFreqs(String line, PowerProfile powerProfile) { + final String[] freqStr = line.split(" "); + // First item would be "uid: " which needs to be ignored. + mCpuFreqsCount = freqStr.length - 1; + mCpuFreqs = new long[mCpuFreqsCount]; + for (int i = 0; i < mCpuFreqsCount; ++i) { + mCpuFreqs[i] = Long.parseLong(freqStr[i + 1], 10); + } + + // Check if the freqs in the proc file correspond to per-cluster freqs. + final IntArray numClusterFreqs = extractClusterInfoFromProcFileFreqs(); + final int numClusters = powerProfile.getNumCpuClusters(); + if (numClusterFreqs.size() == numClusters) { + mPerClusterTimesAvailable = true; + for (int i = 0; i < numClusters; ++i) { + if (numClusterFreqs.get(i) != powerProfile.getNumSpeedStepsInCpuCluster(i)) { + mPerClusterTimesAvailable = false; + break; + } } + } else { + mPerClusterTimesAvailable = false; } - if (callback != null) { - callback.onCpuFreqs(mCpuFreqs); + Slog.i(TAG, "mPerClusterTimesAvailable=" + mPerClusterTimesAvailable); + + return mCpuFreqs; + } + + /** + * Extracts no. of cpu clusters and no. of freqs in each of these clusters from the freqs + * read from the proc file. + * + * We need to assume that freqs in each cluster are strictly increasing. + * For e.g. if the freqs read from proc file are: 12, 34, 15, 45, 12, 15, 52. Then it means + * there are 3 clusters: (12, 34), (15, 45), (12, 15, 52) + * + * @return an IntArray filled with no. of freqs in each cluster. + */ + private IntArray extractClusterInfoFromProcFileFreqs() { + final IntArray numClusterFreqs = new IntArray(); + int freqsFound = 0; + for (int i = 0; i < mCpuFreqsCount; ++i) { + freqsFound++; + if (i + 1 == mCpuFreqsCount || mCpuFreqs[i + 1] <= mCpuFreqs[i]) { + numClusterFreqs.add(freqsFound); + freqsFound = 0; + } } + return numClusterFreqs; } } diff --git a/core/java/com/android/internal/os/KernelUidCpuTimeReader.java b/core/java/com/android/internal/os/KernelUidCpuTimeReader.java index 37d9d1d475ee..65615c0ffb02 100644 --- a/core/java/com/android/internal/os/KernelUidCpuTimeReader.java +++ b/core/java/com/android/internal/os/KernelUidCpuTimeReader.java @@ -16,6 +16,7 @@ package com.android.internal.os; import android.annotation.Nullable; +import android.os.StrictMode; import android.os.SystemClock; import android.text.TextUtils; import android.util.Slog; @@ -65,6 +66,7 @@ public class KernelUidCpuTimeReader { * a fresh delta. */ public void readDelta(@Nullable Callback callback) { + final int oldMask = StrictMode.allowThreadDiskReadsMask(); long nowUs = SystemClock.elapsedRealtime() * 1000; try (BufferedReader reader = new BufferedReader(new FileReader(sProcFile))) { TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(' '); @@ -121,6 +123,8 @@ public class KernelUidCpuTimeReader { } } catch (IOException e) { Slog.e(TAG, "Failed to read uid_cputime: " + e.getMessage()); + } finally { + StrictMode.setThreadPolicyMask(oldMask); } mLastTimeReadUs = nowUs; } @@ -160,12 +164,15 @@ public class KernelUidCpuTimeReader { private void removeUidsFromKernelModule(int startUid, int endUid) { Slog.d(TAG, "Removing uids " + startUid + "-" + endUid); + final int oldMask = StrictMode.allowThreadDiskWritesMask(); try (FileWriter writer = new FileWriter(sRemoveUidProcFile)) { writer.write(startUid + "-" + endUid); writer.flush(); } catch (IOException e) { Slog.e(TAG, "failed to remove uids " + startUid + " - " + endUid + " from uid_cputime module", e); + } finally { + StrictMode.setThreadPolicyMask(oldMask); } } } diff --git a/core/java/com/android/internal/os/SomeArgs.java b/core/java/com/android/internal/os/SomeArgs.java index 8fb56d4757b6..d9aa32532ccd 100644 --- a/core/java/com/android/internal/os/SomeArgs.java +++ b/core/java/com/android/internal/os/SomeArgs.java @@ -48,6 +48,7 @@ public final class SomeArgs { public Object arg6; public Object arg7; public Object arg8; + public Object arg9; public int argi1; public int argi2; public int argi3; diff --git a/core/java/com/android/internal/os/TransferPipe.java b/core/java/com/android/internal/os/TransferPipe.java index f9041507ffdd..738ecc0bdaa0 100644 --- a/core/java/com/android/internal/os/TransferPipe.java +++ b/core/java/com/android/internal/os/TransferPipe.java @@ -16,12 +16,8 @@ package com.android.internal.os; -import java.io.Closeable; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; - +import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Binder; import android.os.IBinder; import android.os.IInterface; @@ -30,6 +26,15 @@ import android.os.RemoteException; import android.os.SystemClock; import android.util.Slog; +import libcore.io.IoUtils; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + /** * Helper for transferring data through a pipe from a client app. */ @@ -81,6 +86,45 @@ public final class TransferPipe implements Runnable, Closeable { goDump(binder, out, args); } + /** + * Read raw bytes from a service's dump function. + * + * <p>This can be used for dumping {@link android.util.proto.ProtoOutputStream protos}. + * + * @param binder The service providing the data + * @param args The arguments passed to the dump function of the service + */ + public static byte[] dumpAsync(@NonNull IBinder binder, @Nullable String... args) + throws IOException, RemoteException { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + try { + TransferPipe.dumpAsync(binder, pipe[1].getFileDescriptor(), args); + + // Data is written completely when dumpAsync is done + pipe[1].close(); + pipe[1] = null; + + byte[] buffer = new byte[4096]; + try (ByteArrayOutputStream combinedBuffer = new ByteArrayOutputStream()) { + try (FileInputStream is = new FileInputStream(pipe[0].getFileDescriptor())) { + while (true) { + int numRead = is.read(buffer); + if (numRead == -1) { + break; + } + + combinedBuffer.write(buffer, 0, numRead); + } + } + + return combinedBuffer.toByteArray(); + } + } finally { + pipe[0].close(); + IoUtils.closeQuietly(pipe[1]); + } + } + static void go(Caller caller, IInterface iface, FileDescriptor out, String prefix, String[] args) throws IOException, RemoteException { go(caller, iface, out, prefix, args, DEFAULT_TIMEOUT); diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 3ee8b472869b..cbc63cf813cb 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -16,14 +16,12 @@ package com.android.internal.os; - +import android.os.IVold; import android.os.Trace; -import dalvik.system.ZygoteHooks; import android.system.ErrnoException; import android.system.Os; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import dalvik.system.ZygoteHooks; /** @hide */ public final class Zygote { @@ -57,13 +55,13 @@ public final class Zygote { public static final int ONLY_USE_SYSTEM_OAT_FILES = 1 << 10; /** No external storage should be mounted. */ - public static final int MOUNT_EXTERNAL_NONE = 0; + public static final int MOUNT_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE; /** Default external storage should be mounted. */ - public static final int MOUNT_EXTERNAL_DEFAULT = 1; + public static final int MOUNT_EXTERNAL_DEFAULT = IVold.REMOUNT_MODE_DEFAULT; /** Read-only external storage should be mounted. */ - public static final int MOUNT_EXTERNAL_READ = 2; + public static final int MOUNT_EXTERNAL_READ = IVold.REMOUNT_MODE_READ; /** Read-write external storage should be mounted. */ - public static final int MOUNT_EXTERNAL_WRITE = 3; + public static final int MOUNT_EXTERNAL_WRITE = IVold.REMOUNT_MODE_WRITE; private static final ZygoteHooks VM_HOOKS = new ZygoteHooks(); diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 4e06577e3588..5fddfba632c5 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -16,6 +16,7 @@ package com.android.internal.policy; +import android.app.WindowConfiguration; import android.graphics.Outline; import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.LayerDrawable; @@ -85,11 +86,8 @@ import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.PopupWindow; -import static android.app.ActivityManager.StackId; -import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; -import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; -import static android.app.ActivityManager.StackId.PINNED_STACK_ID; -import static android.app.ActivityManager.StackId.INVALID_STACK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; @@ -237,10 +235,6 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // If the window type does not require such a view, this member might be null. DecorCaptionView mDecorCaptionView; - // Stack window is currently in. Since querying and changing the stack is expensive, - // this is the stack value the window is currently set up for. - int mStackId; - private boolean mWindowResizeCallbacksAdded = false; private Drawable.Callback mLastBackgroundDrawableCb = null; private BackdropFrameRenderer mBackdropFrameRenderer = null; @@ -437,7 +431,11 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } - return super.dispatchKeyEvent(event); + if (super.dispatchKeyEvent(event)) { + return true; + } + + return (getViewRootImpl() != null) && getViewRootImpl().dispatchKeyFallbackEvent(event); } public boolean superDispatchKeyShortcutEvent(KeyEvent event) { @@ -1502,7 +1500,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind invalidate(); int opacity = PixelFormat.OPAQUE; - if (StackId.hasWindowShadow(mStackId)) { + final WindowConfiguration winConfig = getResources().getConfiguration().windowConfiguration; + if (winConfig.hasWindowShadow()) { // If the window has a shadow, it must be translucent. opacity = PixelFormat.TRANSLUCENT; } else{ @@ -1892,35 +1891,33 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - int workspaceId = getStackId(); - if (mStackId != workspaceId) { - mStackId = workspaceId; - if (mDecorCaptionView == null && StackId.hasWindowDecor(mStackId)) { - // Configuration now requires a caption. - final LayoutInflater inflater = mWindow.getLayoutInflater(); - mDecorCaptionView = createDecorCaptionView(inflater); - if (mDecorCaptionView != null) { - if (mDecorCaptionView.getParent() == null) { - addView(mDecorCaptionView, 0, - new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - } - removeView(mContentRoot); - mDecorCaptionView.addView(mContentRoot, - new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); + + final boolean displayWindowDecor = + newConfig.windowConfiguration.hasWindowDecorCaption(); + if (mDecorCaptionView == null && displayWindowDecor) { + // Configuration now requires a caption. + final LayoutInflater inflater = mWindow.getLayoutInflater(); + mDecorCaptionView = createDecorCaptionView(inflater); + if (mDecorCaptionView != null) { + if (mDecorCaptionView.getParent() == null) { + addView(mDecorCaptionView, 0, + new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } - } else if (mDecorCaptionView != null) { - // We might have to change the kind of surface before we do anything else. - mDecorCaptionView.onConfigurationChanged(StackId.hasWindowDecor(mStackId)); - enableCaption(StackId.hasWindowDecor(workspaceId)); + removeView(mContentRoot); + mDecorCaptionView.addView(mContentRoot, + new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); } + } else if (mDecorCaptionView != null) { + // We might have to change the kind of surface before we do anything else. + mDecorCaptionView.onConfigurationChanged(displayWindowDecor); + enableCaption(displayWindowDecor); } + updateAvailableWidth(); initializeElevation(); } void onResourcesLoaded(LayoutInflater inflater, int layoutResource) { - mStackId = getStackId(); - if (mBackdropFrameRenderer != null) { loadBackgroundDrawablesIfNeeded(); mBackdropFrameRenderer.onResourcesLoaded( @@ -1982,8 +1979,9 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind final WindowManager.LayoutParams attrs = mWindow.getAttributes(); final boolean isApplication = attrs.type == TYPE_BASE_APPLICATION || attrs.type == TYPE_APPLICATION || attrs.type == TYPE_DRAWN_APPLICATION; + final WindowConfiguration winConfig = getResources().getConfiguration().windowConfiguration; // Only a non floating application window on one of the allowed workspaces can get a caption - if (!mWindow.isFloating() && isApplication && StackId.hasWindowDecor(mStackId)) { + if (!mWindow.isFloating() && isApplication && winConfig.hasWindowDecorCaption()) { // Dependent on the brightness of the used title we either use the // dark or the light button frame. if (decorCaptionView == null) { @@ -2096,28 +2094,6 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind return drawable; } - /** - * Returns the Id of the stack which contains this window. - * Note that if no stack can be determined - which usually means that it was not - * created for an activity - the fullscreen stack ID will be returned. - * @return Returns the stack id which contains this window. - **/ - private int getStackId() { - int workspaceId = INVALID_STACK_ID; - final Window.WindowControllerCallback callback = mWindow.getWindowControllerCallback(); - if (callback != null) { - try { - workspaceId = callback.getWindowStackId(); - } catch (RemoteException ex) { - Log.e(mLogTag, "Failed to get the workspace ID of a PhoneWindow."); - } - } - if (workspaceId == INVALID_STACK_ID) { - return FULLSCREEN_WORKSPACE_STACK_ID; - } - return workspaceId; - } - void clearContentView() { if (mDecorCaptionView != null) { mDecorCaptionView.removeContentView(); @@ -2270,7 +2246,9 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind final boolean wasAdjustedForStack = mElevationAdjustedForStack; // Do not use a shadow when we are in resizing mode (mBackdropFrameRenderer not null) // since the shadow is bound to the content size and not the target size. - if ((mStackId == FREEFORM_WORKSPACE_STACK_ID) && !isResizing()) { + final int windowingMode = + getResources().getConfiguration().windowConfiguration.getWindowingMode(); + if ((windowingMode == WINDOWING_MODE_FREEFORM) && !isResizing()) { elevation = hasWindowFocus() ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP : DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP; // Add a maximum shadow height value to the top level view. @@ -2283,7 +2261,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // Convert the DP elevation into physical pixels. elevation = dipToPx(elevation); mElevationAdjustedForStack = true; - } else if (mStackId == PINNED_STACK_ID) { + } else if (windowingMode == WINDOWING_MODE_PINNED) { elevation = dipToPx(DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP); mElevationAdjustedForStack = true; } else { diff --git a/core/java/com/android/internal/policy/DividerSnapAlgorithm.java b/core/java/com/android/internal/policy/DividerSnapAlgorithm.java index fb6b8b0b2e16..3af3e2ad2772 100644 --- a/core/java/com/android/internal/policy/DividerSnapAlgorithm.java +++ b/core/java/com/android/internal/policy/DividerSnapAlgorithm.java @@ -16,6 +16,10 @@ package com.android.internal.policy; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; + import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -99,11 +103,12 @@ public class DividerSnapAlgorithm { public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets) { - this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets, false); + this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets, + DOCKED_INVALID, false); } public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, - boolean isHorizontalDivision, Rect insets, boolean isMinimizedMode) { + boolean isHorizontalDivision, Rect insets, int dockSide, boolean isMinimizedMode) { mMinFlingVelocityPxPerSecond = MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; mMinDismissVelocityPxPerSecond = @@ -121,7 +126,7 @@ public class DividerSnapAlgorithm { com.android.internal.R.dimen.default_minimal_size_resizable_task); mTaskHeightInMinimizedMode = res.getDimensionPixelSize( com.android.internal.R.dimen.task_height_of_minimized_mode); - calculateTargets(isHorizontalDivision); + calculateTargets(isHorizontalDivision, dockSide); mFirstSplitTarget = mTargets.get(1); mLastSplitTarget = mTargets.get(mTargets.size() - 2); mDismissStartTarget = mTargets.get(0); @@ -254,7 +259,7 @@ public class DividerSnapAlgorithm { return mTargets.get(minIndex); } - private void calculateTargets(boolean isHorizontalDivision) { + private void calculateTargets(boolean isHorizontalDivision, int dockedSide) { mTargets.clear(); int dividerMax = isHorizontalDivision ? mDisplayHeight @@ -273,7 +278,7 @@ public class DividerSnapAlgorithm { addMiddleTarget(isHorizontalDivision); break; case SNAP_MODE_MINIMIZED: - addMinimizedTarget(isHorizontalDivision); + addMinimizedTarget(isHorizontalDivision, dockedSide); break; } mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax, @@ -331,12 +336,16 @@ public class DividerSnapAlgorithm { mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); } - private void addMinimizedTarget(boolean isHorizontalDivision) { + private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) { // In portrait offset the position by the statusbar height, in landscape add the statusbar // height as well to match portrait offset int position = mTaskHeightInMinimizedMode + mInsets.top; if (!isHorizontalDivision) { - position += mInsets.left; + if (dockedSide == DOCKED_LEFT) { + position += mInsets.left; + } else if (dockedSide == DOCKED_RIGHT) { + position = mDisplayWidth - position - mInsets.right; + } } mTargets.add(new SnapTarget(position, position, SnapTarget.FLAG_NONE)); } diff --git a/core/java/com/android/internal/print/DumpUtils.java b/core/java/com/android/internal/print/DumpUtils.java new file mode 100644 index 000000000000..28c7fc2182b2 --- /dev/null +++ b/core/java/com/android/internal/print/DumpUtils.java @@ -0,0 +1,356 @@ +/* + * 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 com.android.internal.print; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.ComponentNameProto; +import android.content.Context; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintDocumentInfo; +import android.print.PrintJobId; +import android.print.PrintJobInfo; +import android.print.PrinterCapabilitiesInfo; +import android.print.PrinterId; +import android.print.PrinterInfo; +import android.service.print.MarginsProto; +import android.service.print.MediaSizeProto; +import android.service.print.PageRangeProto; +import android.service.print.PrintAttributesProto; +import android.service.print.PrintDocumentInfoProto; +import android.service.print.PrintJobInfoProto; +import android.service.print.PrinterCapabilitiesProto; +import android.service.print.PrinterIdProto; +import android.service.print.PrinterInfoProto; +import android.service.print.ResolutionProto; +import android.util.proto.ProtoOutputStream; + +/** + * Utilities for dumping print related proto buffer + */ +public class DumpUtils { + /** + * Write a string to a proto if the string is not {@code null}. + * + * @param proto The proto to write to + * @param id The proto-id of the string + * @param string The string to write + */ + public static void writeStringIfNotNull(@NonNull ProtoOutputStream proto, long id, + @Nullable String string) { + if (string != null) { + proto.write(id, string); + } + } + + /** + * Write a {@link ComponentName} to a proto. + * + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param component The component name to write + */ + public static void writeComponentName(@NonNull ProtoOutputStream proto, long id, + @NonNull ComponentName component) { + long token = proto.start(id); + proto.write(ComponentNameProto.PACKAGE_NAME, component.getPackageName()); + proto.write(ComponentNameProto.CLASS_NAME, component.getClassName()); + proto.end(token); + } + + /** + * Write a {@link PrinterId} to a proto. + * + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param printerId The printer id to write + */ + public static void writePrinterId(@NonNull ProtoOutputStream proto, long id, + @NonNull PrinterId printerId) { + long token = proto.start(id); + writeComponentName(proto, PrinterIdProto.SERVICE_NAME, printerId.getServiceName()); + proto.write(PrinterIdProto.LOCAL_ID, printerId.getLocalId()); + proto.end(token); + } + + /** + * Write a {@link PrinterCapabilitiesInfo} to a proto. + * + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param cap The capabilities to write + */ + public static void writePrinterCapabilities(@NonNull Context context, + @NonNull ProtoOutputStream proto, long id, @NonNull PrinterCapabilitiesInfo cap) { + long token = proto.start(id); + writeMargins(proto, PrinterCapabilitiesProto.MIN_MARGINS, cap.getMinMargins()); + + int numMediaSizes = cap.getMediaSizes().size(); + for (int i = 0; i < numMediaSizes; i++) { + writeMediaSize(context, proto, PrinterCapabilitiesProto.MEDIA_SIZES, + cap.getMediaSizes().get(i)); + } + + int numResolutions = cap.getResolutions().size(); + for (int i = 0; i < numResolutions; i++) { + writeResolution(proto, PrinterCapabilitiesProto.RESOLUTIONS, + cap.getResolutions().get(i)); + } + + if ((cap.getColorModes() & PrintAttributes.COLOR_MODE_MONOCHROME) != 0) { + proto.write(PrinterCapabilitiesProto.COLOR_MODES, + PrintAttributesProto.COLOR_MODE_MONOCHROME); + } + if ((cap.getColorModes() & PrintAttributes.COLOR_MODE_COLOR) != 0) { + proto.write(PrinterCapabilitiesProto.COLOR_MODES, + PrintAttributesProto.COLOR_MODE_COLOR); + } + + if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_NONE) != 0) { + proto.write(PrinterCapabilitiesProto.DUPLEX_MODES, + PrintAttributesProto.DUPLEX_MODE_NONE); + } + if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_LONG_EDGE) != 0) { + proto.write(PrinterCapabilitiesProto.DUPLEX_MODES, + PrintAttributesProto.DUPLEX_MODE_LONG_EDGE); + } + if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_SHORT_EDGE) != 0) { + proto.write(PrinterCapabilitiesProto.DUPLEX_MODES, + PrintAttributesProto.DUPLEX_MODE_SHORT_EDGE); + } + + proto.end(token); + } + + + /** + * Write a {@link PrinterInfo} to a proto. + * + * @param context The context used to resolve resources + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param info The printer info to write + */ + public static void writePrinterInfo(@NonNull Context context, @NonNull ProtoOutputStream proto, + long id, @NonNull PrinterInfo info) { + long token = proto.start(id); + writePrinterId(proto, PrinterInfoProto.ID, info.getId()); + proto.write(PrinterInfoProto.NAME, info.getName()); + proto.write(PrinterInfoProto.STATUS, info.getStatus()); + proto.write(PrinterInfoProto.DESCRIPTION, info.getDescription()); + + PrinterCapabilitiesInfo cap = info.getCapabilities(); + if (cap != null) { + writePrinterCapabilities(context, proto, PrinterInfoProto.CAPABILITIES, cap); + } + + proto.end(token); + } + + /** + * Write a {@link PrintAttributes.MediaSize} to a proto. + * + * @param context The context used to resolve resources + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param mediaSize The media size to write + */ + public static void writeMediaSize(@NonNull Context context, @NonNull ProtoOutputStream proto, + long id, @NonNull PrintAttributes.MediaSize mediaSize) { + long token = proto.start(id); + proto.write(MediaSizeProto.ID, mediaSize.getId()); + proto.write(MediaSizeProto.LABEL, mediaSize.getLabel(context.getPackageManager())); + proto.write(MediaSizeProto.HEIGHT_MILS, mediaSize.getHeightMils()); + proto.write(MediaSizeProto.WIDTH_MILS, mediaSize.getWidthMils()); + proto.end(token); + } + + /** + * Write a {@link PrintAttributes.Resolution} to a proto. + * + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param res The resolution to write + */ + public static void writeResolution(@NonNull ProtoOutputStream proto, long id, + @NonNull PrintAttributes.Resolution res) { + long token = proto.start(id); + proto.write(ResolutionProto.ID, res.getId()); + proto.write(ResolutionProto.LABEL, res.getLabel()); + proto.write(ResolutionProto.HORIZONTAL_DPI, res.getHorizontalDpi()); + proto.write(ResolutionProto.VERTICAL_DPI, res.getVerticalDpi()); + proto.end(token); + } + + /** + * Write a {@link PrintAttributes.Margins} to a proto. + * + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param margins The margins to write + */ + public static void writeMargins(@NonNull ProtoOutputStream proto, long id, + @NonNull PrintAttributes.Margins margins) { + long token = proto.start(id); + proto.write(MarginsProto.TOP_MILS, margins.getTopMils()); + proto.write(MarginsProto.LEFT_MILS, margins.getLeftMils()); + proto.write(MarginsProto.RIGHT_MILS, margins.getRightMils()); + proto.write(MarginsProto.BOTTOM_MILS, margins.getBottomMils()); + proto.end(token); + } + + /** + * Write a {@link PrintAttributes} to a proto. + * + * @param context The context used to resolve resources + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param attributes The attributes to write + */ + public static void writePrintAttributes(@NonNull Context context, + @NonNull ProtoOutputStream proto, long id, @NonNull PrintAttributes attributes) { + long token = proto.start(id); + + PrintAttributes.MediaSize mediaSize = attributes.getMediaSize(); + if (mediaSize != null) { + writeMediaSize(context, proto, PrintAttributesProto.MEDIA_SIZE, mediaSize); + } + + proto.write(PrintAttributesProto.IS_PORTRAIT, attributes.isPortrait()); + + PrintAttributes.Resolution res = attributes.getResolution(); + if (res != null) { + writeResolution(proto, PrintAttributesProto.RESOLUTION, res); + } + + PrintAttributes.Margins minMargins = attributes.getMinMargins(); + if (minMargins != null) { + writeMargins(proto, PrintAttributesProto.MIN_MARGINS, minMargins); + } + + proto.write(PrintAttributesProto.COLOR_MODE, attributes.getColorMode()); + proto.write(PrintAttributesProto.DUPLEX_MODE, attributes.getDuplexMode()); + proto.end(token); + } + + /** + * Write a {@link PrintDocumentInfo} to a proto. + * + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param info The info to write + */ + public static void writePrintDocumentInfo(@NonNull ProtoOutputStream proto, long id, + @NonNull PrintDocumentInfo info) { + long token = proto.start(id); + proto.write(PrintDocumentInfoProto.NAME, info.getName()); + + int pageCount = info.getPageCount(); + if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) { + proto.write(PrintDocumentInfoProto.PAGE_COUNT, pageCount); + } + + proto.write(PrintDocumentInfoProto.CONTENT_TYPE, info.getContentType()); + proto.write(PrintDocumentInfoProto.DATA_SIZE, info.getDataSize()); + proto.end(token); + } + + /** + * Write a {@link PageRange} to a proto. + * + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param range The range to write + */ + public static void writePageRange(@NonNull ProtoOutputStream proto, long id, + @NonNull PageRange range) { + long token = proto.start(id); + proto.write(PageRangeProto.START, range.getStart()); + proto.write(PageRangeProto.END, range.getEnd()); + proto.end(token); + } + + /** + * Write a {@link PrintJobInfo} to a proto. + * + * @param context The context used to resolve resources + * @param proto The proto to write to + * @param id The proto-id of the component name + * @param printJobInfo The print job info to write + */ + public static void writePrintJobInfo(@NonNull Context context, @NonNull ProtoOutputStream proto, + long id, @NonNull PrintJobInfo printJobInfo) { + long token = proto.start(id); + proto.write(PrintJobInfoProto.LABEL, printJobInfo.getLabel()); + + PrintJobId printJobId = printJobInfo.getId(); + if (printJobId != null) { + proto.write(PrintJobInfoProto.PRINT_JOB_ID, printJobId.flattenToString()); + } + + int state = printJobInfo.getState(); + if (state >= PrintJobInfoProto.STATE_CREATED && state <= PrintJobInfoProto.STATE_CANCELED) { + proto.write(PrintJobInfoProto.STATE, state); + } else { + proto.write(PrintJobInfoProto.STATE, PrintJobInfoProto.STATE_UNKNOWN); + } + + PrinterId printer = printJobInfo.getPrinterId(); + if (printer != null) { + writePrinterId(proto, PrintJobInfoProto.PRINTER, printer); + } + + String tag = printJobInfo.getTag(); + if (tag != null) { + proto.write(PrintJobInfoProto.TAG, tag); + } + + proto.write(PrintJobInfoProto.CREATION_TIME, printJobInfo.getCreationTime()); + + PrintAttributes attributes = printJobInfo.getAttributes(); + if (attributes != null) { + writePrintAttributes(context, proto, PrintJobInfoProto.ATTRIBUTES, attributes); + } + + PrintDocumentInfo docInfo = printJobInfo.getDocumentInfo(); + if (docInfo != null) { + writePrintDocumentInfo(proto, PrintJobInfoProto.DOCUMENT_INFO, docInfo); + } + + proto.write(PrintJobInfoProto.IS_CANCELING, printJobInfo.isCancelling()); + + PageRange[] pages = printJobInfo.getPages(); + if (pages != null) { + for (int i = 0; i < pages.length; i++) { + writePageRange(proto, PrintJobInfoProto.PAGES, pages[i]); + } + } + + proto.write(PrintJobInfoProto.HAS_ADVANCED_OPTIONS, + printJobInfo.getAdvancedOptions() != null); + proto.write(PrintJobInfoProto.PROGRESS, printJobInfo.getProgress()); + + CharSequence status = printJobInfo.getStatus(context.getPackageManager()); + if (status != null) { + proto.write(PrintJobInfoProto.STATUS, status.toString()); + } + + proto.end(token); + } +} diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index bab0306aaf9f..5ec90941aa88 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -113,6 +113,11 @@ oneway interface IStatusBar void showGlobalActionsMenu(); /** + * Notifies the status bar that a new rotation suggestion is available. + */ + void onProposedRotationChanged(int rotation); + + /** * Set whether the top app currently hides the statusbar. * * @param hidesStatusBar whether it is being hidden diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 82eb1abcba4d..03603e401110 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -58,10 +58,12 @@ interface IStatusBarService void onNotificationError(String pkg, String tag, int id, int uid, int initialPid, String message, int userId); void onClearAllNotifications(int userId); - void onNotificationClear(String pkg, String tag, int id, int userId); + void onNotificationClear(String pkg, String tag, int id, int userId, String key, int dismissalSurface); void onNotificationVisibilityChanged( in NotificationVisibility[] newlyVisibleKeys, in NotificationVisibility[] noLongerVisibleKeys); void onNotificationExpansionChanged(in String key, in boolean userAction, in boolean expanded); + void onNotificationDirectReplied(String key); + void onNotificationSettingsViewed(String key); void setSystemUiVisibility(int vis, int mask, String cause); void onGlobalActionsShown(); diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 91bc6813c5fc..aa8566885aff 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -134,6 +135,13 @@ public class ArrayUtils { } /** + * Checks if given map is null or has zero elements. + */ + public static boolean isEmpty(@Nullable Map<?, ?> map) { + return map == null || map.isEmpty(); + } + + /** * Checks if given array is null or has zero elements. */ public static <T> boolean isEmpty(@Nullable T[] array) { @@ -284,6 +292,15 @@ public class ArrayUtils { return array; } + public static @Nullable long[] convertToLongArray(@Nullable int[] intArray) { + if (intArray == null) return null; + long[] array = new long[intArray.length]; + for (int i = 0; i < intArray.length; i++) { + array[i] = (long) intArray[i]; + } + return array; + } + /** * Adds value to given array if not already present, providing set-like * behavior. @@ -417,14 +434,17 @@ public class ArrayUtils { * Adds value to given array if not already present, providing set-like * behavior. */ - public static @NonNull long[] appendLong(@Nullable long[] cur, long val) { + public static @NonNull long[] appendLong(@Nullable long[] cur, long val, + boolean allowDuplicates) { if (cur == null) { return new long[] { val }; } final int N = cur.length; - for (int i = 0; i < N; i++) { - if (cur[i] == val) { - return cur; + if (!allowDuplicates) { + for (int i = 0; i < N; i++) { + if (cur[i] == val) { + return cur; + } } } long[] ret = new long[N + 1]; @@ -434,6 +454,14 @@ public class ArrayUtils { } /** + * Adds value to given array if not already present, providing set-like + * behavior. + */ + public static @NonNull long[] appendLong(@Nullable long[] cur, long val) { + return appendLong(cur, val, false); + } + + /** * Removes value from given array if present, providing set-like behavior. */ public static @Nullable long[] removeLong(@Nullable long[] cur, long val) { diff --git a/core/java/com/android/internal/util/CollectionUtils.java b/core/java/com/android/internal/util/CollectionUtils.java index f0b47de8be98..f983de17e0b1 100644 --- a/core/java/com/android/internal/util/CollectionUtils.java +++ b/core/java/com/android/internal/util/CollectionUtils.java @@ -30,7 +30,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.function.*; +import java.util.function.Function; import java.util.stream.Stream; /** diff --git a/core/java/com/android/internal/util/FunctionalUtils.java b/core/java/com/android/internal/util/FunctionalUtils.java index cdef97e84f62..eb92c1c0dfb2 100644 --- a/core/java/com/android/internal/util/FunctionalUtils.java +++ b/core/java/com/android/internal/util/FunctionalUtils.java @@ -32,7 +32,7 @@ public class FunctionalUtils { */ @FunctionalInterface public interface ThrowingRunnable { - void run() throws Exception; + void runOrThrow() throws Exception; } /** @@ -43,7 +43,7 @@ public class FunctionalUtils { */ @FunctionalInterface public interface ThrowingSupplier<T> { - T get() throws Exception; + T getOrThrow() throws Exception; } /** diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java new file mode 100644 index 000000000000..72cd24888dcc --- /dev/null +++ b/core/java/com/android/internal/util/LatencyTracker.java @@ -0,0 +1,154 @@ +/* + * 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 com.android.internal.util; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.Trace; +import android.util.EventLog; +import android.util.Log; +import android.util.SparseLongArray; + +import com.android.internal.logging.EventLogTags; + +/** + * Class to track various latencies in SystemUI. It then outputs the latency to logcat so these + * latencies can be captured by tests and then used for dashboards. + * <p> + * This is currently only in Keyguard so it can be shared between SystemUI and Keyguard, but + * eventually we'd want to merge these two packages together so Keyguard can use common classes + * that are shared with SystemUI. + */ +public class LatencyTracker { + + private static final String ACTION_RELOAD_PROPERTY = + "com.android.systemui.RELOAD_LATENCY_TRACKER_PROPERTY"; + + private static final String TAG = "LatencyTracker"; + + /** + * Time it takes until the first frame of the notification panel to be displayed while expanding + */ + public static final int ACTION_EXPAND_PANEL = 0; + + /** + * Time it takes until the first frame of recents is drawn after invoking it with the button. + */ + public static final int ACTION_TOGGLE_RECENTS = 1; + + /** + * Time between we get a fingerprint acquired signal until we start with the unlock animation + */ + public static final int ACTION_FINGERPRINT_WAKE_AND_UNLOCK = 2; + + /** + * Time it takes to check PIN/Pattern/Password. + */ + public static final int ACTION_CHECK_CREDENTIAL = 3; + + /** + * Time it takes to check fully PIN/Pattern/Password, i.e. that's the time spent including the + * actions to unlock a user. + */ + public static final int ACTION_CHECK_CREDENTIAL_UNLOCKED = 4; + + /** + * Time it takes to turn on the screen. + */ + public static final int ACTION_TURN_ON_SCREEN = 5; + + /** + * Time it takes to rotate the screen. + */ + public static final int ACTION_ROTATE_SCREEN = 6; + + private static final String[] NAMES = new String[] { + "expand panel", + "toggle recents", + "fingerprint wake-and-unlock", + "check credential", + "check credential unlocked", + "turn on screen", + "rotate the screen"}; + + private static LatencyTracker sLatencyTracker; + + private final SparseLongArray mStartRtc = new SparseLongArray(); + private boolean mEnabled; + + public static LatencyTracker getInstance(Context context) { + if (sLatencyTracker == null) { + sLatencyTracker = new LatencyTracker(context); + } + return sLatencyTracker; + } + + private LatencyTracker(Context context) { + context.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + reloadProperty(); + } + }, new IntentFilter(ACTION_RELOAD_PROPERTY)); + reloadProperty(); + } + + private void reloadProperty() { + mEnabled = SystemProperties.getBoolean("debug.systemui.latency_tracking", false); + } + + public static boolean isEnabled(Context ctx) { + return Build.IS_DEBUGGABLE && getInstance(ctx).mEnabled; + } + + /** + * Notifies that an action is starting. This needs to be called from the main thread. + * + * @param action The action to start. One of the ACTION_* values. + */ + public void onActionStart(int action) { + if (!mEnabled) { + return; + } + Trace.asyncTraceBegin(Trace.TRACE_TAG_APP, NAMES[action], 0); + mStartRtc.put(action, SystemClock.elapsedRealtime()); + } + + /** + * Notifies that an action has ended. This needs to be called from the main thread. + * + * @param action The action to end. One of the ACTION_* values. + */ + public void onActionEnd(int action) { + if (!mEnabled) { + return; + } + long endRtc = SystemClock.elapsedRealtime(); + long startRtc = mStartRtc.get(action, -1); + if (startRtc == -1) { + return; + } + mStartRtc.delete(action); + Trace.asyncTraceEnd(Trace.TRACE_TAG_APP, NAMES[action], 0); + long duration = endRtc - startRtc; + Log.i(TAG, "action=" + action + " latency=" + duration); + EventLog.writeEvent(EventLogTags.SYSUI_LATENCY, action, (int) duration); + } +} diff --git a/core/java/com/android/internal/util/LocalLog.java b/core/java/com/android/internal/util/LocalLog.java index f0e6171562f9..8edb739f273c 100644 --- a/core/java/com/android/internal/util/LocalLog.java +++ b/core/java/com/android/internal/util/LocalLog.java @@ -20,6 +20,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import android.util.Slog; +import android.util.proto.ProtoOutputStream; /** * Helper class for logging serious issues, which also keeps a small @@ -63,4 +64,16 @@ public class LocalLog { return true; } } + + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + + synchronized (mLines) { + for (int i = 0; i < mLines.size(); ++i) { + proto.write(LocalLogProto.LINES, mLines.get(i)); + } + } + + proto.end(token); + } } diff --git a/core/java/com/android/internal/util/RingBuffer.java b/core/java/com/android/internal/util/RingBuffer.java index ad84353f23a9..9a6e542ccbbd 100644 --- a/core/java/com/android/internal/util/RingBuffer.java +++ b/core/java/com/android/internal/util/RingBuffer.java @@ -45,10 +45,40 @@ public class RingBuffer<T> { return (int) Math.min(mBuffer.length, (long) mCursor); } + public boolean isEmpty() { + return size() == 0; + } + + public void clear() { + for (int i = 0; i < size(); ++i) { + mBuffer[i] = null; + } + mCursor = 0; + } + public void append(T t) { mBuffer[indexOf(mCursor++)] = t; } + /** + * Returns object of type <T> at the next writable slot, creating one if it is not already + * available. In case of any errors while creating the object, <code>null</code> will + * be returned. + */ + public T getNextSlot() { + final int nextSlotIdx = indexOf(mCursor++); + T item = mBuffer[nextSlotIdx]; + if (item == null) { + try { + item = (T) mBuffer.getClass().getComponentType().newInstance(); + } catch (IllegalAccessException | InstantiationException e) { + return null; + } + mBuffer[nextSlotIdx] = item; + } + return item; + } + public T[] toArray() { // Only generic way to create a T[] from another T[] T[] out = Arrays.copyOf(mBuffer, size(), (Class<T[]>) mBuffer.getClass()); diff --git a/core/java/com/android/internal/util/UserIcons.java b/core/java/com/android/internal/util/UserIcons.java index daf745ff6a2c..bfe43237da58 100644 --- a/core/java/com/android/internal/util/UserIcons.java +++ b/core/java/com/android/internal/util/UserIcons.java @@ -61,17 +61,19 @@ public class UserIcons { * Returns a default user icon for the given user. * * Note that for guest users, you should pass in {@code UserHandle.USER_NULL}. + * + * @param resources resources object to fetch user icon / color. * @param userId the user id or {@code UserHandle.USER_NULL} for a non-user specific icon * @param light whether we want a light icon (suitable for a dark background) */ - public static Drawable getDefaultUserIcon(int userId, boolean light) { + public static Drawable getDefaultUserIcon(Resources resources, int userId, boolean light) { int colorResId = light ? R.color.user_icon_default_white : R.color.user_icon_default_gray; if (userId != UserHandle.USER_NULL) { // Return colored icon instead colorResId = USER_ICON_COLORS[userId % USER_ICON_COLORS.length]; } - Drawable icon = Resources.getSystem().getDrawable(R.drawable.ic_account_circle, null).mutate(); - icon.setColorFilter(Resources.getSystem().getColor(colorResId, null), Mode.SRC_IN); + Drawable icon = resources.getDrawable(R.drawable.ic_account_circle, null).mutate(); + icon.setColorFilter(resources.getColor(colorResId, null), Mode.SRC_IN); icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); return icon; } diff --git a/core/java/com/android/internal/util/function/QuadConsumer.java b/core/java/com/android/internal/util/function/QuadConsumer.java new file mode 100644 index 000000000000..d899c01b16c6 --- /dev/null +++ b/core/java/com/android/internal/util/function/QuadConsumer.java @@ -0,0 +1,29 @@ +/* + * 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 com.android.internal.util.function; + + +import java.util.function.Consumer; + +/** + * A 4-argument {@link Consumer} + * + * @hide + */ +public interface QuadConsumer<A, B, C, D> { + void accept(A a, B b, C c, D d); +} diff --git a/core/java/com/android/internal/util/function/QuadFunction.java b/core/java/com/android/internal/util/function/QuadFunction.java new file mode 100644 index 000000000000..700d9536409d --- /dev/null +++ b/core/java/com/android/internal/util/function/QuadFunction.java @@ -0,0 +1,29 @@ +/* + * 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 com.android.internal.util.function; + + +import java.util.function.Function; + +/** + * A 4-argument {@link Function} + * + * @hide + */ +public interface QuadFunction<A, B, C, D, R> { + R apply(A a, B b, C c, D d); +} diff --git a/core/java/com/android/internal/util/function/QuadPredicate.java b/core/java/com/android/internal/util/function/QuadPredicate.java new file mode 100644 index 000000000000..512c98ba1e47 --- /dev/null +++ b/core/java/com/android/internal/util/function/QuadPredicate.java @@ -0,0 +1,29 @@ +/* + * 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 com.android.internal.util.function; + + +import java.util.function.Predicate; + +/** + * A 4-argument {@link Predicate} + * + * @hide + */ +public interface QuadPredicate<A, B, C, D> { + boolean test(A a, B b, C c, D d); +} diff --git a/core/java/com/android/internal/util/function/TriConsumer.java b/core/java/com/android/internal/util/function/TriConsumer.java new file mode 100644 index 000000000000..40d614ec5aff --- /dev/null +++ b/core/java/com/android/internal/util/function/TriConsumer.java @@ -0,0 +1,29 @@ +/* + * 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 com.android.internal.util.function; + + +import java.util.function.Consumer; + +/** + * A 3-argument {@link Consumer} + * + * @hide + */ +public interface TriConsumer<A, B, C> { + void accept(A a, B b, C c); +} diff --git a/core/java/com/android/internal/util/function/TriFunction.java b/core/java/com/android/internal/util/function/TriFunction.java new file mode 100644 index 000000000000..2b1df86e72e2 --- /dev/null +++ b/core/java/com/android/internal/util/function/TriFunction.java @@ -0,0 +1,29 @@ +/* + * 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 com.android.internal.util.function; + + +import java.util.function.Function; + +/** + * A 3-argument {@link Function} + * + * @hide + */ +public interface TriFunction<A, B, C, R> { + R apply(A a, B b, C c); +} diff --git a/core/java/com/android/internal/util/function/TriPredicate.java b/core/java/com/android/internal/util/function/TriPredicate.java new file mode 100644 index 000000000000..d9cd9683ee26 --- /dev/null +++ b/core/java/com/android/internal/util/function/TriPredicate.java @@ -0,0 +1,29 @@ +/* + * 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 com.android.internal.util.function; + + +import java.util.function.Predicate; + +/** + * A 3-argument {@link Predicate} + * + * @hide + */ +public interface TriPredicate<A, B, C> { + boolean test(A a, B b, C c); +} diff --git a/core/java/com/android/internal/util/function/pooled/ArgumentPlaceholder.java b/core/java/com/android/internal/util/function/pooled/ArgumentPlaceholder.java new file mode 100644 index 000000000000..cf86b7171864 --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/ArgumentPlaceholder.java @@ -0,0 +1,33 @@ +/* + * 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 com.android.internal.util.function.pooled; + +/** + * A placeholder for an argument of type {@code R} + * + * @see PooledLambda + * @hide + */ +public final class ArgumentPlaceholder<R> { + private ArgumentPlaceholder() {} + static final ArgumentPlaceholder<?> INSTANCE = new ArgumentPlaceholder<>(); + + @Override + public String toString() { + return "_"; + } +} diff --git a/core/java/com/android/internal/util/function/pooled/OmniFunction.java b/core/java/com/android/internal/util/function/pooled/OmniFunction.java new file mode 100755 index 000000000000..c0f506ec889f --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/OmniFunction.java @@ -0,0 +1,132 @@ +/* + * 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 com.android.internal.util.function.pooled; + +import com.android.internal.util.FunctionalUtils.ThrowingRunnable; +import com.android.internal.util.FunctionalUtils.ThrowingSupplier; +import com.android.internal.util.function.QuadConsumer; +import com.android.internal.util.function.QuadFunction; +import com.android.internal.util.function.TriConsumer; +import com.android.internal.util.function.TriFunction; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; + +/** + * An interface implementing all supported function interfaces, delegating each to {@link #invoke} + * + * @hide + */ +abstract class OmniFunction<A, B, C, D, R> implements + PooledFunction<A, R>, BiFunction<A, B, R>, TriFunction<A, B, C, R>, + QuadFunction<A, B, C, D, R>, + PooledConsumer<A>, BiConsumer<A, B>, TriConsumer<A, B, C>, QuadConsumer<A, B, C, D>, + PooledPredicate<A>, BiPredicate<A, B>, + PooledSupplier<R>, PooledRunnable, + ThrowingRunnable, ThrowingSupplier<R>, + PooledSupplier.OfInt, PooledSupplier.OfLong, PooledSupplier.OfDouble { + + abstract R invoke(A a, B b, C c, D d); + + @Override + public R apply(A o, B o2) { + return invoke(o, o2, null, null); + } + + @Override + public R apply(A o) { + return invoke(o, null, null, null); + } + + abstract public <V> OmniFunction<A, B, C, D, V> andThen(Function<? super R, ? extends V> after); + abstract public OmniFunction<A, B, C, D, R> negate(); + + @Override + public void accept(A o, B o2) { + invoke(o, o2, null, null); + } + + @Override + public void accept(A o) { + invoke(o, null, null, null); + } + + @Override + public void run() { + invoke(null, null, null, null); + } + + @Override + public R get() { + return invoke(null, null, null, null); + } + + @Override + public boolean test(A o, B o2) { + return (Boolean) invoke(o, o2, null, null); + } + + @Override + public boolean test(A o) { + return (Boolean) invoke(o, null, null, null); + } + + @Override + public PooledRunnable asRunnable() { + return this; + } + + @Override + public PooledConsumer<A> asConsumer() { + return this; + } + + @Override + public R apply(A a, B b, C c) { + return invoke(a, b, c, null); + } + + @Override + public void accept(A a, B b, C c) { + invoke(a, b, c, null); + } + + @Override + public R apply(A a, B b, C c, D d) { + return invoke(a, b, c, d); + } + + @Override + public void accept(A a, B b, C c, D d) { + invoke(a, b, c, d); + } + + @Override + public void runOrThrow() throws Exception { + run(); + } + + @Override + public R getOrThrow() throws Exception { + return get(); + } + + @Override + abstract public OmniFunction<A, B, C, D, R> recycleOnUse(); +} diff --git a/core/java/com/android/internal/util/function/pooled/PooledConsumer.java b/core/java/com/android/internal/util/function/pooled/PooledConsumer.java new file mode 100644 index 000000000000..f66586ee6791 --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/PooledConsumer.java @@ -0,0 +1,31 @@ +/* + * 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 com.android.internal.util.function.pooled; + +import java.util.function.Consumer; + +/** + * {@link Consumer} + {@link PooledLambda} + * + * @see PooledLambda + * @hide + */ +public interface PooledConsumer<T> extends PooledLambda, Consumer<T> { + + /** @inheritDoc */ + PooledConsumer<T> recycleOnUse(); +} diff --git a/core/java/com/android/internal/util/function/pooled/PooledFunction.java b/core/java/com/android/internal/util/function/pooled/PooledFunction.java new file mode 100644 index 000000000000..1f166fafc7e6 --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/PooledFunction.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 com.android.internal.util.function.pooled; + +import java.util.function.Function; + +/** + * {@link Function} + {@link PooledLambda} + * + * @see PooledLambda + * @hide + */ +public interface PooledFunction<A, R> extends PooledLambda, Function<A, R> { + + /** + * Ignores the result + */ + PooledConsumer<A> asConsumer(); + + /** @inheritDoc */ + PooledFunction<A, R> recycleOnUse(); +} diff --git a/core/java/com/android/internal/util/function/pooled/PooledLambda.java b/core/java/com/android/internal/util/function/pooled/PooledLambda.java new file mode 100755 index 000000000000..17b140dec396 --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/PooledLambda.java @@ -0,0 +1,813 @@ +/* + * 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 com.android.internal.util.function.pooled; + +import static com.android.internal.util.function.pooled.PooledLambdaImpl.acquire; +import static com.android.internal.util.function.pooled.PooledLambdaImpl.acquireConstSupplier; + +import android.os.Message; + +import com.android.internal.util.function.QuadConsumer; +import com.android.internal.util.function.QuadFunction; +import com.android.internal.util.function.TriConsumer; +import com.android.internal.util.function.TriFunction; +import com.android.internal.util.function.pooled.PooledLambdaImpl.LambdaType.ReturnType; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * A recyclable anonymous function. + * Allows obtaining {@link Function}s/{@link Runnable}s/{@link Supplier}s/etc. without allocating a + * new instance each time + * + * This exploits the mechanic that stateless lambdas (such as plain/non-bound method references) + * get translated into a singleton instance, making it possible to create a recyclable container + * ({@link PooledLambdaImpl}) holding a reference to such a singleton function, as well as + * (possibly partial) arguments required for its invocation. + * + * To obtain an instance, use one of the factory methods in this class. + * + * You can call {@link #recycleOnUse} to make the instance automatically recycled upon invocation, + * making if effectively <b>one-time use</b>. + * This is often the behavior you want, as it allows to not worry about manual recycling. + * Some notable examples: {@link android.os.Handler#post(Runnable)}, + * {@link android.app.Activity#runOnUiThread(Runnable)}, {@link android.view.View#post(Runnable)} + * + * For factories of functions that take further arguments, the corresponding 'missing' argument's + * position is marked by an argument of type {@link ArgumentPlaceholder} with the type parameter + * corresponding to missing argument's type. + * You can fill the 'missing argument' spot with {@link #__()} + * (which is the factory function for {@link ArgumentPlaceholder}) + * + * @hide + */ +@SuppressWarnings({"unchecked", "unused", "WeakerAccess"}) +public interface PooledLambda { + + /** + * Recycles this instance. No-op if already recycled. + */ + void recycle(); + + /** + * Makes this instance automatically {@link #recycle} itself after the first call. + * + * @return this instance for convenience + */ + PooledLambda recycleOnUse(); + + + // Factories + + /** + * @return {@link ArgumentPlaceholder} with the inferred type parameter value + */ + static <R> ArgumentPlaceholder<R> __() { + return (ArgumentPlaceholder<R>) ArgumentPlaceholder.INSTANCE; + } + + /** + * @param typeHint the explicitly specified type of the missing argument + * @return {@link ArgumentPlaceholder} with the specified type parameter value + */ + static <R> ArgumentPlaceholder<R> __(Class<R> typeHint) { + return __(); + } + + /** + * Wraps the given value into a {@link PooledSupplier} + * + * @param value a value to wrap + * @return a pooled supplier of {@code value} + */ + static <R> PooledSupplier<R> obtainSupplier(R value) { + PooledLambdaImpl r = acquireConstSupplier(ReturnType.OBJECT); + r.mFunc = value; + return r; + } + + /** + * Wraps the given value into a {@link PooledSupplier} + * + * @param value a value to wrap + * @return a pooled supplier of {@code value} + */ + static PooledSupplier.OfInt obtainSupplier(int value) { + PooledLambdaImpl r = acquireConstSupplier(ReturnType.INT); + r.mConstValue = value; + return r; + } + + /** + * Wraps the given value into a {@link PooledSupplier} + * + * @param value a value to wrap + * @return a pooled supplier of {@code value} + */ + static PooledSupplier.OfLong obtainSupplier(long value) { + PooledLambdaImpl r = acquireConstSupplier(ReturnType.LONG); + r.mConstValue = value; + return r; + } + + /** + * Wraps the given value into a {@link PooledSupplier} + * + * @param value a value to wrap + * @return a pooled supplier of {@code value} + */ + static PooledSupplier.OfDouble obtainSupplier(double value) { + PooledLambdaImpl r = acquireConstSupplier(ReturnType.DOUBLE); + r.mConstValue = Double.doubleToRawLongBits(value); + return r; + } + + /** + * {@link PooledRunnable} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @return a {@link PooledRunnable}, equivalent to lambda: + * {@code () -> function(arg1) } + */ + static <A> PooledRunnable obtainRunnable( + Consumer<? super A> function, + A arg1) { + return acquire(PooledLambdaImpl.sPool, + function, 1, 0, ReturnType.VOID, arg1, null, null, null); + } + + /** + * {@link PooledSupplier} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @return a {@link PooledSupplier}, equivalent to lambda: + * {@code () -> function(arg1) } + */ + static <A> PooledSupplier<Boolean> obtainSupplier( + Predicate<? super A> function, + A arg1) { + return acquire(PooledLambdaImpl.sPool, + function, 1, 0, ReturnType.BOOLEAN, arg1, null, null, null); + } + + /** + * {@link PooledSupplier} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @return a {@link PooledSupplier}, equivalent to lambda: + * {@code () -> function(arg1) } + */ + static <A, R> PooledSupplier<R> obtainSupplier( + Function<? super A, ? extends R> function, + A arg1) { + return acquire(PooledLambdaImpl.sPool, + function, 1, 0, ReturnType.OBJECT, arg1, null, null, null); + } + + /** + * Factory of {@link Message}s that contain an + * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its + * {@link Message#getCallback internal callback}. + * + * The callback is equivalent to one obtainable via + * {@link #obtainRunnable(Consumer, Object)} + * + * Note that using this method with {@link android.os.Handler#handleMessage} + * is more efficient than the alternative of {@link android.os.Handler#post} + * with a {@link PooledRunnable} due to the lack of 2 separate synchronization points + * when obtaining {@link Message} and {@link PooledRunnable} from pools separately + * + * You may optionally set a {@link Message#what} for the message if you want to be + * able to cancel it via {@link android.os.Handler#removeMessages}, but otherwise + * there's no need to do so + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @return a {@link Message} invoking {@code function(arg1) } when handled + */ + static <A> Message obtainMessage( + Consumer<? super A> function, + A arg1) { + synchronized (Message.sPoolSync) { + PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool, + function, 1, 0, ReturnType.VOID, arg1, null, null, null); + return Message.obtain().setCallback(callback.recycleOnUse()); + } + } + + /** + * {@link PooledRunnable} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @return a {@link PooledRunnable}, equivalent to lambda: + * {@code () -> function(arg1, arg2) } + */ + static <A, B> PooledRunnable obtainRunnable( + BiConsumer<? super A, ? super B> function, + A arg1, B arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 0, ReturnType.VOID, arg1, arg2, null, null); + } + + /** + * {@link PooledSupplier} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @return a {@link PooledSupplier}, equivalent to lambda: + * {@code () -> function(arg1, arg2) } + */ + static <A, B> PooledSupplier<Boolean> obtainSupplier( + BiPredicate<? super A, ? super B> function, + A arg1, B arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 0, ReturnType.BOOLEAN, arg1, arg2, null, null); + } + + /** + * {@link PooledSupplier} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @return a {@link PooledSupplier}, equivalent to lambda: + * {@code () -> function(arg1, arg2) } + */ + static <A, B, R> PooledSupplier<R> obtainSupplier( + BiFunction<? super A, ? super B, ? extends R> function, + A arg1, B arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 0, ReturnType.OBJECT, arg1, arg2, null, null); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 placeholder for a missing argument. Use {@link #__} to get one + * @param arg2 parameter supplied to {@code function} on call + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg1) -> function(arg1, arg2) } + */ + static <A, B> PooledConsumer<A> obtainConsumer( + BiConsumer<? super A, ? super B> function, + ArgumentPlaceholder<A> arg1, B arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 1, ReturnType.VOID, arg1, arg2, null, null); + } + + /** + * {@link PooledPredicate} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 placeholder for a missing argument. Use {@link #__} to get one + * @param arg2 parameter supplied to {@code function} on call + * @return a {@link PooledPredicate}, equivalent to lambda: + * {@code (arg1) -> function(arg1, arg2) } + */ + static <A, B> PooledPredicate<A> obtainPredicate( + BiPredicate<? super A, ? super B> function, + ArgumentPlaceholder<A> arg1, B arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 1, ReturnType.BOOLEAN, arg1, arg2, null, null); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 placeholder for a missing argument. Use {@link #__} to get one + * @param arg2 parameter supplied to {@code function} on call + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg1) -> function(arg1, arg2) } + */ + static <A, B, R> PooledFunction<A, R> obtainFunction( + BiFunction<? super A, ? super B, ? extends R> function, + ArgumentPlaceholder<A> arg1, B arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 1, ReturnType.OBJECT, arg1, arg2, null, null); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 placeholder for a missing argument. Use {@link #__} to get one + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg2) -> function(arg1, arg2) } + */ + static <A, B> PooledConsumer<B> obtainConsumer( + BiConsumer<? super A, ? super B> function, + A arg1, ArgumentPlaceholder<B> arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 1, ReturnType.VOID, arg1, arg2, null, null); + } + + /** + * {@link PooledPredicate} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 placeholder for a missing argument. Use {@link #__} to get one + * @return a {@link PooledPredicate}, equivalent to lambda: + * {@code (arg2) -> function(arg1, arg2) } + */ + static <A, B> PooledPredicate<B> obtainPredicate( + BiPredicate<? super A, ? super B> function, + A arg1, ArgumentPlaceholder<B> arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 1, ReturnType.BOOLEAN, arg1, arg2, null, null); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 placeholder for a missing argument. Use {@link #__} to get one + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg2) -> function(arg1, arg2) } + */ + static <A, B, R> PooledFunction<B, R> obtainFunction( + BiFunction<? super A, ? super B, ? extends R> function, + A arg1, ArgumentPlaceholder<B> arg2) { + return acquire(PooledLambdaImpl.sPool, + function, 2, 1, ReturnType.OBJECT, arg1, arg2, null, null); + } + + /** + * Factory of {@link Message}s that contain an + * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its + * {@link Message#getCallback internal callback}. + * + * The callback is equivalent to one obtainable via + * {@link #obtainRunnable(BiConsumer, Object, Object)} + * + * Note that using this method with {@link android.os.Handler#handleMessage} + * is more efficient than the alternative of {@link android.os.Handler#post} + * with a {@link PooledRunnable} due to the lack of 2 separate synchronization points + * when obtaining {@link Message} and {@link PooledRunnable} from pools separately + * + * You may optionally set a {@link Message#what} for the message if you want to be + * able to cancel it via {@link android.os.Handler#removeMessages}, but otherwise + * there's no need to do so + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @return a {@link Message} invoking {@code function(arg1, arg2) } when handled + */ + static <A, B> Message obtainMessage( + BiConsumer<? super A, ? super B> function, + A arg1, B arg2) { + synchronized (Message.sPoolSync) { + PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool, + function, 2, 0, ReturnType.VOID, arg1, arg2, null, null); + return Message.obtain().setCallback(callback.recycleOnUse()); + } + } + + /** + * {@link PooledRunnable} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @return a {@link PooledRunnable}, equivalent to lambda: + * {@code () -> function(arg1, arg2, arg3) } + */ + static <A, B, C> PooledRunnable obtainRunnable( + TriConsumer<? super A, ? super B, ? super C> function, + A arg1, B arg2, C arg3) { + return acquire(PooledLambdaImpl.sPool, + function, 3, 0, ReturnType.VOID, arg1, arg2, arg3, null); + } + + /** + * {@link PooledSupplier} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @return a {@link PooledSupplier}, equivalent to lambda: + * {@code () -> function(arg1, arg2, arg3) } + */ + static <A, B, C, R> PooledSupplier<R> obtainSupplier( + TriFunction<? super A, ? super B, ? super C, ? extends R> function, + A arg1, B arg2, C arg3) { + return acquire(PooledLambdaImpl.sPool, + function, 3, 0, ReturnType.OBJECT, arg1, arg2, arg3, null); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 placeholder for a missing argument. Use {@link #__} to get one + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg1) -> function(arg1, arg2, arg3) } + */ + static <A, B, C> PooledConsumer<A> obtainConsumer( + TriConsumer<? super A, ? super B, ? super C> function, + ArgumentPlaceholder<A> arg1, B arg2, C arg3) { + return acquire(PooledLambdaImpl.sPool, + function, 3, 1, ReturnType.VOID, arg1, arg2, arg3, null); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 placeholder for a missing argument. Use {@link #__} to get one + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg1) -> function(arg1, arg2, arg3) } + */ + static <A, B, C, R> PooledFunction<A, R> obtainFunction( + TriFunction<? super A, ? super B, ? super C, ? extends R> function, + ArgumentPlaceholder<A> arg1, B arg2, C arg3) { + return acquire(PooledLambdaImpl.sPool, + function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 placeholder for a missing argument. Use {@link #__} to get one + * @param arg3 parameter supplied to {@code function} on call + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg2) -> function(arg1, arg2, arg3) } + */ + static <A, B, C> PooledConsumer<B> obtainConsumer( + TriConsumer<? super A, ? super B, ? super C> function, + A arg1, ArgumentPlaceholder<B> arg2, C arg3) { + return acquire(PooledLambdaImpl.sPool, + function, 3, 1, ReturnType.VOID, arg1, arg2, arg3, null); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 placeholder for a missing argument. Use {@link #__} to get one + * @param arg3 parameter supplied to {@code function} on call + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg2) -> function(arg1, arg2, arg3) } + */ + static <A, B, C, R> PooledFunction<B, R> obtainFunction( + TriFunction<? super A, ? super B, ? super C, ? extends R> function, + A arg1, ArgumentPlaceholder<B> arg2, C arg3) { + return acquire(PooledLambdaImpl.sPool, + function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 placeholder for a missing argument. Use {@link #__} to get one + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg3) -> function(arg1, arg2, arg3) } + */ + static <A, B, C> PooledConsumer<C> obtainConsumer( + TriConsumer<? super A, ? super B, ? super C> function, + A arg1, B arg2, ArgumentPlaceholder<C> arg3) { + return acquire(PooledLambdaImpl.sPool, + function, 3, 1, ReturnType.VOID, arg1, arg2, arg3, null); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 placeholder for a missing argument. Use {@link #__} to get one + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg3) -> function(arg1, arg2, arg3) } + */ + static <A, B, C, R> PooledFunction<C, R> obtainFunction( + TriFunction<? super A, ? super B, ? super C, ? extends R> function, + A arg1, B arg2, ArgumentPlaceholder<C> arg3) { + return acquire(PooledLambdaImpl.sPool, + function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null); + } + + /** + * Factory of {@link Message}s that contain an + * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its + * {@link Message#getCallback internal callback}. + * + * The callback is equivalent to one obtainable via + * {@link #obtainRunnable(TriConsumer, Object, Object, Object)} + * + * Note that using this method with {@link android.os.Handler#handleMessage} + * is more efficient than the alternative of {@link android.os.Handler#post} + * with a {@link PooledRunnable} due to the lack of 2 separate synchronization points + * when obtaining {@link Message} and {@link PooledRunnable} from pools separately + * + * You may optionally set a {@link Message#what} for the message if you want to be + * able to cancel it via {@link android.os.Handler#removeMessages}, but otherwise + * there's no need to do so + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @return a {@link Message} invoking {@code function(arg1, arg2, arg3) } when handled + */ + static <A, B, C> Message obtainMessage( + TriConsumer<? super A, ? super B, ? super C> function, + A arg1, B arg2, C arg3) { + synchronized (Message.sPoolSync) { + PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool, + function, 3, 0, ReturnType.VOID, arg1, arg2, arg3, null); + return Message.obtain().setCallback(callback.recycleOnUse()); + } + } + + /** + * {@link PooledRunnable} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link PooledRunnable}, equivalent to lambda: + * {@code () -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D> PooledRunnable obtainRunnable( + QuadConsumer<? super A, ? super B, ? super C, ? super D> function, + A arg1, B arg2, C arg3, D arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 0, ReturnType.VOID, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledSupplier} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link PooledSupplier}, equivalent to lambda: + * {@code () -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D, R> PooledSupplier<R> obtainSupplier( + QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function, + A arg1, B arg2, C arg3, D arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 placeholder for a missing argument. Use {@link #__} to get one + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg1) -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D> PooledConsumer<A> obtainConsumer( + QuadConsumer<? super A, ? super B, ? super C, ? super D> function, + ArgumentPlaceholder<A> arg1, B arg2, C arg3, D arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 1, ReturnType.VOID, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 placeholder for a missing argument. Use {@link #__} to get one + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg1) -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D, R> PooledFunction<A, R> obtainFunction( + QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function, + ArgumentPlaceholder<A> arg1, B arg2, C arg3, D arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 placeholder for a missing argument. Use {@link #__} to get one + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg2) -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D> PooledConsumer<B> obtainConsumer( + QuadConsumer<? super A, ? super B, ? super C, ? super D> function, + A arg1, ArgumentPlaceholder<B> arg2, C arg3, D arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 1, ReturnType.VOID, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 placeholder for a missing argument. Use {@link #__} to get one + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg2) -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D, R> PooledFunction<B, R> obtainFunction( + QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function, + A arg1, ArgumentPlaceholder<B> arg2, C arg3, D arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 placeholder for a missing argument. Use {@link #__} to get one + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg3) -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D> PooledConsumer<C> obtainConsumer( + QuadConsumer<? super A, ? super B, ? super C, ? super D> function, + A arg1, B arg2, ArgumentPlaceholder<C> arg3, D arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 1, ReturnType.VOID, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 placeholder for a missing argument. Use {@link #__} to get one + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg3) -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D, R> PooledFunction<C, R> obtainFunction( + QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function, + A arg1, B arg2, ArgumentPlaceholder<C> arg3, D arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledConsumer} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 placeholder for a missing argument. Use {@link #__} to get one + * @return a {@link PooledConsumer}, equivalent to lambda: + * {@code (arg4) -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D> PooledConsumer<D> obtainConsumer( + QuadConsumer<? super A, ? super B, ? super C, ? super D> function, + A arg1, B arg2, C arg3, ArgumentPlaceholder<D> arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 1, ReturnType.VOID, arg1, arg2, arg3, arg4); + } + + /** + * {@link PooledFunction} factory + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 placeholder for a missing argument. Use {@link #__} to get one + * @return a {@link PooledFunction}, equivalent to lambda: + * {@code (arg4) -> function(arg1, arg2, arg3, arg4) } + */ + static <A, B, C, D, R> PooledFunction<D, R> obtainFunction( + QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function, + A arg1, B arg2, C arg3, ArgumentPlaceholder<D> arg4) { + return acquire(PooledLambdaImpl.sPool, + function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4); + } + + /** + * Factory of {@link Message}s that contain an + * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its + * {@link Message#getCallback internal callback}. + * + * The callback is equivalent to one obtainable via + * {@link #obtainRunnable(QuadConsumer, Object, Object, Object, Object)} + * + * Note that using this method with {@link android.os.Handler#handleMessage} + * is more efficient than the alternative of {@link android.os.Handler#post} + * with a {@link PooledRunnable} due to the lack of 2 separate synchronization points + * when obtaining {@link Message} and {@link PooledRunnable} from pools separately + * + * You may optionally set a {@link Message#what} for the message if you want to be + * able to cancel it via {@link android.os.Handler#removeMessages}, but otherwise + * there's no need to do so + * + * @param function non-capturing lambda(typically an unbounded method reference) + * to be invoked on call + * @param arg1 parameter supplied to {@code function} on call + * @param arg2 parameter supplied to {@code function} on call + * @param arg3 parameter supplied to {@code function} on call + * @param arg4 parameter supplied to {@code function} on call + * @return a {@link Message} invoking {@code function(arg1, arg2, arg3, arg4) } when handled + */ + static <A, B, C, D> Message obtainMessage( + QuadConsumer<? super A, ? super B, ? super C, ? super D> function, + A arg1, B arg2, C arg3, D arg4) { + synchronized (Message.sPoolSync) { + PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool, + function, 4, 0, ReturnType.VOID, arg1, arg2, arg3, arg4); + return Message.obtain().setCallback(callback.recycleOnUse()); + } + } +} diff --git a/core/java/com/android/internal/util/function/pooled/PooledLambdaImpl.java b/core/java/com/android/internal/util/function/pooled/PooledLambdaImpl.java new file mode 100755 index 000000000000..03e013cd46b8 --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/PooledLambdaImpl.java @@ -0,0 +1,557 @@ +/* + * 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 com.android.internal.util.function.pooled; + +import android.annotation.Nullable; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pools; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.BitUtils; +import com.android.internal.util.function.QuadConsumer; +import com.android.internal.util.function.QuadFunction; +import com.android.internal.util.function.QuadPredicate; +import com.android.internal.util.function.TriConsumer; +import com.android.internal.util.function.TriFunction; +import com.android.internal.util.function.TriPredicate; + +import java.util.Arrays; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * @see PooledLambda + * @hide + */ +final class PooledLambdaImpl<R> extends OmniFunction<Object, Object, Object, Object, R> { + + private static final boolean DEBUG = false; + private static final String LOG_TAG = "PooledLambdaImpl"; + + private static final int MAX_ARGS = 4; + + private static final int MAX_POOL_SIZE = 50; + + static class Pool extends Pools.SynchronizedPool<PooledLambdaImpl> { + + public Pool(Object lock) { + super(MAX_POOL_SIZE, lock); + } + } + + static final Pool sPool = new Pool(new Object()); + static final Pool sMessageCallbacksPool = new Pool(Message.sPoolSync); + + private PooledLambdaImpl() {} + + /** + * The function reference to be invoked + * + * May be the return value itself in case when an immediate result constant is provided instead + */ + Object mFunc; + + /** + * A primitive result value to be immediately returned on invocation instead of calling + * {@link #mFunc} + */ + long mConstValue; + + /** + * Arguments for {@link #mFunc} + */ + @Nullable Object[] mArgs = null; + + /** + * Flag for {@link #mFlags} + * + * Indicates whether this instance is recycled + */ + private static final int FLAG_RECYCLED = 1 << MAX_ARGS; + + /** + * Flag for {@link #mFlags} + * + * Indicates whether this instance should be immediately recycled on invocation + * (as requested via {@link PooledLambda#recycleOnUse()}) or not(default) + */ + private static final int FLAG_RECYCLE_ON_USE = 1 << (MAX_ARGS + 1); + + /** + * Flag for {@link #mFlags} + * + * Indicates that this instance was acquired from {@link #sMessageCallbacksPool} as opposed to + * {@link #sPool} + */ + private static final int FLAG_ACQUIRED_FROM_MESSAGE_CALLBACKS_POOL = 1 << (MAX_ARGS + 2); + + /** @see #mFlags */ + static final int MASK_EXPOSED_AS = LambdaType.MASK << (MAX_ARGS + 3); + + /** @see #mFlags */ + static final int MASK_FUNC_TYPE = LambdaType.MASK << + (MAX_ARGS + 3 + LambdaType.MASK_BIT_COUNT); + + /** + * Bit schema: + * AAAABCDEEEEEEFFFFFF + * + * Where: + * A - whether {@link #mArgs arg} at corresponding index was specified at + * {@link #acquire creation time} (0) or {@link #invoke invocation time} (1) + * B - {@link #FLAG_RECYCLED} + * C - {@link #FLAG_RECYCLE_ON_USE} + * D - {@link #FLAG_ACQUIRED_FROM_MESSAGE_CALLBACKS_POOL} + * E - {@link LambdaType} representing the type of the lambda returned to the caller from a + * factory method + * F - {@link LambdaType} of {@link #mFunc} as resolved when calling a factory method + */ + int mFlags = 0; + + + @Override + public void recycle() { + if (DEBUG) Log.i(LOG_TAG, this + ".recycle()"); + if (!isRecycled()) doRecycle(); + } + + private void doRecycle() { + if (DEBUG) Log.i(LOG_TAG, this + ".doRecycle()"); + Pool pool = (mFlags & FLAG_ACQUIRED_FROM_MESSAGE_CALLBACKS_POOL) != 0 + ? PooledLambdaImpl.sMessageCallbacksPool + : PooledLambdaImpl.sPool; + + mFunc = null; + if (mArgs != null) Arrays.fill(mArgs, null); + mFlags = FLAG_RECYCLED; + mConstValue = 0L; + + pool.release(this); + } + + @Override + R invoke(Object a1, Object a2, Object a3, Object a4) { + checkNotRecycled(); + if (DEBUG) { + Log.i(LOG_TAG, this + ".invoke(" + + commaSeparateFirstN( + new Object[] { a1, a2, a3, a4 }, + LambdaType.decodeArgCount(getFlags(MASK_EXPOSED_AS))) + + ")"); + } + boolean ignored = fillInArg(a1) && fillInArg(a2) && fillInArg(a3) && fillInArg(a4); + int argCount = LambdaType.decodeArgCount(getFlags(MASK_FUNC_TYPE)); + if (argCount != LambdaType.MASK_ARG_COUNT) { + for (int i = 0; i < argCount; i++) { + if (mArgs[i] == ArgumentPlaceholder.INSTANCE) { + throw new IllegalStateException("Missing argument #" + i + " among " + + Arrays.toString(mArgs)); + } + } + } + try { + return doInvoke(); + } finally { + if (isRecycleOnUse()) doRecycle(); + if (!isRecycled()) { + int argsSize = ArrayUtils.size(mArgs); + for (int i = 0; i < argsSize; i++) { + popArg(i); + } + } + } + } + + private boolean fillInArg(Object invocationArg) { + int argsSize = ArrayUtils.size(mArgs); + for (int i = 0; i < argsSize; i++) { + if (mArgs[i] == ArgumentPlaceholder.INSTANCE) { + mArgs[i] = invocationArg; + mFlags |= BitUtils.bitAt(i); + return true; + } + } + if (invocationArg != null && invocationArg != ArgumentPlaceholder.INSTANCE) { + throw new IllegalStateException("No more arguments expected for provided arg " + + invocationArg + " among " + Arrays.toString(mArgs)); + } + return false; + } + + private void checkNotRecycled() { + if (isRecycled()) throw new IllegalStateException("Instance is recycled: " + this); + } + + @SuppressWarnings("unchecked") + private R doInvoke() { + final int funcType = getFlags(MASK_FUNC_TYPE); + final int argCount = LambdaType.decodeArgCount(funcType); + final int returnType = LambdaType.decodeReturnType(funcType); + + switch (argCount) { + case LambdaType.MASK_ARG_COUNT: { + switch (returnType) { + case LambdaType.ReturnType.INT: return (R) (Integer) getAsInt(); + case LambdaType.ReturnType.LONG: return (R) (Long) getAsLong(); + case LambdaType.ReturnType.DOUBLE: return (R) (Double) getAsDouble(); + default: return (R) mFunc; + } + } + case 0: { + switch (returnType) { + case LambdaType.ReturnType.VOID: { + ((Runnable) mFunc).run(); + return null; + } + case LambdaType.ReturnType.BOOLEAN: + case LambdaType.ReturnType.OBJECT: { + return (R) ((Supplier) mFunc).get(); + } + } + } break; + case 1: { + switch (returnType) { + case LambdaType.ReturnType.VOID: { + ((Consumer) mFunc).accept(popArg(0)); + return null; + } + case LambdaType.ReturnType.BOOLEAN: { + return (R) (Object) ((Predicate) mFunc).test(popArg(0)); + } + case LambdaType.ReturnType.OBJECT: { + return (R) ((Function) mFunc).apply(popArg(0)); + } + } + } break; + case 2: { + switch (returnType) { + case LambdaType.ReturnType.VOID: { + ((BiConsumer) mFunc).accept(popArg(0), popArg(1)); + return null; + } + case LambdaType.ReturnType.BOOLEAN: { + return (R) (Object) ((BiPredicate) mFunc).test(popArg(0), popArg(1)); + } + case LambdaType.ReturnType.OBJECT: { + return (R) ((BiFunction) mFunc).apply(popArg(0), popArg(1)); + } + } + } break; + case 3: { + switch (returnType) { + case LambdaType.ReturnType.VOID: { + ((TriConsumer) mFunc).accept(popArg(0), popArg(1), popArg(2)); + return null; + } + case LambdaType.ReturnType.BOOLEAN: { + return (R) (Object) ((TriPredicate) mFunc).test( + popArg(0), popArg(1), popArg(2)); + } + case LambdaType.ReturnType.OBJECT: { + return (R) ((TriFunction) mFunc).apply(popArg(0), popArg(1), popArg(2)); + } + } + } break; + case 4: { + switch (returnType) { + case LambdaType.ReturnType.VOID: { + ((QuadConsumer) mFunc).accept(popArg(0), popArg(1), popArg(2), popArg(3)); + return null; + } + case LambdaType.ReturnType.BOOLEAN: { + return (R) (Object) ((QuadPredicate) mFunc).test( + popArg(0), popArg(1), popArg(2), popArg(3)); + } + case LambdaType.ReturnType.OBJECT: { + return (R) ((QuadFunction) mFunc).apply( + popArg(0), popArg(1), popArg(2), popArg(3)); + } + } + } break; + } + throw new IllegalStateException("Unknown function type: " + LambdaType.toString(funcType)); + } + + private boolean isConstSupplier() { + return LambdaType.decodeArgCount(getFlags(MASK_FUNC_TYPE)) == LambdaType.MASK_ARG_COUNT; + } + + private Object popArg(int index) { + Object result = mArgs[index]; + if (isInvocationArgAtIndex(index)) { + mArgs[index] = ArgumentPlaceholder.INSTANCE; + mFlags &= ~BitUtils.bitAt(index); + } + return result; + } + + @Override + public String toString() { + if (isRecycled()) return "<recycled PooledLambda@" + hashCodeHex(this) + ">"; + + StringBuilder sb = new StringBuilder(); + if (isConstSupplier()) { + sb.append(getFuncTypeAsString()).append("(").append(doInvoke()).append(")"); + } else { + if (mFunc instanceof PooledLambdaImpl) { + sb.append(mFunc); + } else { + sb.append(getFuncTypeAsString()).append("@").append(hashCodeHex(mFunc)); + } + sb.append("("); + sb.append(commaSeparateFirstN(mArgs, LambdaType.decodeArgCount(getFlags(MASK_FUNC_TYPE)))); + sb.append(")"); + } + return sb.toString(); + } + + private String commaSeparateFirstN(@Nullable Object[] arr, int n) { + if (arr == null) return ""; + return TextUtils.join(",", Arrays.copyOf(arr, n)); + } + + private static String hashCodeHex(Object o) { + return Integer.toHexString(o.hashCode()); + } + + private String getFuncTypeAsString() { + if (isRecycled()) throw new IllegalStateException(); + if (isConstSupplier()) return "supplier"; + String name = LambdaType.toString(getFlags(MASK_EXPOSED_AS)); + if (name.endsWith("Consumer")) return "consumer"; + if (name.endsWith("Function")) return "function"; + if (name.endsWith("Predicate")) return "predicate"; + if (name.endsWith("Supplier")) return "supplier"; + if (name.endsWith("Runnable")) return "runnable"; + throw new IllegalStateException("Don't know the string representation of " + name); + } + + /** + * Internal non-typesafe factory method for {@link PooledLambdaImpl} + */ + static <E extends PooledLambda> E acquire(Pool pool, Object f, + int fNumArgs, int numPlaceholders, int fReturnType, + Object a, Object b, Object c, Object d) { + PooledLambdaImpl r = acquire(pool); + if (DEBUG) { + Log.i(LOG_TAG, + "acquire(this = @" + hashCodeHex(r) + + ", f = " + f + + ", fNumArgs = " + fNumArgs + + ", numPlaceholders = " + numPlaceholders + + ", fReturnType = " + LambdaType.ReturnType.toString(fReturnType) + + ", a = " + a + + ", b = " + b + + ", c = " + c + + ", d = " + d + + ")"); + } + r.mFunc = f; + r.setFlags(MASK_FUNC_TYPE, LambdaType.encode(fNumArgs, fReturnType)); + r.setFlags(MASK_EXPOSED_AS, LambdaType.encode(numPlaceholders, fReturnType)); + if (ArrayUtils.size(r.mArgs) < fNumArgs) r.mArgs = new Object[fNumArgs]; + setIfInBounds(r.mArgs, 0, a); + setIfInBounds(r.mArgs, 1, b); + setIfInBounds(r.mArgs, 2, c); + setIfInBounds(r.mArgs, 3, d); + return (E) r; + } + + static PooledLambdaImpl acquireConstSupplier(int type) { + PooledLambdaImpl r = acquire(PooledLambdaImpl.sPool); + int lambdaType = LambdaType.encode(LambdaType.MASK_ARG_COUNT, type); + r.setFlags(PooledLambdaImpl.MASK_FUNC_TYPE, lambdaType); + r.setFlags(PooledLambdaImpl.MASK_EXPOSED_AS, lambdaType); + return r; + } + + static PooledLambdaImpl acquire(Pool pool) { + PooledLambdaImpl r = pool.acquire(); + if (r == null) r = new PooledLambdaImpl(); + r.mFlags &= ~FLAG_RECYCLED; + r.setFlags(FLAG_ACQUIRED_FROM_MESSAGE_CALLBACKS_POOL, + pool == sMessageCallbacksPool ? 1 : 0); + return r; + } + + private static void setIfInBounds(Object[] array, int i, Object a) { + if (i < ArrayUtils.size(array)) array[i] = a; + } + + @Override + public OmniFunction<Object, Object, Object, Object, R> negate() { + throw new UnsupportedOperationException(); + } + + @Override + public <V> OmniFunction<Object, Object, Object, Object, V> andThen( + Function<? super R, ? extends V> after) { + throw new UnsupportedOperationException(); + } + + @Override + public double getAsDouble() { + return Double.longBitsToDouble(mConstValue); + } + + @Override + public int getAsInt() { + return (int) mConstValue; + } + + @Override + public long getAsLong() { + return mConstValue; + } + + @Override + public OmniFunction<Object, Object, Object, Object, R> recycleOnUse() { + if (DEBUG) Log.i(LOG_TAG, this + ".recycleOnUse()"); + mFlags |= FLAG_RECYCLE_ON_USE; + return this; + } + + private boolean isRecycled() { + return (mFlags & FLAG_RECYCLED) != 0; + } + + private boolean isRecycleOnUse() { + return (mFlags & FLAG_RECYCLE_ON_USE) != 0; + } + + private boolean isInvocationArgAtIndex(int argIndex) { + return (mFlags & (1 << argIndex)) != 0; + } + + int getFlags(int mask) { + return unmask(mask, mFlags); + } + + void setFlags(int mask, int value) { + mFlags &= ~mask; + mFlags |= mask(mask, value); + } + + /** + * 0xFF000, 0xAB -> 0xAB000 + */ + private static int mask(int mask, int value) { + return (value << Integer.numberOfTrailingZeros(mask)) & mask; + } + + /** + * 0xFF000, 0xAB123 -> 0xAB + */ + private static int unmask(int mask, int bits) { + return (bits & mask) / (1 << Integer.numberOfTrailingZeros(mask)); + } + + /** + * Contract for encoding a supported lambda type in {@link #MASK_BIT_COUNT} bits + */ + static class LambdaType { + public static final int MASK_ARG_COUNT = 0b111; + public static final int MASK_RETURN_TYPE = 0b111000; + public static final int MASK = MASK_ARG_COUNT | MASK_RETURN_TYPE; + public static final int MASK_BIT_COUNT = 6; + + static int encode(int argCount, int returnType) { + return mask(MASK_ARG_COUNT, argCount) | mask(MASK_RETURN_TYPE, returnType); + } + + static int decodeArgCount(int type) { + return type & MASK_ARG_COUNT; + } + + static int decodeReturnType(int type) { + return unmask(MASK_RETURN_TYPE, type); + } + + static String toString(int type) { + int argCount = decodeArgCount(type); + int returnType = decodeReturnType(type); + if (argCount == 0) { + if (returnType == ReturnType.VOID) return "Runnable"; + if (returnType == ReturnType.OBJECT || returnType == ReturnType.BOOLEAN) { + return "Supplier"; + } + } + return argCountPrefix(argCount) + ReturnType.lambdaSuffix(returnType); + } + + private static String argCountPrefix(int argCount) { + switch (argCount) { + case MASK_ARG_COUNT: return ""; + case 1: return ""; + case 2: return "Bi"; + case 3: return "Tri"; + case 4: return "Quad"; + default: throw new IllegalArgumentException("" + argCount); + } + } + + static class ReturnType { + public static final int VOID = 1; + public static final int BOOLEAN = 2; + public static final int OBJECT = 3; + public static final int INT = 4; + public static final int LONG = 5; + public static final int DOUBLE = 6; + + static String toString(int returnType) { + switch (returnType) { + case VOID: return "VOID"; + case BOOLEAN: return "BOOLEAN"; + case OBJECT: return "OBJECT"; + case INT: return "INT"; + case LONG: return "LONG"; + case DOUBLE: return "DOUBLE"; + default: return "" + returnType; + } + } + + static String lambdaSuffix(int type) { + return prefix(type) + suffix(type); + } + + private static String prefix(int type) { + switch (type) { + case INT: return "Int"; + case LONG: return "Long"; + case DOUBLE: return "Double"; + default: return ""; + } + } + + private static String suffix(int type) { + switch (type) { + case VOID: return "Consumer"; + case BOOLEAN: return "Predicate"; + case OBJECT: return "Function"; + default: return "Supplier"; + } + } + } + } +} diff --git a/core/java/com/android/internal/util/function/pooled/PooledPredicate.java b/core/java/com/android/internal/util/function/pooled/PooledPredicate.java new file mode 100644 index 000000000000..9b14366452e5 --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/PooledPredicate.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 com.android.internal.util.function.pooled; + +import java.util.function.Predicate; + +/** + * {@link Predicate} + {@link PooledLambda} + * + * @see PooledLambda + * @hide + */ +public interface PooledPredicate<T> extends PooledLambda, Predicate<T> { + + /** + * Ignores the result + */ + PooledConsumer<T> asConsumer(); + + /** @inheritDoc */ + PooledPredicate<T> recycleOnUse(); +} diff --git a/core/java/com/android/internal/util/function/pooled/PooledRunnable.java b/core/java/com/android/internal/util/function/pooled/PooledRunnable.java new file mode 100644 index 000000000000..89ca82e2f3ce --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/PooledRunnable.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 com.android.internal.util.function.pooled; + +import com.android.internal.util.FunctionalUtils.ThrowingRunnable; + +/** + * {@link Runnable} + {@link PooledLambda} + * + * @see PooledLambda + * @hide + */ +public interface PooledRunnable extends PooledLambda, Runnable, ThrowingRunnable { + /** @inheritDoc */ + PooledRunnable recycleOnUse(); +} diff --git a/core/java/com/android/internal/util/function/pooled/PooledSupplier.java b/core/java/com/android/internal/util/function/pooled/PooledSupplier.java new file mode 100644 index 000000000000..dd7f73eeff14 --- /dev/null +++ b/core/java/com/android/internal/util/function/pooled/PooledSupplier.java @@ -0,0 +1,59 @@ +/* + * 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 com.android.internal.util.function.pooled; + +import com.android.internal.util.FunctionalUtils.ThrowingSupplier; + +import java.util.function.DoubleSupplier; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; +import java.util.function.Supplier; + +/** + * {@link Supplier} + {@link PooledLambda} + * + * @see PooledLambda + * @hide + */ +public interface PooledSupplier<T> extends PooledLambda, Supplier<T>, ThrowingSupplier<T> { + + /** + * Ignores the result + */ + PooledRunnable asRunnable(); + + /** @inheritDoc */ + PooledSupplier<T> recycleOnUse(); + + /** {@link PooledLambda} + {@link IntSupplier} */ + interface OfInt extends IntSupplier, PooledLambda { + /** @inheritDoc */ + PooledSupplier.OfInt recycleOnUse(); + } + + /** {@link PooledLambda} + {@link LongSupplier} */ + interface OfLong extends LongSupplier, PooledLambda { + /** @inheritDoc */ + PooledSupplier.OfLong recycleOnUse(); + } + + /** {@link PooledLambda} + {@link DoubleSupplier} */ + interface OfDouble extends DoubleSupplier, PooledLambda { + /** @inheritDoc */ + PooledSupplier.OfDouble recycleOnUse(); + } +} diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index 361fd3da97c7..7178a0d677af 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -22,6 +22,7 @@ import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.MergedConfiguration; +import android.view.DisplayCutout; import android.view.DragEvent; import android.view.IWindow; import android.view.IWindowSession; @@ -41,7 +42,8 @@ public class BaseIWindow extends IWindow.Stub { public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout, - boolean alwaysConsumeNavBar, int displayId) { + boolean alwaysConsumeNavBar, int displayId, + DisplayCutout.ParcelableWrapper displayCutout) { if (reportDraw) { try { mSession.finishDrawing(this); diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index b9798075ad27..1fd5564773b1 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -36,6 +36,7 @@ import com.android.internal.view.IInputMethodClient; interface IInputMethodManager { // TODO: Use ParceledListSlice instead List<InputMethodInfo> getInputMethodList(); + List<InputMethodInfo> getVrInputMethodList(); // TODO: Use ParceledListSlice instead List<InputMethodInfo> getEnabledInputMethodList(); List<InputMethodSubtype> getEnabledInputMethodSubtypeList(in String imiId, @@ -79,7 +80,6 @@ interface IInputMethodManager { boolean switchToLastInputMethod(in IBinder token); boolean switchToNextInputMethod(in IBinder token, boolean onlyCurrentIme); boolean shouldOfferSwitchingToNextInputMethod(in IBinder token); - boolean setInputMethodEnabled(String id, boolean enabled); void setAdditionalInputMethodSubtypes(String id, in InputMethodSubtype[] subtypes); int getInputMethodWindowVisibleHeight(); void clearLastInputMethodWindowForTransition(in IBinder token); diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index cc0ef756c6b3..5b65bbe1e90e 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -16,6 +16,8 @@ package com.android.internal.view; +import android.annotation.AnyThread; +import android.annotation.BinderThread; import android.annotation.NonNull; import android.inputmethodservice.AbstractInputMethodService; import android.os.Bundle; @@ -34,6 +36,7 @@ import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags; import android.view.inputmethod.InputContentInfo; import java.lang.ref.WeakReference; +import java.util.concurrent.atomic.AtomicBoolean; public class InputConnectionWrapper implements InputConnection { private static final int MAX_WAIT_TIME_MILLIS = 2000; @@ -44,6 +47,14 @@ public class InputConnectionWrapper implements InputConnection { @MissingMethodFlags private final int mMissingMethods; + /** + * {@code true} if the system already decided to take away IME focus from the target app. This + * can be signaled even when the corresponding signal is in the task queue and + * {@link InputMethodService#onUnbindInput()} is not yet called back on the UI thread. + */ + @NonNull + private final AtomicBoolean mIsUnbindIssued; + static class InputContextCallback extends IInputContextCallback.Stub { private static final String TAG = "InputConnectionWrapper.ICC"; public int mSeq; @@ -67,6 +78,7 @@ public class InputConnectionWrapper implements InputConnection { * sequence number is set to a new integer. We use a sequence number so that replies that * occur after a timeout has expired are not interpreted as replies to a later request. */ + @AnyThread private static InputContextCallback getInstance() { synchronized (InputContextCallback.class) { // Return sInstance if it's non-null, otherwise construct a new callback @@ -90,6 +102,7 @@ public class InputConnectionWrapper implements InputConnection { /** * Makes the given InputContextCallback available for use in the future. */ + @AnyThread private void dispose() { synchronized (InputContextCallback.class) { // If sInstance is non-null, just let this object be garbage-collected @@ -102,7 +115,8 @@ public class InputConnectionWrapper implements InputConnection { } } } - + + @BinderThread public void setTextBeforeCursor(CharSequence textBeforeCursor, int seq) { synchronized (this) { if (seq == mSeq) { @@ -116,6 +130,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @BinderThread public void setTextAfterCursor(CharSequence textAfterCursor, int seq) { synchronized (this) { if (seq == mSeq) { @@ -129,6 +144,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @BinderThread public void setSelectedText(CharSequence selectedText, int seq) { synchronized (this) { if (seq == mSeq) { @@ -142,6 +158,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @BinderThread public void setCursorCapsMode(int capsMode, int seq) { synchronized (this) { if (seq == mSeq) { @@ -155,6 +172,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @BinderThread public void setExtractedText(ExtractedText extractedText, int seq) { synchronized (this) { if (seq == mSeq) { @@ -168,6 +186,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @BinderThread public void setRequestUpdateCursorAnchorInfoResult(boolean result, int seq) { synchronized (this) { if (seq == mSeq) { @@ -181,6 +200,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @BinderThread public void setCommitContentResult(boolean result, int seq) { synchronized (this) { if (seq == mSeq) { @@ -199,6 +219,7 @@ public class InputConnectionWrapper implements InputConnection { * * <p>The caller must be synchronized on this callback object. */ + @AnyThread void waitForResultLocked() { long startTime = SystemClock.uptimeMillis(); long endTime = startTime + MAX_WAIT_TIME_MILLIS; @@ -219,13 +240,20 @@ public class InputConnectionWrapper implements InputConnection { public InputConnectionWrapper( @NonNull WeakReference<AbstractInputMethodService> inputMethodService, - IInputContext inputContext, @MissingMethodFlags final int missingMethods) { + IInputContext inputContext, @MissingMethodFlags final int missingMethods, + @NonNull AtomicBoolean isUnbindIssued) { mInputMethodService = inputMethodService; mIInputContext = inputContext; mMissingMethods = missingMethods; + mIsUnbindIssued = isUnbindIssued; } + @AnyThread public CharSequence getTextAfterCursor(int length, int flags) { + if (mIsUnbindIssued.get()) { + return null; + } + CharSequence value = null; try { InputContextCallback callback = InputContextCallback.getInstance(); @@ -242,8 +270,13 @@ public class InputConnectionWrapper implements InputConnection { } return value; } - + + @AnyThread public CharSequence getTextBeforeCursor(int length, int flags) { + if (mIsUnbindIssued.get()) { + return null; + } + CharSequence value = null; try { InputContextCallback callback = InputContextCallback.getInstance(); @@ -261,7 +294,12 @@ public class InputConnectionWrapper implements InputConnection { return value; } + @AnyThread public CharSequence getSelectedText(int flags) { + if (mIsUnbindIssued.get()) { + return null; + } + if (isMethodMissing(MissingMethodFlags.GET_SELECTED_TEXT)) { // This method is not implemented. return null; @@ -283,7 +321,12 @@ public class InputConnectionWrapper implements InputConnection { return value; } + @AnyThread public int getCursorCapsMode(int reqModes) { + if (mIsUnbindIssued.get()) { + return 0; + } + int value = 0; try { InputContextCallback callback = InputContextCallback.getInstance(); @@ -301,7 +344,12 @@ public class InputConnectionWrapper implements InputConnection { return value; } + @AnyThread public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { + if (mIsUnbindIssued.get()) { + return null; + } + ExtractedText value = null; try { InputContextCallback callback = InputContextCallback.getInstance(); @@ -318,7 +366,8 @@ public class InputConnectionWrapper implements InputConnection { } return value; } - + + @AnyThread public boolean commitText(CharSequence text, int newCursorPosition) { try { mIInputContext.commitText(text, newCursorPosition); @@ -328,6 +377,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean commitCompletion(CompletionInfo text) { if (isMethodMissing(MissingMethodFlags.COMMIT_CORRECTION)) { // This method is not implemented. @@ -341,6 +391,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean commitCorrection(CorrectionInfo correctionInfo) { try { mIInputContext.commitCorrection(correctionInfo); @@ -350,6 +401,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean setSelection(int start, int end) { try { mIInputContext.setSelection(start, end); @@ -358,7 +410,8 @@ public class InputConnectionWrapper implements InputConnection { return false; } } - + + @AnyThread public boolean performEditorAction(int actionCode) { try { mIInputContext.performEditorAction(actionCode); @@ -367,7 +420,8 @@ public class InputConnectionWrapper implements InputConnection { return false; } } - + + @AnyThread public boolean performContextMenuAction(int id) { try { mIInputContext.performContextMenuAction(id); @@ -377,6 +431,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean setComposingRegion(int start, int end) { if (isMethodMissing(MissingMethodFlags.SET_COMPOSING_REGION)) { // This method is not implemented. @@ -390,6 +445,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean setComposingText(CharSequence text, int newCursorPosition) { try { mIInputContext.setComposingText(text, newCursorPosition); @@ -399,6 +455,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean finishComposingText() { try { mIInputContext.finishComposingText(); @@ -408,6 +465,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean beginBatchEdit() { try { mIInputContext.beginBatchEdit(); @@ -416,7 +474,8 @@ public class InputConnectionWrapper implements InputConnection { return false; } } - + + @AnyThread public boolean endBatchEdit() { try { mIInputContext.endBatchEdit(); @@ -425,7 +484,8 @@ public class InputConnectionWrapper implements InputConnection { return false; } } - + + @AnyThread public boolean sendKeyEvent(KeyEvent event) { try { mIInputContext.sendKeyEvent(event); @@ -435,6 +495,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean clearMetaKeyStates(int states) { try { mIInputContext.clearMetaKeyStates(states); @@ -444,6 +505,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean deleteSurroundingText(int beforeLength, int afterLength) { try { mIInputContext.deleteSurroundingText(beforeLength, afterLength); @@ -453,6 +515,7 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { if (isMethodMissing(MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS)) { // This method is not implemented. @@ -466,11 +529,13 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean reportFullscreenMode(boolean enabled) { // Nothing should happen when called from input method. return false; } + @AnyThread public boolean performPrivateCommand(String action, Bundle data) { try { mIInputContext.performPrivateCommand(action, data); @@ -480,7 +545,12 @@ public class InputConnectionWrapper implements InputConnection { } } + @AnyThread public boolean requestCursorUpdates(int cursorUpdateMode) { + if (mIsUnbindIssued.get()) { + return false; + } + boolean result = false; if (isMethodMissing(MissingMethodFlags.REQUEST_CURSOR_UPDATES)) { // This method is not implemented. @@ -502,16 +572,23 @@ public class InputConnectionWrapper implements InputConnection { return result; } + @AnyThread public Handler getHandler() { // Nothing should happen when called from input method. return null; } + @AnyThread public void closeConnection() { // Nothing should happen when called from input method. } + @AnyThread public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { + if (mIsUnbindIssued.get()) { + return false; + } + boolean result = false; if (isMethodMissing(MissingMethodFlags.COMMIT_CONTENT)) { // This method is not implemented. @@ -542,10 +619,12 @@ public class InputConnectionWrapper implements InputConnection { return result; } + @AnyThread private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) { return (mMissingMethods & methodFlag) == methodFlag; } + @AnyThread @Override public String toString() { return "InputConnectionWrapper{idHash=#" diff --git a/core/java/com/android/internal/view/RotationPolicy.java b/core/java/com/android/internal/view/RotationPolicy.java index b479cb1fb6d3..d7b91325c961 100644 --- a/core/java/com/android/internal/view/RotationPolicy.java +++ b/core/java/com/android/internal/view/RotationPolicy.java @@ -108,11 +108,19 @@ public final class RotationPolicy { * Enables or disables rotation lock from the system UI toggle. */ public static void setRotationLock(Context context, final boolean enabled) { + final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION; + setRotationLockAtAngle(context, enabled, rotation); + } + + /** + * Enables or disables rotation lock at a specific rotation from system UI. + */ + public static void setRotationLockAtAngle(Context context, final boolean enabled, + final int rotation) { Settings.System.putIntForUser(context.getContentResolver(), Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0, UserHandle.USER_CURRENT); - final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION; setRotationLock(enabled, rotation); } diff --git a/core/java/com/android/internal/view/TooltipPopup.java b/core/java/com/android/internal/view/TooltipPopup.java index d38ea2c19af4..24f0b0cc91c5 100644 --- a/core/java/com/android/internal/view/TooltipPopup.java +++ b/core/java/com/android/internal/view/TooltipPopup.java @@ -142,7 +142,7 @@ public class TooltipPopup { mTmpAnchorPos[1] -= mTmpAppPos[1]; // mTmpAnchorPos is now relative to the main app window. - outParams.x = mTmpAnchorPos[0] + offsetX - mTmpDisplayFrame.width() / 2; + outParams.x = mTmpAnchorPos[0] + offsetX - appView.getWidth() / 2; final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); mContentView.measure(spec, spec); @@ -157,6 +157,9 @@ public class TooltipPopup { outParams.y = yBelow; } } else { + // Use mTmpDisplayFrame.height() as the lower boundary instead of appView.getHeight(), + // as the latter includes the navigation bar, and tooltips do not look good over + // the navigation bar. if (yBelow + tooltipHeight <= mTmpDisplayFrame.height()) { outParams.y = yBelow; } else { diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java index 83a2838be07e..8f80bfe3fb50 100644 --- a/core/java/com/android/internal/view/menu/ListMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java @@ -16,14 +16,18 @@ package com.android.internal.view.menu; +import com.android.internal.R; + import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.AbsListView; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; @@ -34,25 +38,28 @@ import android.widget.TextView; /** * The item view for each item in the ListView-based MenuViews. */ -public class ListMenuItemView extends LinearLayout implements MenuView.ItemView { +public class ListMenuItemView extends LinearLayout + implements MenuView.ItemView, AbsListView.SelectionBoundsAdjuster { private static final String TAG = "ListMenuItemView"; - private MenuItemImpl mItemData; - + private MenuItemImpl mItemData; + private ImageView mIconView; private RadioButton mRadioButton; private TextView mTitleView; private CheckBox mCheckBox; private TextView mShortcutView; private ImageView mSubMenuArrowView; - + private ImageView mGroupDivider; + private Drawable mBackground; private int mTextAppearance; private Context mTextAppearanceContext; private boolean mPreserveIconSpacing; private Drawable mSubMenuArrow; - + private boolean mHasListDivider; + private int mMenuType; - + private LayoutInflater mInflater; private boolean mForceShowIcon; @@ -71,8 +78,14 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView com.android.internal.R.styleable.MenuView_preserveIconSpacing, false); mTextAppearanceContext = context; mSubMenuArrow = a.getDrawable(com.android.internal.R.styleable.MenuView_subMenuArrow); - + + final TypedArray b = context.getTheme() + .obtainStyledAttributes(null, new int[] { com.android.internal.R.attr.divider }, + com.android.internal.R.attr.dropDownListViewStyle, 0); + mHasListDivider = b.hasValue(0); + a.recycle(); + b.recycle(); } public ListMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) { @@ -86,20 +99,21 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView @Override protected void onFinishInflate() { super.onFinishInflate(); - + setBackgroundDrawable(mBackground); - + mTitleView = findViewById(com.android.internal.R.id.title); if (mTextAppearance != -1) { mTitleView.setTextAppearance(mTextAppearanceContext, mTextAppearance); } - + mShortcutView = findViewById(com.android.internal.R.id.shortcut); mSubMenuArrowView = findViewById(com.android.internal.R.id.submenuarrow); if (mSubMenuArrowView != null) { mSubMenuArrowView.setImageDrawable(mSubMenuArrow); } + mGroupDivider = findViewById(com.android.internal.R.id.group_divider); } public void initialize(MenuItemImpl itemData, int menuType) { @@ -124,13 +138,13 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView public void setTitle(CharSequence title) { if (title != null) { mTitleView.setText(title); - + if (mTitleView.getVisibility() != VISIBLE) mTitleView.setVisibility(VISIBLE); } else { if (mTitleView.getVisibility() != GONE) mTitleView.setVisibility(GONE); } } - + public MenuItemImpl getItemData() { return mItemData; } @@ -139,11 +153,11 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView if (!checkable && mRadioButton == null && mCheckBox == null) { return; } - + // Depending on whether its exclusive check or not, the checkbox or // radio button will be the one in use (and the other will be otherCompoundButton) final CompoundButton compoundButton; - final CompoundButton otherCompoundButton; + final CompoundButton otherCompoundButton; if (mItemData.isExclusiveCheckable()) { if (mRadioButton == null) { @@ -158,15 +172,15 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView compoundButton = mCheckBox; otherCompoundButton = mRadioButton; } - + if (checkable) { compoundButton.setChecked(mItemData.isChecked()); - + final int newVisibility = checkable ? VISIBLE : GONE; if (compoundButton.getVisibility() != newVisibility) { compoundButton.setVisibility(newVisibility); } - + // Make sure the other compound button isn't visible if (otherCompoundButton != null && otherCompoundButton.getVisibility() != GONE) { otherCompoundButton.setVisibility(GONE); @@ -176,10 +190,10 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView if (mRadioButton != null) mRadioButton.setVisibility(GONE); } } - + public void setChecked(boolean checked) { CompoundButton compoundButton; - + if (mItemData.isExclusiveCheckable()) { if (mRadioButton == null) { insertRadioButton(); @@ -191,7 +205,7 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView } compoundButton = mCheckBox; } - + compoundButton.setChecked(checked); } @@ -213,21 +227,21 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView mShortcutView.setVisibility(newVisibility); } } - + public void setIcon(Drawable icon) { final boolean showIcon = mItemData.shouldShowIcon() || mForceShowIcon; if (!showIcon && !mPreserveIconSpacing) { return; } - + if (mIconView == null && icon == null && !mPreserveIconSpacing) { return; } - + if (mIconView == null) { insertIconView(); } - + if (icon != null || mPreserveIconSpacing) { mIconView.setImageDrawable(showIcon ? icon : null); @@ -238,7 +252,7 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView mIconView.setVisibility(GONE); } } - + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mIconView != null && mPreserveIconSpacing) { @@ -258,7 +272,7 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView this, false); addView(mIconView, 0); } - + private void insertRadioButton() { LayoutInflater inflater = getInflater(); mRadioButton = @@ -266,7 +280,7 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView this, false); addView(mRadioButton); } - + private void insertCheckBox() { LayoutInflater inflater = getInflater(); mCheckBox = @@ -282,7 +296,7 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView public boolean showsIcon() { return mForceShowIcon; } - + private LayoutInflater getInflater() { if (mInflater == null) { mInflater = LayoutInflater.from(mContext); @@ -298,4 +312,28 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView info.setCanOpenPopup(true); } } + + /** + * Enable or disable group dividers for this view. + */ + public void setGroupDividerEnabled(boolean groupDividerEnabled) { + // If mHasListDivider is true, disabling the groupDivider. + // Otherwise, checking enbling it according to groupDividerEnabled flag. + if (mGroupDivider != null) { + mGroupDivider.setVisibility(!mHasListDivider + && groupDividerEnabled ? View.VISIBLE : View.GONE); + } + } + + @Override + public void adjustListItemSelectionBounds(Rect rect) { + if (mGroupDivider != null && mGroupDivider.getVisibility() == View.VISIBLE) { + // groupDivider is a part of MenuItemListView. + // If ListMenuItem with divider enabled is hovered/clicked, divider also gets selected. + // Clipping the selector bounds from the top divider portion when divider is enabled, + // so that divider does not get selected on hover or click. + final LayoutParams lp = (LayoutParams) mGroupDivider.getLayoutParams(); + rect.top += mGroupDivider.getHeight() + lp.topMargin + lp.bottomMargin; + } + } } diff --git a/core/java/com/android/internal/view/menu/MenuAdapter.java b/core/java/com/android/internal/view/menu/MenuAdapter.java index 673cfd12d878..2834d39a4f98 100644 --- a/core/java/com/android/internal/view/menu/MenuAdapter.java +++ b/core/java/com/android/internal/view/menu/MenuAdapter.java @@ -81,6 +81,14 @@ public class MenuAdapter extends BaseAdapter { convertView = mInflater.inflate(ITEM_LAYOUT, parent, false); } + final int currGroupId = getItem(position).getGroupId(); + final int prevGroupId = + position - 1 >= 0 ? getItem(position - 1).getGroupId() : currGroupId; + // Show a divider if adjacent items are in different groups. + ((ListMenuItemView) convertView) + .setGroupDividerEnabled(mAdapterMenu.isGroupDividerEnabled() + && (currGroupId != prevGroupId)); + MenuView.ItemView itemView = (MenuView.ItemView) convertView; if (mForceShowIcon) { ((ListMenuItemView) convertView).setForceShowIcon(true); diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java index 7eb0f4dbddc7..b53459e072d4 100644 --- a/core/java/com/android/internal/view/menu/MenuBuilder.java +++ b/core/java/com/android/internal/view/menu/MenuBuilder.java @@ -156,7 +156,12 @@ public class MenuBuilder implements Menu { * Currently expanded menu item; must be collapsed when we clear. */ private MenuItemImpl mExpandedItem; - + + /** + * Whether group dividers are enabled. + */ + private boolean mGroupDividerEnabled = false; + /** * Called by menu to notify of close and selection changes. */ @@ -462,6 +467,15 @@ public class MenuBuilder implements Menu { return addSubMenu(group, id, categoryOrder, mResources.getString(title)); } + @Override + public void setGroupDividerEnabled(boolean groupDividerEnabled) { + mGroupDividerEnabled = groupDividerEnabled; + } + + public boolean isGroupDividerEnabled() { + return mGroupDividerEnabled; + } + public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { PackageManager pm = mContext.getPackageManager(); diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java index 15e271edc882..9d012de33089 100644 --- a/core/java/com/android/internal/view/menu/MenuItemImpl.java +++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java @@ -658,7 +658,7 @@ public final class MenuItemImpl implements MenuItem { @Override public boolean requiresOverflow() { - return (mShowAsAction & SHOW_AS_OVERFLOW_ALWAYS) == SHOW_AS_OVERFLOW_ALWAYS; + return !requiresActionButton() && !requestsActionButton(); } public void setIsActionButton(boolean isActionButton) { diff --git a/core/java/com/android/internal/view/menu/SubMenuBuilder.java b/core/java/com/android/internal/view/menu/SubMenuBuilder.java index cf741bf5bb02..897440ebf893 100644 --- a/core/java/com/android/internal/view/menu/SubMenuBuilder.java +++ b/core/java/com/android/internal/view/menu/SubMenuBuilder.java @@ -130,4 +130,14 @@ public class SubMenuBuilder extends MenuBuilder implements SubMenu { } return super.getActionViewStatesKey() + ":" + itemId; } + + @Override + public void setGroupDividerEnabled(boolean groupDividerEnabled) { + mParentMenu.setGroupDividerEnabled(groupDividerEnabled); + } + + @Override + public boolean isGroupDividerEnabled() { + return mParentMenu.isGroupDividerEnabled(); + } } diff --git a/core/java/com/android/internal/widget/FloatingToolbar.java b/core/java/com/android/internal/widget/FloatingToolbar.java index f63b5a213528..e3b1c01fd12b 100644 --- a/core/java/com/android/internal/widget/FloatingToolbar.java +++ b/core/java/com/android/internal/widget/FloatingToolbar.java @@ -64,6 +64,7 @@ import com.android.internal.R; import com.android.internal.util.Preconditions; import java.util.ArrayList; +import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -118,6 +119,36 @@ public final class FloatingToolbar { }; /** + * Sorts the list of menu items to conform to certain requirements. + */ + private final Comparator<MenuItem> mMenuItemComparator = (menuItem1, menuItem2) -> { + // Ensure the assist menu item is always the first item: + if (menuItem1.getItemId() == android.R.id.textAssist) { + return menuItem2.getItemId() == android.R.id.textAssist ? 0 : -1; + } + if (menuItem2.getItemId() == android.R.id.textAssist) { + return 1; + } + + // Order by SHOW_AS_ACTION type: + if (menuItem1.requiresActionButton()) { + return menuItem2.requiresActionButton() ? 0 : -1; + } + if (menuItem2.requiresActionButton()) { + return 1; + } + if (menuItem1.requiresOverflow()) { + return menuItem2.requiresOverflow() ? 0 : 1; + } + if (menuItem2.requiresOverflow()) { + return -1; + } + + // Order by order value: + return menuItem1.getOrder() - menuItem2.getOrder(); + }; + + /** * Initializes a floating toolbar. */ public FloatingToolbar(Window window) { @@ -230,7 +261,7 @@ public final class FloatingToolbar { private void doShow() { List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); - tidy(menuItems); + menuItems.sort(mMenuItemComparator); if (!isCurrentlyShowing(menuItems) || mWidthChanged) { mPopup.dismiss(); mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth); @@ -288,36 +319,6 @@ public final class FloatingToolbar { return menuItems; } - /** - * Update the list of menu items to conform to certain requirements. - */ - private void tidy(List<MenuItem> menuItems) { - int assistItemIndex = -1; - Drawable assistItemDrawable = null; - - final int size = menuItems.size(); - for (int i = 0; i < size; i++) { - final MenuItem menuItem = menuItems.get(i); - - if (menuItem.getItemId() == android.R.id.textAssist) { - assistItemIndex = i; - assistItemDrawable = menuItem.getIcon(); - } - - // Remove icons for all menu items with text. - if (!TextUtils.isEmpty(menuItem.getTitle())) { - menuItem.setIcon(null); - } - } - if (assistItemIndex > -1) { - final MenuItem assistMenuItem = menuItems.remove(assistItemIndex); - // Ensure the assist menu item preserves its icon. - assistMenuItem.setIcon(assistItemDrawable); - // Ensure the assist menu item is always the first item. - menuItems.add(0, assistMenuItem); - } - } - private void registerOrientationHandler() { unregisterOrientationHandler(); mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler); @@ -1148,7 +1149,8 @@ public final class FloatingToolbar { // add the overflow menu items to the end of the remainingMenuItems list. final LinkedList<MenuItem> overflowMenuItems = new LinkedList(); for (MenuItem menuItem : menuItems) { - if (menuItem.requiresOverflow()) { + if (menuItem.getItemId() != android.R.id.textAssist + && menuItem.requiresOverflow()) { overflowMenuItems.add(menuItem); } else { remainingMenuItems.add(menuItem); @@ -1171,7 +1173,9 @@ public final class FloatingToolbar { break; } - View menuItemButton = createMenuItemButton(mContext, menuItem, mIconTextSpacing); + final boolean showIcon = isFirstItem && menuItem.getItemId() == R.id.textAssist; + final View menuItemButton = createMenuItemButton( + mContext, menuItem, mIconTextSpacing, showIcon); // Adding additional start padding for the first button to even out button spacing. if (isFirstItem) { @@ -1193,16 +1197,17 @@ public final class FloatingToolbar { } menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); - final int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth); + final int menuItemButtonWidth = Math.min( + menuItemButton.getMeasuredWidth(), toolbarWidth); final boolean isNewGroup = !isFirstItem && lastGroupId != menuItem.getGroupId(); final int extraPadding = isNewGroup ? menuItemButton.getPaddingEnd() * 2 : 0; // Check if we can fit an item while reserving space for the overflowButton. - boolean canFitWithOverflow = + final boolean canFitWithOverflow = menuItemButtonWidth <= availableWidth - mOverflowButtonSize.getWidth() - extraPadding; - boolean canFitNoOverflow = + final boolean canFitNoOverflow = isLastItem && menuItemButtonWidth <= availableWidth - extraPadding; if (canFitWithOverflow || canFitNoOverflow) { if (isNewGroup) { @@ -1211,7 +1216,8 @@ public final class FloatingToolbar { // Add extra padding to the end of the previous button. // Half of the extra padding (less borderWidth) goes to the previous button. - View previousButton = mMainPanel.getChildAt(mMainPanel.getChildCount() - 1); + final View previousButton = mMainPanel.getChildAt( + mMainPanel.getChildCount() - 1); final int prevPaddingEnd = previousButton.getPaddingEnd() + extraPadding / 2 - dividerWidth; previousButton.setPaddingRelative( @@ -1612,7 +1618,8 @@ public final class FloatingToolbar { public View getView(MenuItem menuItem, int minimumWidth, View convertView) { Preconditions.checkNotNull(menuItem); if (convertView != null) { - updateMenuItemButton(convertView, menuItem, mIconTextSpacing); + updateMenuItemButton( + convertView, menuItem, mIconTextSpacing, shouldShowIcon(menuItem)); } else { convertView = createMenuButton(menuItem); } @@ -1621,17 +1628,26 @@ public final class FloatingToolbar { } public int calculateWidth(MenuItem menuItem) { - updateMenuItemButton(mCalculator, menuItem, mIconTextSpacing); + updateMenuItemButton( + mCalculator, menuItem, mIconTextSpacing, shouldShowIcon(menuItem)); mCalculator.measure( View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); return mCalculator.getMeasuredWidth(); } private View createMenuButton(MenuItem menuItem) { - View button = createMenuItemButton(mContext, menuItem, mIconTextSpacing); + View button = createMenuItemButton( + mContext, menuItem, mIconTextSpacing, shouldShowIcon(menuItem)); button.setPadding(mSidePadding, 0, mSidePadding, 0); return button; } + + private boolean shouldShowIcon(MenuItem menuItem) { + if (menuItem != null) { + return menuItem.getGroupId() == android.R.id.textAssist; + } + return false; + } } } @@ -1639,11 +1655,11 @@ public final class FloatingToolbar { * Creates and returns a menu button for the specified menu item. */ private static View createMenuItemButton( - Context context, MenuItem menuItem, int iconTextSpacing) { + Context context, MenuItem menuItem, int iconTextSpacing, boolean showIcon) { final View menuItemButton = LayoutInflater.from(context) .inflate(R.layout.floating_popup_menu_button, null); if (menuItem != null) { - updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing); + updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing, showIcon); } return menuItemButton; } @@ -1652,18 +1668,19 @@ public final class FloatingToolbar { * Updates the specified menu item button with the specified menu item data. */ private static void updateMenuItemButton( - View menuItemButton, MenuItem menuItem, int iconTextSpacing) { - final TextView buttonText = (TextView) menuItemButton.findViewById( + View menuItemButton, MenuItem menuItem, int iconTextSpacing, boolean showIcon) { + final TextView buttonText = menuItemButton.findViewById( R.id.floating_toolbar_menu_item_text); + buttonText.setEllipsize(null); if (TextUtils.isEmpty(menuItem.getTitle())) { buttonText.setVisibility(View.GONE); } else { buttonText.setVisibility(View.VISIBLE); buttonText.setText(menuItem.getTitle()); } - final ImageView buttonIcon = (ImageView) menuItemButton - .findViewById(R.id.floating_toolbar_menu_item_image); - if (menuItem.getIcon() == null) { + final ImageView buttonIcon = menuItemButton.findViewById( + R.id.floating_toolbar_menu_item_image); + if (menuItem.getIcon() == null || !showIcon) { buttonIcon.setVisibility(View.GONE); if (buttonText != null) { buttonText.setPaddingRelative(0, 0, 0, 0); diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java index 31b167d43f66..09f7282f097a 100644 --- a/core/java/com/android/internal/widget/ImageFloatingTextView.java +++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java @@ -81,6 +81,7 @@ public class ImageFloatingTextView extends TextView { .setTextDirection(getTextDirectionHeuristic()) .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier()) .setIncludePad(getIncludeFontPadding()) + .setUseLineSpacingFromFallbacks(true) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); int maxLines; @@ -175,8 +176,4 @@ public class ImageFloatingTextView extends TextView { } return false; } - - public int getLayoutHeight() { - return getLayout().getHeight(); - } } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 54399061a38f..4be6b28ad95c 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -1614,7 +1614,8 @@ public class LockPatternUtils { STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW, SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, - STRONG_AUTH_REQUIRED_AFTER_TIMEOUT}) + STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN}) @Retention(RetentionPolicy.SOURCE) public @interface StrongAuthFlags {} @@ -1651,6 +1652,11 @@ public class LockPatternUtils { public static final int STRONG_AUTH_REQUIRED_AFTER_TIMEOUT = 0x10; /** + * Strong authentication is required because the user has triggered lockdown. + */ + public static final int STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN = 0x20; + + /** * Strong auth flags that do not prevent fingerprint from being accepted as auth. * * If any other flags are set, fingerprint is disabled. diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java new file mode 100644 index 000000000000..792f9212d93e --- /dev/null +++ b/core/java/com/android/internal/widget/MessagingGroup.java @@ -0,0 +1,393 @@ +/* + * 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 com.android.internal.widget; + +import android.annotation.AttrRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StyleRes; +import android.content.Context; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Pools; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RemoteViews; + +import com.android.internal.R; +import com.android.internal.util.NotificationColorUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * A message of a {@link MessagingLayout}. + */ +@RemoteViews.RemoteView +public class MessagingGroup extends LinearLayout implements MessagingLinearLayout.MessagingChild { + private static Pools.SimplePool<MessagingGroup> sInstancePool + = new Pools.SynchronizedPool<>(10); + private MessagingLinearLayout mMessageContainer; + private ImageFloatingTextView mSenderName; + private ImageView mAvatarView; + private String mAvatarSymbol = ""; + private int mLayoutColor; + private CharSequence mAvatarName = ""; + private Icon mAvatarIcon; + private ColorFilter mMessageBackgroundFilter; + private int mTextColor; + private List<MessagingMessage> mMessages; + private ArrayList<MessagingMessage> mAddedMessages = new ArrayList<>(); + private boolean mFirstLayout; + private boolean mIsHidingAnimated; + + public MessagingGroup(@NonNull Context context) { + super(context); + } + + public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mMessageContainer = findViewById(R.id.group_message_container); + mSenderName = findViewById(R.id.message_name); + mSenderName.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR); + mAvatarView = findViewById(R.id.message_icon); + } + + public void setSender(CharSequence sender) { + if (sender == null) { + mAvatarView.setVisibility(GONE); + mSenderName.setVisibility(GONE); + setGravity(Gravity.END); + mMessageBackgroundFilter = new PorterDuffColorFilter(mLayoutColor, + PorterDuff.Mode.SRC_ATOP); + mTextColor = NotificationColorUtil.isColorLight(mLayoutColor) ? getNormalTextColor() + : Color.WHITE; + } else { + mSenderName.setText(sender); + mAvatarView.setVisibility(VISIBLE); + mSenderName.setVisibility(VISIBLE); + setGravity(Gravity.START); + mMessageBackgroundFilter = null; + mTextColor = getNormalTextColor(); + } + } + + private int getNormalTextColor() { + return mContext.getColor(R.color.notification_primary_text_color_light); + } + + public void setAvatar(Icon icon) { + mAvatarIcon = icon; + mAvatarView.setImageIcon(icon); + mAvatarSymbol = ""; + mLayoutColor = 0; + mAvatarName = ""; + } + + static MessagingGroup createGroup(MessagingLinearLayout layout) {; + MessagingGroup createdGroup = sInstancePool.acquire(); + if (createdGroup == null) { + createdGroup = (MessagingGroup) LayoutInflater.from(layout.getContext()).inflate( + R.layout.notification_template_messaging_group, layout, + false); + createdGroup.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR); + } + layout.addView(createdGroup); + return createdGroup; + } + + public void removeMessage(MessagingMessage messagingMessage) { + mMessageContainer.removeView(messagingMessage); + Runnable recycleRunnable = () -> { + mMessageContainer.removeTransientView(messagingMessage); + messagingMessage.recycle(); + if (mMessageContainer.getChildCount() == 0 + && mMessageContainer.getTransientViewCount() == 0) { + ViewParent parent = getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(MessagingGroup.this); + } + setAvatar(null); + mAvatarView.setAlpha(1.0f); + mAvatarView.setTranslationY(0.0f); + mSenderName.setAlpha(1.0f); + mSenderName.setTranslationY(0.0f); + sInstancePool.release(MessagingGroup.this); + } + }; + if (isShown()) { + mMessageContainer.addTransientView(messagingMessage, 0); + performRemoveAnimation(messagingMessage, recycleRunnable); + if (mMessageContainer.getChildCount() == 0) { + removeGroupAnimated(null); + } + } else { + recycleRunnable.run(); + } + + } + + private void removeGroupAnimated(Runnable endAction) { + MessagingPropertyAnimator.fadeOut(mAvatarView, null); + MessagingPropertyAnimator.startLocalTranslationTo(mAvatarView, + (int) (-getHeight() * 0.5f), MessagingLayout.FAST_OUT_LINEAR_IN); + MessagingPropertyAnimator.fadeOut(mSenderName, null); + MessagingPropertyAnimator.startLocalTranslationTo(mSenderName, + (int) (-getHeight() * 0.5f), MessagingLayout.FAST_OUT_LINEAR_IN); + boolean endActionTriggered = false; + for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) { + View child = mMessageContainer.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + final ViewGroup.LayoutParams lp = child.getLayoutParams(); + if (lp instanceof MessagingLinearLayout.LayoutParams + && ((MessagingLinearLayout.LayoutParams) lp).hide + && !((MessagingLinearLayout.LayoutParams) lp).visibleBefore) { + continue; + } + Runnable childEndAction = endActionTriggered ? null : endAction; + performRemoveAnimation(child, childEndAction); + endActionTriggered = true; + } + if (!endActionTriggered && endAction != null) { + endAction.run(); + } + } + + public void performRemoveAnimation(View message, + Runnable recycleRunnable) { + MessagingPropertyAnimator.fadeOut(message, recycleRunnable); + MessagingPropertyAnimator.startLocalTranslationTo(message, + (int) (-getHeight() * 0.5f), MessagingLayout.FAST_OUT_LINEAR_IN); + } + + public CharSequence getSenderName() { + return mSenderName.getText(); + } + + public void setSenderVisible(boolean visible) { + mSenderName.setVisibility(visible ? VISIBLE : GONE); + } + + public static void dropCache() { + sInstancePool = new Pools.SynchronizedPool<>(10); + } + + @Override + public int getMeasuredType() { + boolean hasNormal = false; + for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) { + View child = mMessageContainer.getChildAt(i); + if (child instanceof MessagingLinearLayout.MessagingChild) { + int type = ((MessagingLinearLayout.MessagingChild) child).getMeasuredType(); + if (type == MEASURED_TOO_SMALL) { + if (hasNormal) { + return MEASURED_SHORTENED; + } else { + return MEASURED_TOO_SMALL; + } + } else if (type == MEASURED_SHORTENED) { + return MEASURED_SHORTENED; + } else { + hasNormal = true; + } + } + } + return MEASURED_NORMAL; + } + + @Override + public int getConsumedLines() { + int result = 0; + for (int i = 0; i < mMessageContainer.getChildCount(); i++) { + View child = mMessageContainer.getChildAt(i); + if (child instanceof MessagingLinearLayout.MessagingChild) { + result += ((MessagingLinearLayout.MessagingChild) child).getConsumedLines(); + } + } + // A group is usually taking up quite some space with the padding and the name, let's add 1 + return result + 1; + } + + @Override + public void setMaxDisplayedLines(int lines) { + mMessageContainer.setMaxDisplayedLines(lines); + } + + @Override + public void hideAnimated() { + setIsHidingAnimated(true); + removeGroupAnimated(() -> setIsHidingAnimated(false)); + } + + @Override + public boolean isHidingAnimated() { + return mIsHidingAnimated; + } + + private void setIsHidingAnimated(boolean isHiding) { + ViewParent parent = getParent(); + mIsHidingAnimated = isHiding; + invalidate(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).invalidate(); + } + } + + public Icon getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol, + int layoutColor) { + if (mAvatarName.equals(avatarName) && mAvatarSymbol.equals(avatarSymbol) + && layoutColor == mLayoutColor) { + return mAvatarIcon; + } + return null; + } + + public void setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol, + int layoutColor) { + if (!mAvatarName.equals(avatarName) || !mAvatarSymbol.equals(avatarSymbol) + || layoutColor != mLayoutColor) { + setAvatar(cachedIcon); + mAvatarSymbol = avatarSymbol; + mLayoutColor = layoutColor; + mAvatarName = avatarName; + } + } + + public void setLayoutColor(int layoutColor) { + mLayoutColor = layoutColor; + } + + public void setMessages(List<MessagingMessage> group) { + // Let's now make sure all children are added and in the correct order + for (int messageIndex = 0; messageIndex < group.size(); messageIndex++) { + MessagingMessage message = group.get(messageIndex); + if (message.getGroup() != this) { + message.setMessagingGroup(this); + ViewParent parent = mMessageContainer.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(message); + } + mMessageContainer.addView(message, messageIndex); + mAddedMessages.add(message); + } + if (messageIndex != mMessageContainer.indexOfChild(message)) { + mMessageContainer.removeView(message); + mMessageContainer.addView(message, messageIndex); + } + // Let's make sure the message color is correct + Drawable targetDrawable = message.getBackground(); + + if (targetDrawable != null) { + targetDrawable.mutate().setColorFilter(mMessageBackgroundFilter); + } + message.setTextColor(mTextColor); + } + mMessages = group; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (!mAddedMessages.isEmpty()) { + getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + for (MessagingMessage message : mAddedMessages) { + if (!message.isShown()) { + continue; + } + MessagingPropertyAnimator.fadeIn(message); + if (!mFirstLayout) { + MessagingPropertyAnimator.startLocalTranslationFrom(message, + message.getHeight(), MessagingLayout.LINEAR_OUT_SLOW_IN); + } + } + mAddedMessages.clear(); + getViewTreeObserver().removeOnPreDrawListener(this); + return true; + } + }); + } + mFirstLayout = false; + } + + /** + * Calculates the group compatibility between this and another group. + * + * @param otherGroup the other group to compare it with + * + * @return 0 if the groups are totally incompatible or 1 + the number of matching messages if + * they match. + */ + public int calculateGroupCompatibility(MessagingGroup otherGroup) { + if (TextUtils.equals(getSenderName(),otherGroup.getSenderName())) { + int result = 1; + for (int i = 0; i < mMessages.size() && i < otherGroup.mMessages.size(); i++) { + MessagingMessage ownMessage = mMessages.get(mMessages.size() - 1 - i); + MessagingMessage otherMessage = otherGroup.mMessages.get( + otherGroup.mMessages.size() - 1 - i); + if (!ownMessage.sameAs(otherMessage)) { + return result; + } + result++; + } + return result; + } + return 0; + } + + public View getSender() { + return mSenderName; + } + + public View getAvatar() { + return mAvatarView; + } + + public MessagingLinearLayout getMessageContainer() { + return mMessageContainer; + } +} diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java new file mode 100644 index 000000000000..2acdc015f8ef --- /dev/null +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -0,0 +1,444 @@ +/* + * 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 com.android.internal.widget; + +import android.annotation.AttrRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StyleRes; +import android.app.Notification; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.RemotableViewMethod; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; +import android.widget.FrameLayout; +import android.widget.RemoteViews; +import android.widget.TextView; + +import com.android.internal.R; +import com.android.internal.graphics.ColorUtils; +import com.android.internal.util.NotificationColorUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal + * messages and adapts the layout accordingly. + */ +@RemoteViews.RemoteView +public class MessagingLayout extends FrameLayout { + + private static final float COLOR_SHIFT_AMOUNT = 60; + private static final Consumer<MessagingMessage> REMOVE_MESSAGE + = MessagingMessage::removeMessage; + public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); + public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); + public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR + = new MessagingPropertyAnimator(); + private List<MessagingMessage> mMessages = new ArrayList<>(); + private List<MessagingMessage> mHistoricMessages = new ArrayList<>(); + private MessagingLinearLayout mMessagingLinearLayout; + private View mContractedMessage; + private boolean mShowHistoricMessages; + private ArrayList<MessagingGroup> mGroups = new ArrayList<>(); + private TextView mTitleView; + private int mLayoutColor; + private int mAvatarSize; + private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Paint mTextPaint = new Paint(); + private CharSequence mConversationTitle; + private Icon mLargeIcon; + private boolean mIsOneToOne; + private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); + + public MessagingLayout(@NonNull Context context) { + super(context); + } + + public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mMessagingLinearLayout = findViewById(R.id.notification_messaging); + mMessagingLinearLayout.setMessagingLayout(this); + // We still want to clip, but only on the top, since views can temporarily out of bounds + // during transitions. + DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); + Rect rect = new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels); + mMessagingLinearLayout.setClipBounds(rect); + mTitleView = findViewById(R.id.title); + mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setAntiAlias(true); + } + + @RemotableViewMethod + public void setLargeIcon(Icon icon) { + mLargeIcon = icon; + } + + @RemotableViewMethod + public void setData(Bundle extras) { + Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); + List<Notification.MessagingStyle.Message> newMessages + = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); + Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); + List<Notification.MessagingStyle.Message> newHistoricMessages + = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); + mConversationTitle = null; + TextView headerText = findViewById(R.id.header_text); + if (headerText != null) { + mConversationTitle = headerText.getText(); + } + bind(newMessages, newHistoricMessages); + } + + private void bind(List<Notification.MessagingStyle.Message> newMessages, + List<Notification.MessagingStyle.Message> newHistoricMessages) { + + List<MessagingMessage> historicMessages = createMessages(newHistoricMessages, + true /* isHistoric */); + List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */); + addMessagesToGroups(historicMessages, messages); + + // Let's remove the remaining messages + mMessages.forEach(REMOVE_MESSAGE); + mHistoricMessages.forEach(REMOVE_MESSAGE); + + mMessages = messages; + mHistoricMessages = historicMessages; + + updateContractedMessage(); + updateHistoricMessageVisibility(); + updateTitleAndNamesDisplay(); + } + + private void updateTitleAndNamesDisplay() { + ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>(); + ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>(); + for (int i = 0; i < mGroups.size(); i++) { + MessagingGroup group = mGroups.get(i); + CharSequence senderName = group.getSenderName(); + if (TextUtils.isEmpty(senderName)) { + continue; + } + boolean visible = !mIsOneToOne; + group.setSenderVisible(visible); + if ((visible || mLargeIcon == null) && !uniqueNames.containsKey(senderName)) { + char c = senderName.charAt(0); + if (uniqueCharacters.containsKey(c)) { + // this character was already used, lets make it more unique. We first need to + // resolve the existing character if it exists + CharSequence existingName = uniqueCharacters.get(c); + if (existingName != null) { + uniqueNames.put(existingName, findNameSplit((String) existingName)); + uniqueCharacters.put(c, null); + } + uniqueNames.put(senderName, findNameSplit((String) senderName)); + } else { + uniqueNames.put(senderName, Character.toString(c)); + uniqueCharacters.put(c, senderName); + } + } + } + + // Now that we have the correct symbols, let's look what we have cached + ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>(); + for (int i = 0; i < mGroups.size(); i++) { + // Let's now set the avatars + MessagingGroup group = mGroups.get(i); + CharSequence senderName = group.getSenderName(); + if (TextUtils.isEmpty(senderName) || (mIsOneToOne && mLargeIcon != null)) { + continue; + } + String symbol = uniqueNames.get(senderName); + Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName, + symbol, mLayoutColor); + if (cachedIcon != null) { + cachedAvatars.put(senderName, cachedIcon); + } + } + + for (int i = 0; i < mGroups.size(); i++) { + // Let's now set the avatars + MessagingGroup group = mGroups.get(i); + CharSequence senderName = group.getSenderName(); + if (TextUtils.isEmpty(senderName)) { + continue; + } + if (mIsOneToOne && mLargeIcon != null) { + group.setAvatar(mLargeIcon); + } else { + Icon cachedIcon = cachedAvatars.get(senderName); + if (cachedIcon == null) { + cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName), + mLayoutColor); + cachedAvatars.put(senderName, cachedIcon); + } + group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName), + mLayoutColor); + } + } + } + + public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) { + Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + float radius = mAvatarSize / 2.0f; + int color = findColor(senderName, layoutColor); + mPaint.setColor(color); + canvas.drawCircle(radius, radius, radius, mPaint); + boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f; + mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE); + mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.75f : mAvatarSize * 0.4f); + int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)) ; + canvas.drawText(symbol, radius, yPos, mTextPaint); + return Icon.createWithBitmap(bitmap); + } + + private int findColor(CharSequence senderName, int layoutColor) { + double luminance = NotificationColorUtil.calculateLuminance(layoutColor); + float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f; + + // we need to offset the range if the luminance is too close to the borders + shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0); + shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0); + return NotificationColorUtil.getShiftedColor(layoutColor, + (int) (shift * COLOR_SHIFT_AMOUNT)); + } + + private String findNameSplit(String existingName) { + String[] split = existingName.split(" "); + if (split.length > 1) { + return Character.toString(split[0].charAt(0)) + + Character.toString(split[1].charAt(0)); + } + return existingName.substring(0, 1); + } + + @RemotableViewMethod + public void setLayoutColor(int color) { + mLayoutColor = color; + } + + @RemotableViewMethod + public void setIsOneToOne(boolean oneToOne) { + mIsOneToOne = oneToOne; + } + + private void addMessagesToGroups(List<MessagingMessage> historicMessages, + List<MessagingMessage> messages) { + // Let's first find our groups! + List<List<MessagingMessage>> groups = new ArrayList<>(); + List<CharSequence> senders = new ArrayList<>(); + + // Lets first find the groups + findGroups(historicMessages, messages, groups, senders); + + // Let's now create the views and reorder them accordingly + createGroupViews(groups, senders); + } + + private void createGroupViews(List<List<MessagingMessage>> groups, List<CharSequence> senders) { + mGroups.clear(); + for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { + List<MessagingMessage> group = groups.get(groupIndex); + MessagingGroup newGroup = null; + // we'll just take the first group that exists or create one there is none + for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) { + MessagingMessage message = group.get(messageIndex); + newGroup = message.getGroup(); + if (newGroup != null) { + break; + } + } + if (newGroup == null) { + newGroup = MessagingGroup.createGroup(mMessagingLinearLayout); + mAddedGroups.add(newGroup); + } + newGroup.setLayoutColor(mLayoutColor); + newGroup.setSender(senders.get(groupIndex)); + mGroups.add(newGroup); + + if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) { + mMessagingLinearLayout.removeView(newGroup); + mMessagingLinearLayout.addView(newGroup, groupIndex); + } + newGroup.setMessages(group); + } + } + + private void findGroups(List<MessagingMessage> historicMessages, + List<MessagingMessage> messages, List<List<MessagingMessage>> groups, + List<CharSequence> senders) { + CharSequence currentSender = null; + List<MessagingMessage> currentGroup = null; + int histSize = historicMessages.size(); + for (int i = 0; i < histSize + messages.size(); i++) { + MessagingMessage message; + if (i < histSize) { + message = historicMessages.get(i); + } else { + message = messages.get(i - histSize); + } + boolean isNewGroup = currentGroup == null; + CharSequence sender = message.getMessage().getSender(); + isNewGroup |= !TextUtils.equals(sender, currentSender); + if (isNewGroup) { + currentGroup = new ArrayList<>(); + groups.add(currentGroup); + senders.add(sender); + currentSender = sender; + } + currentGroup.add(message); + } + } + + private void updateContractedMessage() { + for (int i = mMessages.size() - 1; i >= 0; i--) { + MessagingMessage m = mMessages.get(i); + // Incoming messages have a non-empty sender. + if (!TextUtils.isEmpty(m.getMessage().getSender())) { + mContractedMessage = m; + return; + } + } + if (!mMessages.isEmpty()) { + // No incoming messages, fall back to outgoing message + mContractedMessage = mMessages.get(mMessages.size() - 1); + return; + } + mContractedMessage = null; + } + + /** + * Creates new messages, reusing existing ones if they are available. + * + * @param newMessages the messages to parse. + */ + private List<MessagingMessage> createMessages( + List<Notification.MessagingStyle.Message> newMessages, boolean historic) { + List<MessagingMessage> result = new ArrayList<>();; + for (int i = 0; i < newMessages.size(); i++) { + Notification.MessagingStyle.Message m = newMessages.get(i); + MessagingMessage message = findAndRemoveMatchingMessage(m); + if (message == null) { + message = MessagingMessage.createMessage(this, m); + message.addOnLayoutChangeListener(MESSAGING_PROPERTY_ANIMATOR); + } + message.setIsHistoric(historic); + result.add(message); + } + return result; + } + + private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) { + for (int i = 0; i < mMessages.size(); i++) { + MessagingMessage existing = mMessages.get(i); + if (existing.sameAs(m)) { + mMessages.remove(i); + return existing; + } + } + for (int i = 0; i < mHistoricMessages.size(); i++) { + MessagingMessage existing = mHistoricMessages.get(i); + if (existing.sameAs(m)) { + mHistoricMessages.remove(i); + return existing; + } + } + return null; + } + + public void showHistoricMessages(boolean show) { + mShowHistoricMessages = show; + updateHistoricMessageVisibility(); + } + + private void updateHistoricMessageVisibility() { + for (int i = 0; i < mHistoricMessages.size(); i++) { + MessagingMessage existing = mHistoricMessages.get(i); + existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (!mAddedGroups.isEmpty()) { + getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + for (MessagingGroup group : mAddedGroups) { + if (!group.isShown()) { + continue; + } + MessagingPropertyAnimator.fadeIn(group.getAvatar()); + MessagingPropertyAnimator.fadeIn(group.getSender()); + MessagingPropertyAnimator.startLocalTranslationFrom(group, + group.getHeight(), LINEAR_OUT_SLOW_IN); + } + mAddedGroups.clear(); + getViewTreeObserver().removeOnPreDrawListener(this); + return true; + } + }); + } + } + + public View getContractedMessage() { + return mContractedMessage; + } + + public MessagingLinearLayout getMessagingLinearLayout() { + return mMessagingLinearLayout; + } + + public ArrayList<MessagingGroup> getMessagingGroups() { + return mGroups; + } +} diff --git a/core/java/com/android/internal/widget/MessagingLinearLayout.java b/core/java/com/android/internal/widget/MessagingLinearLayout.java index 70473a014c0d..f0ef37076618 100644 --- a/core/java/com/android/internal/widget/MessagingLinearLayout.java +++ b/core/java/com/android/internal/widget/MessagingLinearLayout.java @@ -36,28 +36,14 @@ import com.android.internal.R; @RemoteViews.RemoteView public class MessagingLinearLayout extends ViewGroup { - private static final int NOT_MEASURED_BEFORE = -1; /** * Spacing to be applied between views. */ private int mSpacing; - /** - * The maximum height allowed. - */ - private int mMaxHeight; + private int mMaxDisplayedLines = Integer.MAX_VALUE; - private int mIndentLines; - - /** - * Id of the child that's also visible in the contracted layout. - */ - private int mContractedChildId; - /** - * The last measured with in a layout pass if it was measured before or - * {@link #NOT_MEASURED_BEFORE} if this is the first layout pass. - */ - private int mLastMeasuredWidth = NOT_MEASURED_BEFORE; + private MessagingLayout mMessagingLayout; public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); @@ -79,7 +65,6 @@ public class MessagingLinearLayout extends ViewGroup { a.recycle(); } - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // This is essentially a bottom-up linear layout that only adds children that fit entirely @@ -91,118 +76,67 @@ public class MessagingLinearLayout extends ViewGroup { break; } int widthSize = MeasureSpec.getSize(widthMeasureSpec); - boolean recalculateVisibility = mLastMeasuredWidth == NOT_MEASURED_BEFORE - || getMeasuredHeight() != targetHeight - || mLastMeasuredWidth != widthSize; - - final int count = getChildCount(); - if (recalculateVisibility) { - // We only need to recalculate the view visibilities if the view wasn't measured already - // in this pass, otherwise we may drop messages here already since we are measured - // exactly with what we returned before, which was optimized already with the - // line-indents. - for (int i = 0; i < count; ++i) { - final View child = getChildAt(i); - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - lp.hide = true; - } - - int totalHeight = mPaddingTop + mPaddingBottom; - boolean first = true; - - // Starting from the bottom: we measure every view as if it were the only one. If it still - - // fits, we take it, otherwise we stop there. - for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) { - if (getChildAt(i).getVisibility() == GONE) { - continue; - } - final View child = getChildAt(i); - LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); - ImageFloatingTextView textChild = null; - if (child instanceof ImageFloatingTextView) { - // Pretend we need the image padding for all views, we don't know which - // one will end up needing to do this (might end up not using all the space, - // but calculating this exactly would be more expensive). - textChild = (ImageFloatingTextView) child; - textChild.setNumIndentLines(mIndentLines == 2 ? 3 : mIndentLines); - } - - int spacing = first ? 0 : mSpacing; - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight - - mPaddingTop - mPaddingBottom + spacing); - - final int childHeight = child.getMeasuredHeight(); - int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin + - lp.bottomMargin + spacing); - first = false; - boolean measuredTooSmall = false; - if (textChild != null) { - measuredTooSmall = childHeight < textChild.getLayoutHeight() - + textChild.getPaddingTop() + textChild.getPaddingBottom(); - } - - if (newHeight <= targetHeight && !measuredTooSmall) { - totalHeight = newHeight; - lp.hide = false; - } else { - break; - } - } - } // Now that we know which views to take, fix up the indents and see what width we get. int measuredWidth = mPaddingLeft + mPaddingRight; - int imageLines = mIndentLines; - // Need to redo the height because it may change due to changing indents. - int totalHeight = mPaddingTop + mPaddingBottom; - boolean first = true; - for (int i = 0; i < count; i++) { + final int count = getChildCount(); + int totalHeight; + for (int i = 0; i < count; ++i) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.hide = true; + } + + totalHeight = mPaddingTop + mPaddingBottom; + boolean first = true; + int linesRemaining = mMaxDisplayedLines; - if (child.getVisibility() == GONE || lp.hide) { + // Starting from the bottom: we measure every view as if it were the only one. If it still + // fits, we take it, otherwise we stop there. + for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) { + if (getChildAt(i).getVisibility() == GONE) { continue; } - - if (child instanceof ImageFloatingTextView) { - ImageFloatingTextView textChild = (ImageFloatingTextView) child; - if (imageLines == 2 && textChild.getLineCount() > 2) { - // HACK: If we need indent for two lines, and they're coming from the same - // view, we need extra spacing to compensate for the lack of margins, - // so add an extra line of indent. - imageLines = 3; - } - boolean changed = textChild.setNumIndentLines(Math.max(0, imageLines)); - if (changed || !recalculateVisibility) { - final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, - mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, - lp.width); - // we want to measure it at most as high as it is currently, otherwise we'll - // drop later lines - final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, - targetHeight - child.getMeasuredHeight(), lp.height); - - child.measure(childWidthMeasureSpec, childHeightMeasureSpec);; - } - imageLines -= textChild.getLineCount(); + final View child = getChildAt(i); + LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + MessagingChild messagingChild = null; + if (child instanceof MessagingChild) { + messagingChild = (MessagingChild) child; + messagingChild.setMaxDisplayedLines(linesRemaining); } + int spacing = first ? 0 : mSpacing; + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight + - mPaddingTop - mPaddingBottom + spacing); - measuredWidth = Math.max(measuredWidth, - child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin - + mPaddingLeft + mPaddingRight); - totalHeight = Math.max(totalHeight, totalHeight + child.getMeasuredHeight() + - lp.topMargin + lp.bottomMargin + (first ? 0 : mSpacing)); + final int childHeight = child.getMeasuredHeight(); + int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin + + lp.bottomMargin + spacing); first = false; + int measureType = MessagingChild.MEASURED_NORMAL; + if (messagingChild != null) { + measureType = messagingChild.getMeasuredType(); + linesRemaining -= messagingChild.getConsumedLines(); + } + boolean isShortened = measureType == MessagingChild.MEASURED_SHORTENED; + boolean isTooSmall = measureType == MessagingChild.MEASURED_TOO_SMALL; + if (newHeight <= targetHeight && !isTooSmall) { + totalHeight = newHeight; + measuredWidth = Math.max(measuredWidth, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + + mPaddingLeft + mPaddingRight); + lp.hide = false; + if (isShortened || linesRemaining <= 0) { + break; + } + } else { + break; + } } - setMeasuredDimension( resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec), - resolveSize(Math.max(getSuggestedMinimumHeight(), totalHeight), - heightMeasureSpec)); - mLastMeasuredWidth = widthSize; + Math.max(getSuggestedMinimumHeight(), totalHeight)); } @Override @@ -221,13 +155,22 @@ public class MessagingLinearLayout extends ViewGroup { childTop = mPaddingTop; boolean first = true; - + final boolean shown = isShown(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - if (child.getVisibility() == GONE || lp.hide) { + MessagingChild messagingChild = (MessagingChild) child; + if (lp.hide) { + if (shown && lp.visibleBefore) { + messagingChild.hideAnimated(); + } + lp.visibleBefore = false; continue; + } else { + lp.visibleBefore = true; } final int childWidth = child.getMeasuredWidth(); @@ -251,14 +194,16 @@ public class MessagingLinearLayout extends ViewGroup { first = false; } - mLastMeasuredWidth = NOT_MEASURED_BEFORE; } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.hide) { - return true; + MessagingChild messagingChild = (MessagingChild) child; + if (!messagingChild.isHidingAnimated()) { + return true; + } } return super.drawChild(canvas, child, drawingTime); } @@ -284,31 +229,37 @@ public class MessagingLinearLayout extends ViewGroup { } /** - * Sets how many lines should be indented to avoid a floating image. + * Sets how many lines should be displayed at most */ @RemotableViewMethod - public void setNumIndentLines(int numberLines) { - mIndentLines = numberLines; + public void setMaxDisplayedLines(int numberLines) { + mMaxDisplayedLines = numberLines; } - /** - * Set id of the child that's also visible in the contracted layout. - */ - @RemotableViewMethod - public void setContractedChildId(int contractedChildId) { - mContractedChildId = contractedChildId; + public void setMessagingLayout(MessagingLayout layout) { + mMessagingLayout = layout; } - /** - * Get id of the child that's also visible in the contracted layout. - */ - public int getContractedChildId() { - return mContractedChildId; + public MessagingLayout getMessagingLayout() { + return mMessagingLayout; + } + + public interface MessagingChild { + int MEASURED_NORMAL = 0; + int MEASURED_SHORTENED = 1; + int MEASURED_TOO_SMALL = 2; + + int getMeasuredType(); + int getConsumedLines(); + void setMaxDisplayedLines(int lines); + void hideAnimated(); + boolean isHidingAnimated(); } public static class LayoutParams extends MarginLayoutParams { - boolean hide = false; + public boolean hide = false; + public boolean visibleBefore = false; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java new file mode 100644 index 000000000000..f09621f544bc --- /dev/null +++ b/core/java/com/android/internal/widget/MessagingMessage.java @@ -0,0 +1,197 @@ +/* + * 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 com.android.internal.widget; + +import android.annotation.AttrRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StyleRes; +import android.app.Notification; +import android.content.Context; +import android.text.Layout; +import android.util.AttributeSet; +import android.util.Pools; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.RemoteViews; + +import com.android.internal.R; + +import java.util.Objects; + +/** + * A message of a {@link MessagingLayout}. + */ +@RemoteViews.RemoteView +public class MessagingMessage extends ImageFloatingTextView implements + MessagingLinearLayout.MessagingChild { + + private static Pools.SimplePool<MessagingMessage> sInstancePool + = new Pools.SynchronizedPool<>(10); + private Notification.MessagingStyle.Message mMessage; + private MessagingGroup mGroup; + private boolean mIsHistoric; + private boolean mIsHidingAnimated; + + public MessagingMessage(@NonNull Context context) { + super(context); + } + + public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + private void setMessage(Notification.MessagingStyle.Message message) { + mMessage = message; + setText(message.getText()); + } + + public Notification.MessagingStyle.Message getMessage() { + return mMessage; + } + + boolean sameAs(Notification.MessagingStyle.Message message) { + if (!Objects.equals(message.getText(), mMessage.getText())) { + return false; + } + if (!Objects.equals(message.getSender(), mMessage.getSender())) { + return false; + } + if (!Objects.equals(message.getTimestamp(), mMessage.getTimestamp())) { + return false; + } + return true; + } + + boolean sameAs(MessagingMessage message) { + return sameAs(message.getMessage()); + } + + static MessagingMessage createMessage(MessagingLayout layout, + Notification.MessagingStyle.Message m) { + MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout(); + MessagingMessage createdMessage = sInstancePool.acquire(); + if (createdMessage == null) { + createdMessage = (MessagingMessage) LayoutInflater.from(layout.getContext()).inflate( + R.layout.notification_template_messaging_message, messagingLinearLayout, + false); + } + createdMessage.setMessage(m); + return createdMessage; + } + + public void removeMessage() { + mGroup.removeMessage(this); + } + + public void recycle() { + mGroup = null; + mMessage = null; + setAlpha(1.0f); + setTranslationY(0); + sInstancePool.release(this); + } + + public void setMessagingGroup(MessagingGroup group) { + mGroup = group; + } + + public static void dropCache() { + sInstancePool = new Pools.SynchronizedPool<>(10); + } + + public void setIsHistoric(boolean isHistoric) { + mIsHistoric = isHistoric; + } + + public MessagingGroup getGroup() { + return mGroup; + } + + @Override + public int getMeasuredType() { + boolean measuredTooSmall = getMeasuredHeight() + < getLayoutHeight() + getPaddingTop() + getPaddingBottom(); + if (measuredTooSmall) { + return MEASURED_TOO_SMALL; + } else { + Layout layout = getLayout(); + if (layout == null) { + return MEASURED_TOO_SMALL; + } + if (layout.getEllipsisCount(layout.getLineCount() - 1) > 0) { + return MEASURED_SHORTENED; + } else { + return MEASURED_NORMAL; + } + } + } + + @Override + public void hideAnimated() { + setIsHidingAnimated(true); + mGroup.performRemoveAnimation(this, () -> setIsHidingAnimated(false)); + } + + private void setIsHidingAnimated(boolean isHiding) { + ViewParent parent = getParent(); + mIsHidingAnimated = isHiding; + invalidate(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).invalidate(); + } + } + + @Override + public boolean isHidingAnimated() { + return mIsHidingAnimated; + } + + @Override + public void setMaxDisplayedLines(int lines) { + setMaxLines(lines); + } + + @Override + public int getConsumedLines() { + return getLineCount(); + } + + public int getLayoutHeight() { + Layout layout = getLayout(); + if (layout == null) { + return 0; + } + return layout.getHeight(); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/core/java/com/android/internal/widget/MessagingPropertyAnimator.java b/core/java/com/android/internal/widget/MessagingPropertyAnimator.java new file mode 100644 index 000000000000..7c3ab7f9547f --- /dev/null +++ b/core/java/com/android/internal/widget/MessagingPropertyAnimator.java @@ -0,0 +1,233 @@ +/* + * 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 com.android.internal.widget; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.util.IntProperty; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +import com.android.internal.R; + +/** + * A listener that automatically starts animations when the layout bounds change. + */ +public class MessagingPropertyAnimator implements View.OnLayoutChangeListener { + static final long APPEAR_ANIMATION_LENGTH = 210; + private static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); + public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f); + private static final int TAG_LOCAL_TRANSLATION_ANIMATOR = R.id.tag_local_translation_y_animator; + private static final int TAG_LOCAL_TRANSLATION_Y = R.id.tag_local_translation_y; + private static final int TAG_LAYOUT_TOP = R.id.tag_layout_top; + private static final int TAG_ALPHA_ANIMATOR = R.id.tag_alpha_animator; + private static final ViewClippingUtil.ClippingParameters CLIPPING_PARAMETERS = + view -> view.getId() == com.android.internal.R.id.notification_messaging; + private static final IntProperty<View> LOCAL_TRANSLATION_Y = + new IntProperty<View>("localTranslationY") { + @Override + public void setValue(View object, int value) { + setLocalTranslationY(object, value); + } + + @Override + public Integer get(View object) { + return getLocalTranslationY(object); + } + }; + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, + int oldTop, int oldRight, int oldBottom) { + int oldHeight = oldBottom - oldTop; + Integer layoutTop = (Integer) v.getTag(TAG_LAYOUT_TOP); + if (layoutTop != null) { + oldTop = layoutTop; + } + int topChange = oldTop - top; + if (oldHeight == 0 || topChange == 0 || !v.isShown() || isGone(v)) { + // First layout + return; + } + if (layoutTop != null) { + v.setTagInternal(TAG_LAYOUT_TOP, top); + } + int newHeight = bottom - top; + int heightDifference = oldHeight - newHeight; + // Only add the difference if the height changes and it's getting smaller + heightDifference = Math.max(heightDifference, 0); + startLocalTranslationFrom(v, topChange + heightDifference + getLocalTranslationY(v)); + } + + private boolean isGone(View view) { + if (view.getVisibility() == View.GONE) { + return true; + } + final ViewGroup.LayoutParams lp = view.getLayoutParams(); + if (lp instanceof MessagingLinearLayout.LayoutParams + && ((MessagingLinearLayout.LayoutParams) lp).hide) { + return true; + } + return false; + } + + public static void startLocalTranslationFrom(View v, int startTranslation) { + startLocalTranslationFrom(v, startTranslation, MessagingLayout.FAST_OUT_SLOW_IN); + } + + public static void startLocalTranslationFrom(View v, int startTranslation, + Interpolator interpolator) { + startLocalTranslation(v, startTranslation, 0, interpolator); + } + + public static void startLocalTranslationTo(View v, int endTranslation, + Interpolator interpolator) { + startLocalTranslation(v, getLocalTranslationY(v), endTranslation, interpolator); + } + + public static int getLocalTranslationY(View v) { + Integer tag = (Integer) v.getTag(TAG_LOCAL_TRANSLATION_Y); + if (tag == null) { + return 0; + } + return tag; + } + + private static void setLocalTranslationY(View v, int value) { + v.setTagInternal(TAG_LOCAL_TRANSLATION_Y, value); + updateTopAndBottom(v); + } + + private static void updateTopAndBottom(View v) { + int layoutTop = (int) v.getTag(TAG_LAYOUT_TOP); + int localTranslation = getLocalTranslationY(v); + int height = v.getHeight(); + v.setTop(layoutTop + localTranslation); + v.setBottom(layoutTop + height + localTranslation); + } + + private static void startLocalTranslation(final View v, int start, int end, + Interpolator interpolator) { + ObjectAnimator existing = (ObjectAnimator) v.getTag(TAG_LOCAL_TRANSLATION_ANIMATOR); + if (existing != null) { + existing.cancel(); + } + ObjectAnimator animator = ObjectAnimator.ofInt(v, LOCAL_TRANSLATION_Y, start, end); + Integer layoutTop = (Integer) v.getTag(TAG_LAYOUT_TOP); + if (layoutTop == null) { + layoutTop = v.getTop(); + v.setTagInternal(TAG_LAYOUT_TOP, layoutTop); + } + setLocalTranslationY(v, start); + animator.setInterpolator(interpolator); + animator.setDuration(APPEAR_ANIMATION_LENGTH); + animator.addListener(new AnimatorListenerAdapter() { + public boolean mCancelled; + + @Override + public void onAnimationEnd(Animator animation) { + v.setTagInternal(TAG_LOCAL_TRANSLATION_ANIMATOR, null); + setClippingDeactivated(v, false); + if (!mCancelled) { + setLocalTranslationY(v, 0); + v.setTagInternal(TAG_LAYOUT_TOP, null); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + }); + setClippingDeactivated(v, true); + v.setTagInternal(TAG_LOCAL_TRANSLATION_ANIMATOR, animator); + animator.start(); + } + + public static void fadeIn(final View v) { + ObjectAnimator existing = (ObjectAnimator) v.getTag(TAG_ALPHA_ANIMATOR); + if (existing != null) { + existing.cancel(); + } + if (v.getVisibility() == View.INVISIBLE) { + v.setVisibility(View.VISIBLE); + } + ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.ALPHA, + 0.0f, 1.0f); + v.setAlpha(0.0f); + animator.setInterpolator(ALPHA_IN); + animator.setDuration(APPEAR_ANIMATION_LENGTH); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + v.setTagInternal(TAG_ALPHA_ANIMATOR, null); + updateLayerType(v, false /* animating */); + } + }); + updateLayerType(v, true /* animating */); + v.setTagInternal(TAG_ALPHA_ANIMATOR, animator); + animator.start(); + } + + private static void updateLayerType(View view, boolean animating) { + if (view.hasOverlappingRendering() && animating) { + view.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } else if (view.getLayerType() == View.LAYER_TYPE_HARDWARE) { + view.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + public static void fadeOut(final View view, Runnable endAction) { + ObjectAnimator existing = (ObjectAnimator) view.getTag(TAG_ALPHA_ANIMATOR); + if (existing != null) { + existing.cancel(); + } + ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, + view.getAlpha(), 0.0f); + animator.setInterpolator(ALPHA_OUT); + animator.setDuration(APPEAR_ANIMATION_LENGTH); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setTagInternal(TAG_ALPHA_ANIMATOR, null); + updateLayerType(view, false /* animating */); + if (endAction != null) { + endAction.run(); + } + } + }); + updateLayerType(view, true /* animating */); + view.setTagInternal(TAG_ALPHA_ANIMATOR, animator); + animator.start(); + } + + public static void setClippingDeactivated(final View transformedView, boolean deactivated) { + ViewClippingUtil.setClippingDeactivated(transformedView, deactivated, + CLIPPING_PARAMETERS); + } + + public static boolean isAnimatingTranslation(View v) { + return v.getTag(TAG_LOCAL_TRANSLATION_ANIMATOR) != null; + } + + public static boolean isAnimatingAlpha(View v) { + return v.getTag(TAG_ALPHA_ANIMATOR) != null; + } +} diff --git a/core/java/com/android/internal/widget/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java index 073aac542e31..e013553ec046 100644 --- a/core/java/com/android/internal/widget/NotificationActionListLayout.java +++ b/core/java/com/android/internal/widget/NotificationActionListLayout.java @@ -16,8 +16,12 @@ package com.android.internal.widget; +import android.annotation.Nullable; import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; import android.util.Pair; import android.view.Gravity; @@ -37,6 +41,7 @@ import java.util.Comparator; @RemoteViews.RemoteView public class NotificationActionListLayout extends LinearLayout { + private final int mGravity; private int mTotalWidth = 0; private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>(); private ArrayList<View> mMeasureOrderOther = new ArrayList<>(); @@ -45,7 +50,20 @@ public class NotificationActionListLayout extends LinearLayout { private Drawable mDefaultBackground; public NotificationActionListLayout(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + int[] attrIds = { android.R.attr.gravity }; + TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes); + mGravity = ta.getInt(0, 0); + ta.recycle(); } @Override @@ -95,6 +113,7 @@ public class NotificationActionListLayout extends LinearLayout { final boolean constrained = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED; + final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0; final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight; final int otherSize = mMeasureOrderOther.size(); @@ -137,7 +156,7 @@ public class NotificationActionListLayout extends LinearLayout { // Make sure to measure the last child full-width if we didn't use up the entire width, // or we didn't measure yet because there's just one child. - if (lastNotGoneChild != null && (constrained && usedWidth < innerWidth + if (lastNotGoneChild != null && !centerAligned && (constrained && usedWidth < innerWidth || notGoneChildren == 1)) { MarginLayoutParams lp = (MarginLayoutParams) lastNotGoneChild.getLayoutParams(); if (notGoneChildren > 1) { @@ -185,6 +204,11 @@ public class NotificationActionListLayout extends LinearLayout { public void onViewAdded(View child) { super.onViewAdded(child); clearMeasureOrder(); + // For some reason ripples + notification actions seem to be an unhappy combination + // b/69474443 so just turn them off for now. + if (child.getBackground() instanceof RippleDrawable) { + ((RippleDrawable)child.getBackground()).setForceSoftware(true); + } } @Override @@ -201,9 +225,10 @@ public class NotificationActionListLayout extends LinearLayout { } final boolean isLayoutRtl = isLayoutRtl(); final int paddingTop = mPaddingTop; + final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0; int childTop; - int childLeft; + int childLeft = centerAligned ? left + (right - left) / 2 - mTotalWidth / 2 : 0; // Where bottom of child should go final int height = bottom - top; @@ -216,13 +241,12 @@ public class NotificationActionListLayout extends LinearLayout { final int layoutDirection = getLayoutDirection(); switch (Gravity.getAbsoluteGravity(Gravity.START, layoutDirection)) { case Gravity.RIGHT: - // mTotalWidth contains the padding already - childLeft = mPaddingLeft + right - left - mTotalWidth; + childLeft += mPaddingLeft + right - left - mTotalWidth; break; case Gravity.LEFT: default: - childLeft = mPaddingLeft; + childLeft += mPaddingLeft; break; } diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java index e53162cc97fd..5847033feb1e 100644 --- a/core/java/com/android/internal/widget/PointerLocationView.java +++ b/core/java/com/android/internal/widget/PointerLocationView.java @@ -32,7 +32,7 @@ import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; -import android.view.WindowManagerPolicy.PointerEventListener; +import android.view.WindowManagerPolicyConstants.PointerEventListener; import android.view.MotionEvent.PointerCoords; import java.util.ArrayList; diff --git a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java new file mode 100644 index 000000000000..e352b45ef413 --- /dev/null +++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java @@ -0,0 +1,68 @@ +/* + * 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 com.android.internal.widget; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RemoteViews; + +/** + * A LinearLayout that sets it's height again after the last measure pass. This is needed for + * MessagingLayouts where groups need to be able to snap it's height to. + */ +@RemoteViews.RemoteView +public class RemeasuringLinearLayout extends LinearLayout { + + public RemeasuringLinearLayout(Context context) { + super(context); + } + + public RemeasuringLinearLayout(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public RemeasuringLinearLayout(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RemeasuringLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int count = getChildCount(); + int height = 0; + for (int i = 0; i < count; ++i) { + final View child = getChildAt(i); + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + height = Math.max(height, height + child.getMeasuredHeight() + lp.topMargin + + lp.bottomMargin); + } + setMeasuredDimension(getMeasuredWidth(), height); + } +} diff --git a/core/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java index 17c7ebd39678..7635a727ae85 100644 --- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java +++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java @@ -81,6 +81,7 @@ public class ResolverDrawerLayout extends ViewGroup { private int mCollapsibleHeightReserved; private int mTopOffset; + private boolean mShowAtTop; private boolean mIsDragging; private boolean mOpenOnClick; @@ -134,6 +135,7 @@ public class ResolverDrawerLayout extends ViewGroup { mMaxCollapsedHeightSmall = a.getDimensionPixelSize( R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall, mMaxCollapsedHeight); + mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false); a.recycle(); mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material); @@ -162,6 +164,16 @@ public class ResolverDrawerLayout extends ViewGroup { return mCollapseOffset > 0; } + public void setShowAtTop(boolean showOnTop) { + mShowAtTop = showOnTop; + invalidate(); + requestLayout(); + } + + public boolean getShowAtTop() { + return mShowAtTop; + } + public void setCollapsed(boolean collapsed) { if (!isLaidOut()) { mOpenOnLayout = collapsed; @@ -206,6 +218,12 @@ public class ResolverDrawerLayout extends ViewGroup { return false; } + if (getShowAtTop()) { + // Keep the drawer fully open. + mCollapseOffset = 0; + return false; + } + if (isLaidOut()) { final boolean isCollapsedOld = mCollapseOffset != 0; if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight @@ -372,14 +390,23 @@ public class ResolverDrawerLayout extends ViewGroup { mVelocityTracker.computeCurrentVelocity(1000); final float yvel = mVelocityTracker.getYVelocity(mActivePointerId); if (Math.abs(yvel) > mMinFlingVelocity) { - if (isDismissable() - && yvel > 0 && mCollapseOffset > mCollapsibleHeight) { - smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel); - mDismissOnScrollerFinished = true; + if (getShowAtTop()) { + if (isDismissable() && yvel < 0) { + abortAnimation(); + dismiss(); + } else { + smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); + } } else { - smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); + if (isDismissable() + && yvel > 0 && mCollapseOffset > mCollapsibleHeight) { + smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel); + mDismissOnScrollerFinished = true; + } else { + smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); + } } - } else { + }else { smoothScrollTo( mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); } @@ -421,6 +448,11 @@ public class ResolverDrawerLayout extends ViewGroup { mVelocityTracker.clear(); } + private void dismiss() { + mRunOnDismissedListener = new RunOnDismissedListener(); + post(mRunOnDismissedListener); + } + @Override public void computeScroll() { super.computeScroll(); @@ -430,8 +462,7 @@ public class ResolverDrawerLayout extends ViewGroup { if (keepGoing) { postInvalidateOnAnimation(); } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) { - mRunOnDismissedListener = new RunOnDismissedListener(); - post(mRunOnDismissedListener); + dismiss(); } } } @@ -443,6 +474,10 @@ public class ResolverDrawerLayout extends ViewGroup { } private float performDrag(float dy) { + if (getShowAtTop()) { + return 0; + } + final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mCollapsibleHeight + mUncollapsibleHeight)); if (newPos != mCollapseOffset) { @@ -656,7 +691,7 @@ public class ResolverDrawerLayout extends ViewGroup { @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { - if (velocityY > mMinFlingVelocity && mCollapseOffset != 0) { + if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) { smoothScrollTo(0, velocityY); return true; } @@ -666,12 +701,21 @@ public class ResolverDrawerLayout extends ViewGroup { @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) { - if (isDismissable() - && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) { - smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY); - mDismissOnScrollerFinished = true; + if (getShowAtTop()) { + if (isDismissable() && velocityY > 0) { + abortAnimation(); + dismiss(); + } else { + smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY); + } } else { - smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY); + if (isDismissable() + && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) { + smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY); + mDismissOnScrollerFinished = true; + } else { + smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY); + } } return true; } @@ -794,7 +838,11 @@ public class ResolverDrawerLayout extends ViewGroup { updateCollapseOffset(oldCollapsibleHeight, !isDragging()); - mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset; + if (getShowAtTop()) { + mTopOffset = 0; + } else { + mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset; + } setMeasuredDimension(sourceWidth, heightSize); } diff --git a/core/java/com/android/internal/widget/SubtitleView.java b/core/java/com/android/internal/widget/SubtitleView.java index 3230185dbda7..110782896d5a 100644 --- a/core/java/com/android/internal/widget/SubtitleView.java +++ b/core/java/com/android/internal/widget/SubtitleView.java @@ -256,8 +256,11 @@ public class SubtitleView extends View { // StaticLayout.getWidth(), so this is non-trivial. mHasMeasurements = true; mLastMeasuredWidth = maxWidth; - mLayout = new StaticLayout( - mText, mTextPaint, maxWidth, mAlignment, mSpacingMult, mSpacingAdd, true); + mLayout = StaticLayout.Builder.obtain(mText, 0, mText.length(), mTextPaint, maxWidth) + .setAlignment(mAlignment) + .setLineSpacing(mSpacingAdd, mSpacingMult) + .setUseLineSpacingFromFallbacks(true) + .build(); return true; } diff --git a/core/java/com/android/internal/widget/ViewClippingUtil.java b/core/java/com/android/internal/widget/ViewClippingUtil.java new file mode 100644 index 000000000000..59bbed443193 --- /dev/null +++ b/core/java/com/android/internal/widget/ViewClippingUtil.java @@ -0,0 +1,108 @@ +/* + * 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 com.android.internal.widget; + +import android.util.ArraySet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.android.internal.R; + +/** + * A utility class that allows to clip views and their parents to allow for better transitions + */ +public class ViewClippingUtil { + private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag; + private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag; + private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag; + + public static void setClippingDeactivated(final View transformedView, boolean deactivated, + ClippingParameters clippingParameters) { + if (!deactivated && !clippingParameters.isClippingEnablingAllowed(transformedView)) { + return; + } + if (!(transformedView.getParent() instanceof ViewGroup)) { + return; + } + ViewGroup parent = (ViewGroup) transformedView.getParent(); + while (true) { + if (!deactivated && !clippingParameters.isClippingEnablingAllowed(transformedView)) { + return; + } + ArraySet<View> clipSet = (ArraySet<View>) parent.getTag(CLIP_CLIPPING_SET); + if (clipSet == null) { + clipSet = new ArraySet<>(); + parent.setTagInternal(CLIP_CLIPPING_SET, clipSet); + } + Boolean clipChildren = (Boolean) parent.getTag(CLIP_CHILDREN_TAG); + if (clipChildren == null) { + clipChildren = parent.getClipChildren(); + parent.setTagInternal(CLIP_CHILDREN_TAG, clipChildren); + } + Boolean clipToPadding = (Boolean) parent.getTag(CLIP_TO_PADDING); + if (clipToPadding == null) { + clipToPadding = parent.getClipToPadding(); + parent.setTagInternal(CLIP_TO_PADDING, clipToPadding); + } + if (!deactivated) { + clipSet.remove(transformedView); + if (clipSet.isEmpty()) { + parent.setClipChildren(clipChildren); + parent.setClipToPadding(clipToPadding); + parent.setTagInternal(CLIP_CLIPPING_SET, null); + clippingParameters.onClippingStateChanged(parent, true); + } + } else { + clipSet.add(transformedView); + parent.setClipChildren(false); + parent.setClipToPadding(false); + clippingParameters.onClippingStateChanged(parent, false); + } + if (clippingParameters.shouldFinish(parent)) { + return; + } + final ViewParent viewParent = parent.getParent(); + if (viewParent instanceof ViewGroup) { + parent = (ViewGroup) viewParent; + } else { + return; + } + } + } + + public interface ClippingParameters { + /** + * Should we stop clipping at this view? If true is returned, {@param view} is the last view + * where clipping is activated / deactivated. + */ + boolean shouldFinish(View view); + + /** + * Is it allowed to enable clipping on this view. + */ + default boolean isClippingEnablingAllowed(View view) { + return !MessagingPropertyAnimator.isAnimatingTranslation(view); + } + + /** + * A method that is called whenever the view starts clipping again / stops clipping to the + * children and padding. + */ + default void onClippingStateChanged(View view, boolean isClipping) {}; + } +} diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java index 77788921635f..b7a67192f01f 100644 --- a/core/java/com/android/server/SystemConfig.java +++ b/core/java/com/android/server/SystemConfig.java @@ -44,7 +44,9 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; /** * Loads global system configuration info. @@ -60,6 +62,7 @@ public class SystemConfig { private static final int ALLOW_PERMISSIONS = 0x04; private static final int ALLOW_APP_CONFIGS = 0x08; private static final int ALLOW_PRIVAPP_PERMISSIONS = 0x10; + private static final int ALLOW_OEM_PERMISSIONS = 0x20; private static final int ALLOW_ALL = ~0; // Group-ids that are given to all packages as read from etc/permissions/*.xml. @@ -143,6 +146,11 @@ public class SystemConfig { final ArrayMap<String, ArraySet<String>> mPrivAppPermissions = new ArrayMap<>(); final ArrayMap<String, ArraySet<String>> mPrivAppDenyPermissions = new ArrayMap<>(); + final ArrayMap<String, ArraySet<String>> mVendorPrivAppPermissions = new ArrayMap<>(); + final ArrayMap<String, ArraySet<String>> mVendorPrivAppDenyPermissions = new ArrayMap<>(); + + final ArrayMap<String, ArrayMap<String, Boolean>> mOemPermissions = new ArrayMap<>(); + public static SystemConfig getInstance() { synchronized (SystemConfig.class) { if (sInstance == null) { @@ -224,31 +232,52 @@ public class SystemConfig { return mPrivAppDenyPermissions.get(packageName); } + public ArraySet<String> getVendorPrivAppPermissions(String packageName) { + return mVendorPrivAppPermissions.get(packageName); + } + + public ArraySet<String> getVendorPrivAppDenyPermissions(String packageName) { + return mVendorPrivAppDenyPermissions.get(packageName); + } + + public Map<String, Boolean> getOemPermissions(String packageName) { + final Map<String, Boolean> oemPermissions = mOemPermissions.get(packageName); + if (oemPermissions != null) { + return oemPermissions; + } + return Collections.emptyMap(); + } + SystemConfig() { // Read configuration from system readPermissions(Environment.buildPath( Environment.getRootDirectory(), "etc", "sysconfig"), ALLOW_ALL); + // Read configuration from the old permissions dir readPermissions(Environment.buildPath( Environment.getRootDirectory(), "etc", "permissions"), ALLOW_ALL); + // Allow Vendor to customize system configs around libs, features, permissions and apps int vendorPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PERMISSIONS | - ALLOW_APP_CONFIGS; + ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS; readPermissions(Environment.buildPath( Environment.getVendorDirectory(), "etc", "sysconfig"), vendorPermissionFlag); readPermissions(Environment.buildPath( Environment.getVendorDirectory(), "etc", "permissions"), vendorPermissionFlag); + // Allow ODM to customize system configs around libs, features and apps int odmPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_APP_CONFIGS; readPermissions(Environment.buildPath( Environment.getOdmDirectory(), "etc", "sysconfig"), odmPermissionFlag); readPermissions(Environment.buildPath( Environment.getOdmDirectory(), "etc", "permissions"), odmPermissionFlag); - // Only allow OEM to customize features + + // Allow OEM to customize features and OEM permissions + int oemPermissionFlag = ALLOW_FEATURES | ALLOW_OEM_PERMISSIONS; readPermissions(Environment.buildPath( - Environment.getOemDirectory(), "etc", "sysconfig"), ALLOW_FEATURES); + Environment.getOemDirectory(), "etc", "sysconfig"), oemPermissionFlag); readPermissions(Environment.buildPath( - Environment.getOemDirectory(), "etc", "permissions"), ALLOW_FEATURES); + Environment.getOemDirectory(), "etc", "permissions"), oemPermissionFlag); } void readPermissions(File libraryDir, int permissionFlag) { @@ -327,6 +356,7 @@ public class SystemConfig { boolean allowPermissions = (permissionFlag & ALLOW_PERMISSIONS) != 0; boolean allowAppConfigs = (permissionFlag & ALLOW_APP_CONFIGS) != 0; boolean allowPrivappPermissions = (permissionFlag & ALLOW_PRIVAPP_PERMISSIONS) != 0; + boolean allowOemPermissions = (permissionFlag & ALLOW_OEM_PERMISSIONS) != 0; while (true) { XmlUtils.nextElement(parser); if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { @@ -568,7 +598,21 @@ public class SystemConfig { } XmlUtils.skipCurrentTag(parser); } else if ("privapp-permissions".equals(name) && allowPrivappPermissions) { - readPrivAppPermissions(parser); + // privapp permissions from system and vendor partitions are stored + // separately. This is to prevent xml files in the vendor partition from + // granting permissions to priv apps in the system partition and vice + // versa. + boolean vendor = permFile.toPath().startsWith( + Environment.getVendorDirectory().toPath()); + if (vendor) { + readPrivAppPermissions(parser, mVendorPrivAppPermissions, + mVendorPrivAppDenyPermissions); + } else { + readPrivAppPermissions(parser, mPrivAppPermissions, + mPrivAppDenyPermissions); + } + } else if ("oem-permissions".equals(name) && allowOemPermissions) { + readOemPermissions(parser); } else { XmlUtils.skipCurrentTag(parser); continue; @@ -653,7 +697,10 @@ public class SystemConfig { } } - void readPrivAppPermissions(XmlPullParser parser) throws IOException, XmlPullParserException { + private void readPrivAppPermissions(XmlPullParser parser, + ArrayMap<String, ArraySet<String>> grantMap, + ArrayMap<String, ArraySet<String>> denyMap) + throws IOException, XmlPullParserException { String packageName = parser.getAttributeValue(null, "package"); if (TextUtils.isEmpty(packageName)) { Slog.w(TAG, "package is required for <privapp-permissions> in " @@ -661,11 +708,11 @@ public class SystemConfig { return; } - ArraySet<String> permissions = mPrivAppPermissions.get(packageName); + ArraySet<String> permissions = grantMap.get(packageName); if (permissions == null) { permissions = new ArraySet<>(); } - ArraySet<String> denyPermissions = mPrivAppDenyPermissions.get(packageName); + ArraySet<String> denyPermissions = denyMap.get(packageName); int depth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, depth)) { String name = parser.getName(); @@ -690,9 +737,45 @@ public class SystemConfig { denyPermissions.add(permName); } } - mPrivAppPermissions.put(packageName, permissions); + grantMap.put(packageName, permissions); if (denyPermissions != null) { - mPrivAppDenyPermissions.put(packageName, denyPermissions); + denyMap.put(packageName, denyPermissions); + } + } + + void readOemPermissions(XmlPullParser parser) throws IOException, XmlPullParserException { + final String packageName = parser.getAttributeValue(null, "package"); + if (TextUtils.isEmpty(packageName)) { + Slog.w(TAG, "package is required for <oem-permissions> in " + + parser.getPositionDescription()); + return; + } + + ArrayMap<String, Boolean> permissions = mOemPermissions.get(packageName); + if (permissions == null) { + permissions = new ArrayMap<>(); + } + final int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + final String name = parser.getName(); + if ("permission".equals(name)) { + final String permName = parser.getAttributeValue(null, "name"); + if (TextUtils.isEmpty(permName)) { + Slog.w(TAG, "name is required for <permission> in " + + parser.getPositionDescription()); + continue; + } + permissions.put(permName, Boolean.TRUE); + } else if ("deny-permission".equals(name)) { + String permName = parser.getAttributeValue(null, "name"); + if (TextUtils.isEmpty(permName)) { + Slog.w(TAG, "name is required for <deny-permission> in " + + parser.getPositionDescription()); + continue; + } + permissions.put(permName, Boolean.FALSE); + } } + mOemPermissions.put(packageName, permissions); } } diff --git a/core/java/org/chromium/arc/EventLogTags.logtags b/core/java/org/chromium/arc/EventLogTags.logtags new file mode 100644 index 000000000000..1b7160e90224 --- /dev/null +++ b/core/java/org/chromium/arc/EventLogTags.logtags @@ -0,0 +1,11 @@ +# See system/core/logcat/event.logtags for a description of the format of this file. + +option java_package org.chromium.arc + +# We use ID range 300000-399999 for ARC. +# In case of conflicts build will fail, so we do not need to worry too much +# about it. + +# Emitted in ARC system events, such as start of ARC services. +# These events will be watched in automations like autotests. +300000 arc_system_event (event|3) |
