diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-10-24 19:50:40 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-10-24 19:50:40 -0400 |
commit | 47ed54e5d312f899507d28d6e95ccc18a0de19fe (patch) | |
tree | 7a2d435c55c36fbc1d07e895bd0c68b18f84e12c /android | |
parent | 07f9f65561c2b81bcd189b895b31bb2ad0438d74 (diff) | |
download | android-28-47ed54e5d312f899507d28d6e95ccc18a0de19fe.tar.gz |
Import Android SDK Platform P [4413397]
/google/data/ro/projects/android/fetch_artifact \
--bid 4413397 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4413397.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I3cf1f7c36e61c090dcc7de7bcfa812ef2bf96c00
Diffstat (limited to 'android')
314 files changed, 15080 insertions, 14189 deletions
diff --git a/android/accessibilityservice/GestureDescription.java b/android/accessibilityservice/GestureDescription.java index 92567d75..56f4ae2b 100644 --- a/android/accessibilityservice/GestureDescription.java +++ b/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/android/app/Activity.java b/android/app/Activity.java index e0ac9113..85f73bb7 100644 --- a/android/app/Activity.java +++ b/android/app/Activity.java @@ -542,9 +542,9 @@ import java.util.List; * <ul> * <li> <p>When creating a new document, the backing database entry or file for * it is created immediately. For example, if the user chooses to write - * a new e-mail, a new entry for that e-mail is created as soon as they + * a new email, a new entry for that email is created as soon as they * start entering data, so that if they go to any other activity after - * that point this e-mail will now appear in the list of drafts.</p> + * that point this email will now appear in the list of drafts.</p> * <li> <p>When an activity's <code>onPause()</code> method is called, it should * commit to the backing content provider or file any changes the user * has made. This ensures that those changes will be seen by any other @@ -1879,7 +1879,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 @@ -6259,6 +6259,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"); } } diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java index 5e61727f..fc4c8d7f 100644 --- a/android/app/ActivityManager.java +++ b/android/app/ActivityManager.java @@ -16,14 +16,8 @@ package android.app; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - import android.Manifest; +import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -670,138 +664,6 @@ public class ActivityManager { /** Invalid stack ID. */ public static final int INVALID_STACK_ID = -1; - /** First static stack ID. - * @hide */ - private static final int FIRST_STATIC_STACK_ID = 0; - - /** ID of stack where fullscreen activities are normally launched into. - * @hide */ - public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1; - - /** ID of stack where freeform/resized activities are normally launched into. - * @hide */ - public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1; - - /** ID of stack that occupies a dedicated region of the screen. - * @hide */ - public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1; - - /** ID of stack that always on top (always visible) when it exist. - * @hide */ - public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1; - - /** Last static stack stack ID. - * @hide */ - private static final int LAST_STATIC_STACK_ID = PINNED_STACK_ID; - - /** Start of ID range used by stacks that are created dynamically. - * @hide */ - public static final int FIRST_DYNAMIC_STACK_ID = LAST_STATIC_STACK_ID + 1; - - // TODO: Figure-out a way to remove this. - /** @hide */ - public static boolean isStaticStack(int stackId) { - return stackId >= FIRST_STATIC_STACK_ID && stackId <= LAST_STATIC_STACK_ID; - } - - // TODO: It seems this mostly means a stack on a secondary display now. Need to see if - // there are other meanings. If not why not just use information from the display? - /** @hide */ - public static boolean isDynamicStack(int stackId) { - return stackId >= FIRST_DYNAMIC_STACK_ID; - } - - /** - * Returns true if we try to maintain focus in the current stack when the top activity - * finishes. - * @hide - */ - // TODO: Figure-out a way to remove. Probably isn't needed in the new world... - public static boolean keepFocusInStackIfPossible(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID - || stackId == DOCKED_STACK_ID || stackId == PINNED_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. - * @hide - */ - public static boolean replaceWindowsOnTaskMove(int sourceStackId, int targetStackId) { - return sourceStackId == FREEFORM_WORKSPACE_STACK_ID - || targetStackId == FREEFORM_WORKSPACE_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. - * @hide - */ - 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. - * @hide - */ - public static boolean resizeStackWithLaunchBounds(int stackId) { - return 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. - * @hide - */ - // TODO: What about the other side of docked stack if we move this to WindowConfiguration? - public static boolean normallyFullscreenWindows(int stackId) { - return stackId != PINNED_STACK_ID && stackId != FREEFORM_WORKSPACE_STACK_ID - && stackId != DOCKED_STACK_ID; - } - - /** Returns the stack id for the input windowing mode. - * @hide */ - // TODO: To be removed once we are not using stack id for stuff... - public static int getStackIdForWindowingMode(int windowingMode) { - switch (windowingMode) { - case WINDOWING_MODE_PINNED: return PINNED_STACK_ID; - case WINDOWING_MODE_FREEFORM: return FREEFORM_WORKSPACE_STACK_ID; - case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY: return DOCKED_STACK_ID; - case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY: return FULLSCREEN_WORKSPACE_STACK_ID; - case WINDOWING_MODE_FULLSCREEN: return FULLSCREEN_WORKSPACE_STACK_ID; - default: return INVALID_STACK_ID; - } - } - - /** Returns the windowing mode that should be used for this input stack id. - * @hide */ - // TODO: To be removed once we are not using stack id for stuff... - public static int getWindowingModeForStackId(int stackId, boolean inSplitScreenMode) { - final int windowingMode; - switch (stackId) { - case FULLSCREEN_WORKSPACE_STACK_ID: - windowingMode = inSplitScreenMode - ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY : WINDOWING_MODE_FULLSCREEN; - break; - case PINNED_STACK_ID: - windowingMode = WINDOWING_MODE_PINNED; - break; - case DOCKED_STACK_ID: - windowingMode = WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; - break; - case FREEFORM_WORKSPACE_STACK_ID: - windowingMode = WINDOWING_MODE_FREEFORM; - break; - default : - windowingMode = WINDOWING_MODE_UNDEFINED; - } - return windowingMode; - } } /** @@ -1080,11 +942,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; @@ -1098,9 +963,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"); } @@ -1111,9 +994,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); } /** @@ -1122,21 +1018,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; @@ -1158,6 +1055,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; @@ -1173,6 +1071,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) { @@ -1245,6 +1144,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 @@ -1272,6 +1179,13 @@ public class ActivityManager { } /** @hide */ + @TestApi + public int getIconResource() { + return mIconRes; + } + + /** @hide */ + @TestApi public String getIconFilename() { return mIconFilename; } @@ -1337,7 +1251,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)); } } @@ -1349,8 +1266,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)); } } @@ -1373,6 +1292,7 @@ public class ActivityManager { dest.writeInt(1); mIcon.writeToParcel(dest, 0); } + dest.writeInt(mIconRes); dest.writeInt(mColorPrimary); dest.writeInt(mColorBackground); dest.writeInt(mStatusBarColor); @@ -1388,6 +1308,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(); @@ -1408,8 +1329,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; } @@ -1571,7 +1492,6 @@ public class ActivityManager { } dest.writeInt(stackId); dest.writeInt(userId); - dest.writeLong(firstActiveTime); dest.writeLong(lastActiveTime); dest.writeInt(affiliatedTaskId); dest.writeInt(affiliatedTaskColor); @@ -1600,7 +1520,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(); @@ -1643,31 +1562,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. * @@ -1702,33 +1596,7 @@ 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(); + return getService().getRecentTasks(maxNum, flags, UserHandle.myUserId()).getList(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2021,22 +1889,6 @@ public class ActivityManager { } /** - * 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. - * - * @hide - */ - public boolean removeTask(int taskId) throws SecurityException { - try { - return getService().removeTask(taskId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** * 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. diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java index a68c3a5c..b62e4c2d 100644 --- a/android/app/ActivityOptions.java +++ b/android/app/ActivityOptions.java @@ -17,13 +17,13 @@ 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.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; @@ -159,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 @@ -279,6 +285,7 @@ public class ActivityOptions { private int mResultCode; private int mExitCoordinatorIndex; private PendingIntent mUsageTimeReport; + private boolean mLockTaskMode = false; private int mLaunchDisplayId = INVALID_DISPLAY; @WindowConfiguration.WindowingMode private int mLaunchWindowingMode = WINDOWING_MODE_UNDEFINED; @@ -870,6 +877,7 @@ 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); mLaunchWindowingMode = opts.getInt(KEY_LAUNCH_WINDOWING_MODE, WINDOWING_MODE_UNDEFINED); mLaunchActivityType = opts.getInt(KEY_LAUNCH_ACTIVITY_TYPE, ACTIVITY_TYPE_UNDEFINED); @@ -1056,6 +1064,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. @@ -1248,6 +1287,7 @@ public class ActivityOptions { mExitCoordinatorIndex = otherOptions.mExitCoordinatorIndex; break; } + mLockTaskMode = otherOptions.mLockTaskMode; mAnimSpecs = otherOptions.mAnimSpecs; mAnimationFinishedListener = otherOptions.mAnimationFinishedListener; mSpecsFuture = otherOptions.mSpecsFuture; @@ -1322,6 +1362,7 @@ 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_WINDOWING_MODE, mLaunchWindowingMode); b.putInt(KEY_LAUNCH_ACTIVITY_TYPE, mLaunchActivityType); diff --git a/android/app/KeyguardManager.java b/android/app/KeyguardManager.java index 54f74b15..1fe29004 100644 --- a/android/app/KeyguardManager.java +++ b/android/app/KeyguardManager.java @@ -387,8 +387,6 @@ public class KeyguardManager { * 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 { diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java index 47063f08..c06ad3f3 100644 --- a/android/app/NotificationChannel.java +++ b/android/app/NotificationChannel.java @@ -32,6 +32,8 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.util.Preconditions; +import com.android.internal.util.Preconditions; + import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java index eb52cb7f..a52dc1e4 100644 --- a/android/app/NotificationManager.java +++ b/android/app/NotificationManager.java @@ -934,8 +934,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, @@ -1135,6 +1141,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/android/app/StatusBarManager.java b/android/app/StatusBarManager.java index 8987bc02..23c4166d 100644 --- a/android/app/StatusBarManager.java +++ b/android/app/StatusBarManager.java @@ -73,15 +73,16 @@ public class StatusBarManager { 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 | DISABLE2_SYSTEM_ICONS - | DISABLE2_NOTIFICATION_SHADE; + | DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS; @IntDef(flag = true, value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS, DISABLE2_SYSTEM_ICONS, - DISABLE2_NOTIFICATION_SHADE}) + DISABLE2_NOTIFICATION_SHADE, DISABLE2_GLOBAL_ACTIONS}) @Retention(RetentionPolicy.SOURCE) public @interface Disable2Flags {} diff --git a/android/app/TaskStackListener.java b/android/app/TaskStackListener.java index 402e2095..895d12a7 100644 --- a/android/app/TaskStackListener.java +++ b/android/app/TaskStackListener.java @@ -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/android/app/WallpaperManager.java b/android/app/WallpaperManager.java index 942cc995..081bd814 100644 --- a/android/app/WallpaperManager.java +++ b/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/android/app/WindowConfiguration.java b/android/app/WindowConfiguration.java index 6b405384..251863ca 100644 --- a/android/app/WindowConfiguration.java +++ b/android/app/WindowConfiguration.java @@ -511,7 +511,8 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu return windowingMode != WINDOWING_MODE_FREEFORM && windowingMode != WINDOWING_MODE_PINNED; } - private static String windowingModeToString(@WindowingMode int windowingMode) { + /** @hide */ + public static String windowingModeToString(@WindowingMode int windowingMode) { switch (windowingMode) { case WINDOWING_MODE_UNDEFINED: return "undefined"; case WINDOWING_MODE_FULLSCREEN: return "fullscreen"; diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java index 3c530633..ab8edee7 100644 --- a/android/app/admin/DevicePolicyManager.java +++ b/android/app/admin/DevicePolicyManager.java @@ -6533,6 +6533,52 @@ public class DevicePolicyManager { } /** + * 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. diff --git a/android/app/assist/AssistStructure.java b/android/app/assist/AssistStructure.java index d9b7cd7e..e491a4f9 100644 --- a/android/app/assist/AssistStructure.java +++ b/android/app/assist/AssistStructure.java @@ -616,6 +616,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. @@ -713,6 +716,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(); @@ -876,6 +882,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); @@ -1444,6 +1453,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; + } } /** @@ -1776,6 +1818,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; } diff --git a/android/app/job/JobScheduler.java b/android/app/job/JobScheduler.java index 3868439f..0deb2e13 100644 --- a/android/app/job/JobScheduler.java +++ b/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/android/app/job/JobService.java b/android/app/job/JobService.java index 9096b47b..69afed20 100644 --- a/android/app/job/JobService.java +++ b/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,36 @@ 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. + * 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 +114,44 @@ 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> + * 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> + * Once this method returns, the system releases the wakelock that it is holding on + * behalf of the job.</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. + * @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 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. + * 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 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. + * 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 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. + * @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 needsReschedule) { - mEngine.jobFinished(params, needsReschedule); + public final void jobFinished(JobParameters params, boolean wantsReschedule) { + mEngine.jobFinished(params, wantsReschedule); } -}
\ No newline at end of file +} diff --git a/android/slice/Slice.java b/android/app/slice/Slice.java index 57686548..7f9f74b4 100644 --- a/android/slice/Slice.java +++ b/android/app/slice/Slice.java @@ -14,38 +14,35 @@ * limitations under the License. */ -package android.slice; - -import static android.slice.SliceItem.TYPE_ACTION; -import static android.slice.SliceItem.TYPE_COLOR; -import static android.slice.SliceItem.TYPE_IMAGE; -import static android.slice.SliceItem.TYPE_REMOTE_INPUT; -import static android.slice.SliceItem.TYPE_REMOTE_VIEW; -import static android.slice.SliceItem.TYPE_SLICE; -import static android.slice.SliceItem.TYPE_TEXT; -import static android.slice.SliceItem.TYPE_TIMESTAMP; +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.ContentResolver; +import android.content.IContentProvider; 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 android.widget.RemoteViews; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; /** * 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. - * @hide */ public final class Slice implements Parcelable { @@ -53,7 +50,7 @@ public final class Slice implements Parcelable { * @hide */ @StringDef({HINT_TITLE, HINT_LIST, HINT_LIST_ITEM, HINT_LARGE, HINT_ACTIONS, HINT_SELECTED, - HINT_SOURCE, HINT_MESSAGE, HINT_HORIZONTAL, HINT_NO_TINT}) + HINT_SOURCE, HINT_MESSAGE, HINT_HORIZONTAL, HINT_NO_TINT, HINT_PARTIAL}) public @interface SliceHint{ } /** @@ -102,6 +99,12 @@ public final class Slice implements Parcelable { * Hint to indicate that this content should not be tinted. */ public static final String HINT_NO_TINT = "no_tint"; + /** + * 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"; // These two are coming over from prototyping, but we probably don't want in // public API, at least not right now. @@ -109,19 +112,12 @@ public final class Slice implements Parcelable { * @hide */ public static final String HINT_ALT = "alt"; - /** - * @hide - */ - public static final String HINT_PARTIAL = "partial"; private final SliceItem[] mItems; private final @SliceHint String[] mHints; private Uri mUri; - /** - * @hide - */ - public Slice(ArrayList<SliceItem> items, @SliceHint String[] hints, Uri uri) { + Slice(ArrayList<SliceItem> items, @SliceHint String[] hints, Uri uri) { mHints = hints; mItems = items.toArray(new SliceItem[items.size()]); mUri = uri; @@ -147,15 +143,15 @@ public final class Slice implements Parcelable { /** * @return All child {@link SliceItem}s that this Slice contains. */ - public SliceItem[] getItems() { - return mItems; + public List<SliceItem> getItems() { + return Arrays.asList(mItems); } /** * @return All hints associated with this Slice. */ - public @SliceHint String[] getHints() { - return mHints; + public @SliceHint List<String> getHints() { + return Arrays.asList(mHints); } /** @@ -163,14 +159,14 @@ public final class Slice implements Parcelable { */ public SliceItem getPrimaryIcon() { for (SliceItem item : getItems()) { - if (item.getType() == TYPE_IMAGE) { + if (item.getType() == SliceItem.TYPE_IMAGE) { return item; } - if (!(item.getType() == TYPE_SLICE && item.hasHint(Slice.HINT_LIST)) + if (!(item.getType() == SliceItem.TYPE_SLICE && item.hasHint(Slice.HINT_LIST)) && !item.hasHint(Slice.HINT_ACTIONS) && !item.hasHint(Slice.HINT_LIST_ITEM) - && (item.getType() != TYPE_ACTION)) { - SliceItem icon = SliceQuery.find(item, TYPE_IMAGE); + && (item.getType() != SliceItem.TYPE_ACTION)) { + SliceItem icon = SliceQuery.find(item, SliceItem.TYPE_IMAGE); if (icon != null) return icon; } } @@ -235,10 +231,18 @@ public final class Slice implements Parcelable { } /** + * Add hints to the Slice being constructed + */ + public Builder addHints(@SliceHint List<String> hints) { + return addHints(hints.toArray(new String[hints.size()])); + } + + /** * Add a sub-slice to the slice being constructed */ public Builder addSubSlice(@NonNull Slice slice) { - mItems.add(new SliceItem(slice, TYPE_SLICE, slice.getHints())); + mItems.add(new SliceItem(slice, SliceItem.TYPE_SLICE, slice.getHints().toArray( + new String[slice.getHints().size()]))); return this; } @@ -246,7 +250,7 @@ public final class Slice implements Parcelable { * Add an action to the slice being constructed */ public Slice.Builder addAction(@NonNull PendingIntent action, @NonNull Slice s) { - mItems.add(new SliceItem(action, s, TYPE_ACTION, new String[0])); + mItems.add(new SliceItem(action, s, SliceItem.TYPE_ACTION, new String[0])); return this; } @@ -254,31 +258,53 @@ public final class Slice implements Parcelable { * Add text to the slice being constructed */ public Builder addText(CharSequence text, @SliceHint String... hints) { - mItems.add(new SliceItem(text, TYPE_TEXT, hints)); + mItems.add(new SliceItem(text, SliceItem.TYPE_TEXT, hints)); return this; } /** + * Add text to the slice being constructed + */ + public Builder addText(CharSequence text, @SliceHint List<String> hints) { + return addText(text, hints.toArray(new String[hints.size()])); + } + + /** * Add an image to the slice being constructed */ public Builder addIcon(Icon icon, @SliceHint String... hints) { - mItems.add(new SliceItem(icon, TYPE_IMAGE, hints)); + mItems.add(new SliceItem(icon, SliceItem.TYPE_IMAGE, hints)); return this; } /** + * Add an image to the slice being constructed + */ + public Builder addIcon(Icon icon, @SliceHint List<String> hints) { + return addIcon(icon, hints.toArray(new String[hints.size()])); + } + + /** * @hide This isn't final */ public Builder addRemoteView(RemoteViews remoteView, @SliceHint String... hints) { - mItems.add(new SliceItem(remoteView, TYPE_REMOTE_VIEW, hints)); + mItems.add(new SliceItem(remoteView, SliceItem.TYPE_REMOTE_VIEW, hints)); return this; } /** * Add remote input to the slice being constructed */ + public Slice.Builder addRemoteInput(RemoteInput remoteInput, + @SliceHint List<String> hints) { + return addRemoteInput(remoteInput, hints.toArray(new String[hints.size()])); + } + + /** + * Add remote input to the slice being constructed + */ public Slice.Builder addRemoteInput(RemoteInput remoteInput, @SliceHint String... hints) { - mItems.add(new SliceItem(remoteInput, TYPE_REMOTE_INPUT, hints)); + mItems.add(new SliceItem(remoteInput, SliceItem.TYPE_REMOTE_INPUT, hints)); return this; } @@ -286,19 +312,33 @@ public final class Slice implements Parcelable { * Add a color to the slice being constructed */ public Builder addColor(int color, @SliceHint String... hints) { - mItems.add(new SliceItem(color, TYPE_COLOR, hints)); + mItems.add(new SliceItem(color, SliceItem.TYPE_COLOR, hints)); return this; } /** + * Add a color to the slice being constructed + */ + public Builder addColor(int color, @SliceHint List<String> hints) { + return addColor(color, hints.toArray(new String[hints.size()])); + } + + /** * Add a timestamp to the slice being constructed */ public Slice.Builder addTimestamp(long time, @SliceHint String... hints) { - mItems.add(new SliceItem(time, TYPE_TIMESTAMP, hints)); + mItems.add(new SliceItem(time, SliceItem.TYPE_TIMESTAMP, hints)); return this; } /** + * Add a timestamp to the slice being constructed + */ + public Slice.Builder addTimestamp(long time, @SliceHint List<String> hints) { + return addTimestamp(time, hints.toArray(new String[hints.size()])); + } + + /** * Construct the slice. */ public Slice build() { @@ -322,18 +362,18 @@ public final class Slice implements Parcelable { * @hide * @return A string representation of this slice. */ - public String getString() { - return getString(""); + public String toString() { + return toString(""); } - private String getString(String indent) { + private String toString(String indent) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < mItems.length; i++) { sb.append(indent); - if (mItems[i].getType() == TYPE_SLICE) { + if (mItems[i].getType() == SliceItem.TYPE_SLICE) { sb.append("slice:\n"); - sb.append(mItems[i].getSlice().getString(indent + " ")); - } else if (mItems[i].getType() == TYPE_TEXT) { + sb.append(mItems[i].getSlice().toString(indent + " ")); + } else if (mItems[i].getType() == SliceItem.TYPE_TEXT) { sb.append("text: "); sb.append(mItems[i].getText()); sb.append("\n"); @@ -344,4 +384,34 @@ public final class Slice implements Parcelable { } return sb.toString(); } + + /** + * Turns a slice Uri into slice content. + * + * @param resolver ContentResolver to be used. + * @param uri The URI to a slice provider + * @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) { + 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); + final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE, + null, extras); + Bundle.setDefusable(res, true); + return res.getParcelable(SliceProvider.EXTRA_SLICE); + } catch (RemoteException e) { + // Arbitrary and not worth documenting, as Activity + // Manager will kill this process shortly anyway. + return null; + } finally { + resolver.releaseProvider(provider); + } + } } diff --git a/android/slice/SliceItem.java b/android/app/slice/SliceItem.java index 2827ab9d..6e69b051 100644 --- a/android/slice/SliceItem.java +++ b/android/app/slice/SliceItem.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.slice; +package android.app.slice; import android.annotation.IntDef; import android.annotation.NonNull; @@ -23,13 +23,15 @@ import android.app.RemoteInput; import android.graphics.drawable.Icon; import android.os.Parcel; import android.os.Parcelable; -import android.slice.Slice.SliceHint; import android.text.TextUtils; import android.util.Pair; import android.widget.RemoteViews; import com.android.internal.util.ArrayUtils; +import java.util.Arrays; +import java.util.List; + /** * A SliceItem is a single unit in the tree structure of a {@link Slice}. @@ -47,7 +49,6 @@ import com.android.internal.util.ArrayUtils; * 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}. - * @hide */ public final class SliceItem implements Parcelable { @@ -97,14 +98,15 @@ public final class SliceItem implements Parcelable { /** * @hide */ - protected @SliceHint String[] mHints; + protected @Slice.SliceHint + String[] mHints; private final int mType; private final Object mObj; /** * @hide */ - public SliceItem(Object obj, @SliceType int type, @SliceHint String[] hints) { + public SliceItem(Object obj, @SliceType int type, @Slice.SliceHint String[] hints) { mHints = hints; mType = type; mObj = obj; @@ -113,7 +115,7 @@ public final class SliceItem implements Parcelable { /** * @hide */ - public SliceItem(PendingIntent intent, Slice slice, int type, @SliceHint String[] hints) { + public SliceItem(PendingIntent intent, Slice slice, int type, @Slice.SliceHint String[] hints) { this(new Pair<>(intent, slice), type, hints); } @@ -121,14 +123,14 @@ public final class SliceItem implements Parcelable { * Gets all hints associated with this SliceItem. * @return Array of hints. */ - public @NonNull @SliceHint String[] getHints() { - return mHints; + public @NonNull @Slice.SliceHint List<String> getHints() { + return Arrays.asList(mHints); } /** * @hide */ - public void addHint(@SliceHint String hint) { + public void addHint(@Slice.SliceHint String hint) { mHints = ArrayUtils.appendElement(String.class, mHints, hint); } @@ -206,7 +208,7 @@ public final class SliceItem implements Parcelable { * @param hint The hint to check for * @return true if this item contains the given hint */ - public boolean hasHint(@SliceHint String hint) { + public boolean hasHint(@Slice.SliceHint String hint) { return ArrayUtils.contains(mHints, hint); } @@ -234,7 +236,7 @@ public final class SliceItem implements Parcelable { /** * @hide */ - public boolean hasHints(@SliceHint String[] hints) { + public boolean hasHints(@Slice.SliceHint String[] hints) { if (hints == null) return true; for (String hint : hints) { if (!TextUtils.isEmpty(hint) && !ArrayUtils.contains(mHints, hint)) { @@ -247,7 +249,7 @@ public final class SliceItem implements Parcelable { /** * @hide */ - public boolean hasAnyHints(@SliceHint String[] hints) { + public boolean hasAnyHints(@Slice.SliceHint String[] hints) { if (hints == null) return false; for (String hint : hints) { if (ArrayUtils.contains(mHints, hint)) { diff --git a/android/slice/SliceProvider.java b/android/app/slice/SliceProvider.java index 4e21371b..df87b455 100644 --- a/android/slice/SliceProvider.java +++ b/android/app/slice/SliceProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.slice; +package android.app.slice; import android.Manifest.permission; import android.content.ContentProvider; @@ -26,6 +26,8 @@ import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; +import android.os.StrictMode; +import android.os.StrictMode.ThreadPolicy; import android.util.Log; import java.util.concurrent.CountDownLatch; @@ -51,10 +53,15 @@ import java.util.concurrent.CountDownLatch; * </pre> * * @see Slice - * @hide */ 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 @@ -73,8 +80,18 @@ public abstract class SliceProvider extends ContentProvider { /** * 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> + * * @see {@link Slice}. + * @see {@link Slice#HINT_PARTIAL} */ + // TODO: Provide alternate notifyChange that takes in the slice (i.e. notifyChange(Uri, Slice)). public abstract Slice onBindSlice(Uri sliceUri); @Override @@ -120,11 +137,11 @@ public abstract class SliceProvider extends ContentProvider { @Override public final String getType(Uri uri) { if (DEBUG) Log.d(TAG, "getType " + uri); - return null; + return SLICE_TYPE; } @Override - public final Bundle call(String method, String arg, Bundle extras) { + public Bundle call(String method, String arg, Bundle extras) { if (method.equals(METHOD_SLICE)) { getContext().enforceCallingPermission(permission.BIND_SLICE, "Slice binding requires the permission BIND_SLICE"); @@ -143,8 +160,17 @@ public abstract class SliceProvider extends ContentProvider { CountDownLatch latch = new CountDownLatch(1); Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.post(() -> { - output[0] = onBindSlice(sliceUri); - latch.countDown(); + ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + try { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyDeath() + .build()); + output[0] = onBindSlice(sliceUri); + } finally { + StrictMode.setThreadPolicy(oldPolicy); + latch.countDown(); + } }); try { latch.await(); diff --git a/android/slice/SliceQuery.java b/android/app/slice/SliceQuery.java index d99b26a5..d1fe2c90 100644 --- a/android/slice/SliceQuery.java +++ b/android/app/slice/SliceQuery.java @@ -14,12 +14,8 @@ * limitations under the License. */ -package android.slice; +package android.app.slice; -import static android.slice.SliceItem.TYPE_ACTION; -import static android.slice.SliceItem.TYPE_SLICE; - -import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -114,7 +110,9 @@ public class SliceQuery { * @hide */ public static SliceItem find(Slice s, int type, String[] hints, String[] nonHints) { - return find(new SliceItem(s, TYPE_SLICE, s.getHints()), type, hints, nonHints); + List<String> h = s.getHints(); + return find(new SliceItem(s, SliceItem.TYPE_SLICE, h.toArray(new String[h.size()])), type, + hints, nonHints); } /** @@ -140,8 +138,9 @@ public class SliceQuery { @Override public SliceItem next() { SliceItem item = items.poll(); - if (item.getType() == TYPE_SLICE || item.getType() == TYPE_ACTION) { - items.addAll(Arrays.asList(item.getSlice().getItems())); + if (item.getType() == SliceItem.TYPE_SLICE + || item.getType() == SliceItem.TYPE_ACTION) { + items.addAll(item.getSlice().getItems()); } return item; } diff --git a/android/slice/views/ActionRow.java b/android/app/slice/views/ActionRow.java index 93e9c035..c7d99f7f 100644 --- a/android/slice/views/ActionRow.java +++ b/android/app/slice/views/ActionRow.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.app.RemoteInput; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.drawable.Icon; import android.os.AsyncTask; -import android.slice.Slice; -import android.slice.SliceItem; -import android.slice.SliceQuery; import android.util.TypedValue; import android.view.View; import android.view.ViewParent; diff --git a/android/slice/views/GridView.java b/android/app/slice/views/GridView.java index 18a90f7d..6f30c507 100644 --- a/android/slice/views/GridView.java +++ b/android/app/slice/views/GridView.java @@ -14,16 +14,16 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.views.LargeSliceAdapter.SliceListView; import android.content.Context; import android.graphics.Color; -import android.slice.Slice; -import android.slice.SliceItem; -import android.slice.views.LargeSliceAdapter.SliceListView; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; @@ -38,7 +38,7 @@ import android.widget.TextView; import com.android.internal.R; import java.util.ArrayList; -import java.util.Arrays; +import java.util.List; /** * @hide @@ -76,10 +76,10 @@ public class GridView extends LinearLayout implements SliceListView { removeAllViews(); int total = 1; if (slice.getType() == SliceItem.TYPE_SLICE) { - SliceItem[] items = slice.getSlice().getItems(); - total = items.length; + List<SliceItem> items = slice.getSlice().getItems(); + total = items.size(); for (int i = 0; i < total; i++) { - SliceItem item = items[i]; + SliceItem item = items.get(i); if (isFull()) { continue; } @@ -142,7 +142,7 @@ public class GridView extends LinearLayout implements SliceListView { // TODO: Unify sporadic inflates that happen throughout the code. ArrayList<SliceItem> items = new ArrayList<>(); if (item.getType() == SliceItem.TYPE_SLICE) { - items.addAll(Arrays.asList(item.getSlice().getItems())); + items.addAll(item.getSlice().getItems()); } items.forEach(i -> { Context context = getContext(); diff --git a/android/slice/views/LargeSliceAdapter.java b/android/app/slice/views/LargeSliceAdapter.java index e77a1b2a..6794ff98 100644 --- a/android/slice/views/LargeSliceAdapter.java +++ b/android/app/slice/views/LargeSliceAdapter.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.app.slice.views.LargeSliceAdapter.SliceViewHolder; import android.content.Context; -import android.slice.Slice; -import android.slice.SliceItem; -import android.slice.SliceQuery; -import android.slice.views.LargeSliceAdapter.SliceViewHolder; import android.util.ArrayMap; import android.view.LayoutInflater; import android.view.View; diff --git a/android/slice/views/LargeTemplateView.java b/android/app/slice/views/LargeTemplateView.java index d53e8fcb..9e225162 100644 --- a/android/slice/views/LargeTemplateView.java +++ b/android/app/slice/views/LargeTemplateView.java @@ -14,22 +14,21 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.app.slice.views.SliceView.SliceModeView; import android.content.Context; -import android.slice.Slice; -import android.slice.SliceItem; -import android.slice.SliceQuery; -import android.slice.views.SliceView.SliceModeView; import android.util.TypedValue; import com.android.internal.widget.LinearLayoutManager; import com.android.internal.widget.RecyclerView; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -86,7 +85,7 @@ public class LargeTemplateView extends SliceModeView { if (slice.hasHint(Slice.HINT_LIST)) { addList(slice, items); } else { - Arrays.asList(slice.getItems()).forEach(item -> { + slice.getItems().forEach(item -> { if (item.hasHint(Slice.HINT_ACTIONS)) { return; } else if (item.getType() == SliceItem.TYPE_COLOR) { @@ -109,7 +108,7 @@ public class LargeTemplateView extends SliceModeView { } private void addList(Slice slice, List<SliceItem> items) { - List<SliceItem> sliceItems = Arrays.asList(slice.getItems()); + List<SliceItem> sliceItems = slice.getItems(); sliceItems.forEach(i -> i.addHint(Slice.HINT_LIST_ITEM)); items.addAll(sliceItems); } diff --git a/android/slice/views/MessageView.java b/android/app/slice/views/MessageView.java index 7b03e0bd..77252bf2 100644 --- a/android/slice/views/MessageView.java +++ b/android/app/slice/views/MessageView.java @@ -14,16 +14,16 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.app.slice.views.LargeSliceAdapter.SliceListView; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.Drawable; -import android.slice.Slice; -import android.slice.SliceItem; -import android.slice.SliceQuery; -import android.slice.views.LargeSliceAdapter.SliceListView; import android.text.SpannableStringBuilder; import android.util.AttributeSet; import android.util.TypedValue; diff --git a/android/slice/views/RemoteInputView.java b/android/app/slice/views/RemoteInputView.java index a29bb5c0..e53cb1ea 100644 --- a/android/slice/views/RemoteInputView.java +++ b/android/app/slice/views/RemoteInputView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; import android.animation.Animator; import android.app.Notification; diff --git a/android/slice/views/ShortcutView.java b/android/app/slice/views/ShortcutView.java index 8fe2f1ac..b6790c7d 100644 --- a/android/slice/views/ShortcutView.java +++ b/android/app/slice/views/ShortcutView.java @@ -14,20 +14,20 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.app.slice.views.SliceView.SliceModeView; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.net.Uri; -import android.slice.Slice; -import android.slice.SliceItem; -import android.slice.SliceQuery; -import android.slice.views.SliceView.SliceModeView; import android.view.ViewGroup; import com.android.internal.R; diff --git a/android/slice/views/SliceView.java b/android/app/slice/views/SliceView.java index f3792481..32484fca 100644 --- a/android/slice/views/SliceView.java +++ b/android/app/slice/views/SliceView.java @@ -14,23 +14,25 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; import android.annotation.StringDef; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.drawable.ColorDrawable; import android.net.Uri; -import android.slice.Slice; -import android.slice.SliceItem; -import android.slice.SliceQuery; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import android.widget.LinearLayout; +import java.util.List; + /** * A view that can display a {@link Slice} in different {@link SliceMode}'s. * @@ -120,7 +122,7 @@ public class SliceView extends LinearLayout { */ public void bindSlice(Uri sliceUri) { validate(sliceUri); - Slice s = mContext.getContentResolver().bindSlice(sliceUri); + Slice s = Slice.bindSlice(mContext.getContentResolver(), sliceUri); bindSlice(s); } @@ -201,7 +203,7 @@ public class SliceView extends LinearLayout { } // TODO: Smarter mapping here from one state to the next. SliceItem color = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_COLOR); - SliceItem[] items = mCurrentSlice.getItems(); + List<SliceItem> items = mCurrentSlice.getItems(); SliceItem actionRow = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_SLICE, Slice.HINT_ACTIONS, Slice.HINT_ALT); @@ -212,7 +214,7 @@ public class SliceView extends LinearLayout { addView(mCurrentView); addView(mActions); } - if (items.length > 1 || (items.length != 0 && items[0] != actionRow)) { + if (items.size() > 1 || (items.size() != 0 && items.get(0) != actionRow)) { mCurrentView.setVisibility(View.VISIBLE); mCurrentView.setSlice(mCurrentSlice); } else { @@ -239,7 +241,7 @@ public class SliceView extends LinearLayout { } private static void validate(Uri sliceUri) { - if (!ContentResolver.SCHEME_SLICE.equals(sliceUri.getScheme())) { + if (!ContentResolver.SCHEME_CONTENT.equals(sliceUri.getScheme())) { throw new RuntimeException("Invalid uri " + sliceUri); } if (sliceUri.getPathSegments().size() == 0) { diff --git a/android/slice/views/SliceViewUtil.java b/android/app/slice/views/SliceViewUtil.java index 1b5a6d1e..19e8e7c9 100644 --- a/android/slice/views/SliceViewUtil.java +++ b/android/app/slice/views/SliceViewUtil.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; import android.annotation.ColorInt; import android.content.Context; diff --git a/android/slice/views/SmallTemplateView.java b/android/app/slice/views/SmallTemplateView.java index b0b181ed..42b2d213 100644 --- a/android/slice/views/SmallTemplateView.java +++ b/android/app/slice/views/SmallTemplateView.java @@ -14,16 +14,16 @@ * limitations under the License. */ -package android.slice.views; +package android.app.slice.views; import android.app.PendingIntent.CanceledException; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.app.slice.views.LargeSliceAdapter.SliceListView; +import android.app.slice.views.SliceView.SliceModeView; import android.content.Context; import android.os.AsyncTask; -import android.slice.Slice; -import android.slice.SliceItem; -import android.slice.SliceQuery; -import android.slice.views.LargeSliceAdapter.SliceListView; -import android.slice.views.SliceView.SliceModeView; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; @@ -34,7 +34,6 @@ import com.android.internal.R; import java.text.Format; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; import java.util.List; @@ -117,7 +116,7 @@ public class SmallTemplateView extends SliceModeView implements SliceListView { int itemCount = 0; boolean hasSummary = false; ArrayList<SliceItem> sliceItems = new ArrayList<SliceItem>( - Arrays.asList(slice.getSlice().getItems())); + slice.getSlice().getItems()); for (int i = 0; i < sliceItems.size(); i++) { SliceItem item = sliceItems.get(i); if (!hasSummary && item.getType() == SliceItem.TYPE_TEXT @@ -140,9 +139,9 @@ public class SmallTemplateView extends SliceModeView implements SliceListView { mEndContainer.addView(tv); itemCount++; } else if (item.getType() == SliceItem.TYPE_SLICE) { - SliceItem[] subItems = item.getSlice().getItems(); - for (int j = 0; j < subItems.length; j++) { - sliceItems.add(subItems[j]); + List<SliceItem> subItems = item.getSlice().getItems(); + for (int j = 0; j < subItems.size(); j++) { + sliceItems.add(subItems.get(j)); } } } @@ -151,7 +150,8 @@ public class SmallTemplateView extends SliceModeView implements SliceListView { @Override public void setSlice(Slice slice) { - setSliceItem(new SliceItem(slice, SliceItem.TYPE_SLICE, slice.getHints())); + setSliceItem(new SliceItem(slice, SliceItem.TYPE_SLICE, + slice.getHints().toArray(new String[slice.getHints().size()]))); } /** diff --git a/android/app/usage/UsageStatsManager.java b/android/app/usage/UsageStatsManager.java index 051dccbd..fd579fce 100644 --- a/android/app/usage/UsageStatsManager.java +++ b/android/app/usage/UsageStatsManager.java @@ -48,10 +48,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 { @@ -122,7 +122,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 +139,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 +152,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) { @@ -185,7 +185,7 @@ public final class UsageStatsManager { return iter; } } catch (RemoteException e) { - // fallthrough and return null + // fallthrough and return empty result. } return sEmptyResults; } @@ -197,8 +197,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); diff --git a/android/appwidget/AppWidgetManager.java b/android/appwidget/AppWidgetManager.java index 969b19ee..37bb6b05 100644 --- a/android/appwidget/AppWidgetManager.java +++ b/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/android/arch/lifecycle/ActivityFullLifecycleTest.java b/android/arch/lifecycle/ActivityFullLifecycleTest.java index ee4e661a..78dd0150 100644 --- a/android/arch/lifecycle/ActivityFullLifecycleTest.java +++ b/android/arch/lifecycle/ActivityFullLifecycleTest.java @@ -16,48 +16,43 @@ package android.arch.lifecycle; -import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE; -import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY; -import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE; -import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME; -import static android.arch.lifecycle.Lifecycle.Event.ON_START; -import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; -import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; -import static android.arch.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT; +import static android.arch.lifecycle.TestUtils.OrderedTuples.CREATE; +import static android.arch.lifecycle.TestUtils.OrderedTuples.DESTROY; +import static android.arch.lifecycle.TestUtils.OrderedTuples.PAUSE; +import static android.arch.lifecycle.TestUtils.OrderedTuples.RESUME; +import static android.arch.lifecycle.TestUtils.OrderedTuples.START; +import static android.arch.lifecycle.TestUtils.OrderedTuples.STOP; +import static android.arch.lifecycle.TestUtils.flatMap; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import android.app.Activity; import android.arch.lifecycle.Lifecycle.Event; -import android.arch.lifecycle.testapp.CollectingActivity; +import android.arch.lifecycle.testapp.CollectingLifecycleOwner; +import android.arch.lifecycle.testapp.CollectingSupportActivity; import android.arch.lifecycle.testapp.FrameworkLifecycleRegistryActivity; -import android.arch.lifecycle.testapp.FullLifecycleTestActivity; -import android.arch.lifecycle.testapp.SupportLifecycleRegistryActivity; import android.arch.lifecycle.testapp.TestEvent; import android.support.test.filters.SmallTest; import android.support.test.rule.ActivityTestRule; -import android.util.Pair; +import android.support.v4.util.Pair; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import java.util.ArrayList; import java.util.List; @SmallTest @RunWith(Parameterized.class) public class ActivityFullLifecycleTest { @Rule - public ActivityTestRule activityTestRule = - new ActivityTestRule<>(FullLifecycleTestActivity.class); + public final ActivityTestRule<? extends CollectingLifecycleOwner> activityTestRule; @Parameterized.Parameters public static Class[] params() { - return new Class[]{FullLifecycleTestActivity.class, - SupportLifecycleRegistryActivity.class, + return new Class[]{CollectingSupportActivity.class, FrameworkLifecycleRegistryActivity.class}; } @@ -68,28 +63,13 @@ public class ActivityFullLifecycleTest { @Test - public void testFullLifecycle() throws InterruptedException { - Activity activity = activityTestRule.getActivity(); - List<Pair<TestEvent, Event>> results = ((CollectingActivity) activity) - .waitForCollectedEvents(); + public void testFullLifecycle() throws Throwable { + CollectingLifecycleOwner owner = activityTestRule.getActivity(); + TestUtils.waitTillResumed(owner, activityTestRule); + activityTestRule.finishActivity(); - Event[] expectedEvents = - new Event[]{ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY}; - - List<Pair<TestEvent, Event>> expected = new ArrayList<>(); - boolean beforeResume = true; - for (Event i : expectedEvents) { - if (beforeResume) { - expected.add(new Pair<>(ACTIVITY_CALLBACK, i)); - expected.add(new Pair<>(LIFECYCLE_EVENT, i)); - } else { - expected.add(new Pair<>(LIFECYCLE_EVENT, i)); - expected.add(new Pair<>(ACTIVITY_CALLBACK, i)); - } - if (i == ON_RESUME) { - beforeResume = false; - } - } - assertThat(results, is(expected)); + TestUtils.waitTillDestroyed(owner, activityTestRule); + List<Pair<TestEvent, Event>> results = owner.copyCollectedEvents(); + assertThat(results, is(flatMap(CREATE, START, RESUME, PAUSE, STOP, DESTROY))); } } diff --git a/android/arch/lifecycle/AndroidViewModel.java b/android/arch/lifecycle/AndroidViewModel.java index 2c7e1739..106b2ef0 100644 --- a/android/arch/lifecycle/AndroidViewModel.java +++ b/android/arch/lifecycle/AndroidViewModel.java @@ -16,7 +16,9 @@ package android.arch.lifecycle; +import android.annotation.SuppressLint; import android.app.Application; +import android.support.annotation.NonNull; /** * Application context aware {@link ViewModel}. @@ -25,16 +27,19 @@ import android.app.Application; * <p> */ public class AndroidViewModel extends ViewModel { + @SuppressLint("StaticFieldLeak") private Application mApplication; - public AndroidViewModel(Application application) { + public AndroidViewModel(@NonNull Application application) { mApplication = application; } /** * Return the application. */ + @NonNull public <T extends Application> T getApplication() { + //noinspection unchecked return (T) mApplication; } } diff --git a/android/arch/lifecycle/ClassesInfoCache.java b/android/arch/lifecycle/ClassesInfoCache.java index f077daed..d88e2762 100644 --- a/android/arch/lifecycle/ClassesInfoCache.java +++ b/android/arch/lifecycle/ClassesInfoCache.java @@ -46,7 +46,7 @@ class ClassesInfoCache { return mHasLifecycleMethods.get(klass); } - Method[] methods = klass.getDeclaredMethods(); + Method[] methods = getDeclaredMethods(klass); for (Method method : methods) { OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class); if (annotation != null) { @@ -64,6 +64,18 @@ class ClassesInfoCache { return false; } + private Method[] getDeclaredMethods(Class klass) { + try { + return klass.getDeclaredMethods(); + } catch (NoClassDefFoundError e) { + throw new IllegalArgumentException("The observer class has some methods that use " + + "newer APIs which are not available in the current OS version. Lifecycles " + + "cannot access even other methods so you should make sure that your " + + "observer classes only access framework classes that are available " + + "in your min API level OR use lifecycle:compiler annotation processor.", e); + } + } + CallbackInfo getInfo(Class klass) { CallbackInfo existing = mCallbackMap.get(klass); if (existing != null) { @@ -106,7 +118,7 @@ class ClassesInfoCache { } } - Method[] methods = declaredMethods != null ? declaredMethods : klass.getDeclaredMethods(); + Method[] methods = declaredMethods != null ? declaredMethods : getDeclaredMethods(klass); boolean hasLifecycleMethods = false; for (Method method : methods) { OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class); diff --git a/android/arch/lifecycle/Lifecycle.java b/android/arch/lifecycle/Lifecycle.java index 02db5ff9..c0a2090c 100644 --- a/android/arch/lifecycle/Lifecycle.java +++ b/android/arch/lifecycle/Lifecycle.java @@ -17,6 +17,7 @@ package android.arch.lifecycle; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; /** * Defines an object that has an Android Lifecycle. {@link android.support.v4.app.Fragment Fragment} @@ -83,7 +84,7 @@ public abstract class Lifecycle { * @param observer The observer to notify. */ @MainThread - public abstract void addObserver(LifecycleObserver observer); + public abstract void addObserver(@NonNull LifecycleObserver observer); /** * Removes the given observer from the observers list. @@ -99,7 +100,7 @@ public abstract class Lifecycle { * @param observer The observer to be removed. */ @MainThread - public abstract void removeObserver(LifecycleObserver observer); + public abstract void removeObserver(@NonNull LifecycleObserver observer); /** * Returns the current state of the Lifecycle. @@ -193,7 +194,7 @@ public abstract class Lifecycle { * @param state State to compare with * @return true if this State is greater or equal to the given {@code state} */ - public boolean isAtLeast(State state) { + public boolean isAtLeast(@NonNull State state) { return compareTo(state) >= 0; } } diff --git a/android/arch/lifecycle/LifecycleOwner.java b/android/arch/lifecycle/LifecycleOwner.java index 934cf3a2..068bac1b 100644 --- a/android/arch/lifecycle/LifecycleOwner.java +++ b/android/arch/lifecycle/LifecycleOwner.java @@ -16,6 +16,8 @@ package android.arch.lifecycle; +import android.support.annotation.NonNull; + /** * A class that has an Android lifecycle. These events can be used by custom components to * handle lifecycle changes without implementing any code inside the Activity or the Fragment. @@ -29,5 +31,6 @@ public interface LifecycleOwner { * * @return The lifecycle of the provider. */ + @NonNull Lifecycle getLifecycle(); } diff --git a/android/arch/lifecycle/LifecycleRegistry.java b/android/arch/lifecycle/LifecycleRegistry.java index b83e6b8a..bf8aff79 100644 --- a/android/arch/lifecycle/LifecycleRegistry.java +++ b/android/arch/lifecycle/LifecycleRegistry.java @@ -29,9 +29,12 @@ import static android.arch.lifecycle.Lifecycle.State.RESUMED; import static android.arch.lifecycle.Lifecycle.State.STARTED; import android.arch.core.internal.FastSafeIterableMap; +import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Log; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.Map.Entry; @@ -44,6 +47,8 @@ import java.util.Map.Entry; */ public class LifecycleRegistry extends Lifecycle { + private static final String LOG_TAG = "LifecycleRegistry"; + /** * Custom list that keeps observers and can handle removals / additions during traversal. * @@ -59,8 +64,12 @@ public class LifecycleRegistry extends Lifecycle { private State mState; /** * The provider that owns this Lifecycle. + * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak + * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither, + * because it keeps strong references on all other listeners, so you'll leak all of them as + * well. */ - private final LifecycleOwner mLifecycleOwner; + private final WeakReference<LifecycleOwner> mLifecycleOwner; private int mAddingObserverCounter = 0; @@ -86,19 +95,19 @@ public class LifecycleRegistry extends Lifecycle { * @param provider The owner LifecycleOwner */ public LifecycleRegistry(@NonNull LifecycleOwner provider) { - mLifecycleOwner = provider; + mLifecycleOwner = new WeakReference<>(provider); mState = INITIALIZED; } /** - * Only marks the current state as the given value. It doesn't dispatch any event to its - * listeners. + * Moves the Lifecycle to the given state and dispatches necessary events to the observers. * * @param state new state */ @SuppressWarnings("WeakerAccess") - public void markState(State state) { - mState = state; + @MainThread + public void markState(@NonNull State state) { + moveToState(state); } /** @@ -109,8 +118,16 @@ public class LifecycleRegistry extends Lifecycle { * * @param event The event that was received */ - public void handleLifecycleEvent(Lifecycle.Event event) { - mState = getStateAfter(event); + public void handleLifecycleEvent(@NonNull Lifecycle.Event event) { + State next = getStateAfter(event); + moveToState(next); + } + + private void moveToState(State next) { + if (mState == next) { + return; + } + mState = next; if (mHandlingEvent || mAddingObserverCounter != 0) { mNewEventOccurred = true; // we will figure out what to do on upper level. @@ -140,7 +157,7 @@ public class LifecycleRegistry extends Lifecycle { } @Override - public void addObserver(LifecycleObserver observer) { + public void addObserver(@NonNull LifecycleObserver observer) { State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED; ObserverWithState statefulObserver = new ObserverWithState(observer, initialState); ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver); @@ -148,15 +165,19 @@ public class LifecycleRegistry extends Lifecycle { if (previous != null) { return; } + LifecycleOwner lifecycleOwner = mLifecycleOwner.get(); + if (lifecycleOwner == null) { + // it is null we should be destroyed. Fallback quickly + return; + } boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent; - State targetState = calculateTargetState(observer); mAddingObserverCounter++; while ((statefulObserver.mState.compareTo(targetState) < 0 && mObserverMap.contains(observer))) { pushParentState(statefulObserver.mState); - statefulObserver.dispatchEvent(mLifecycleOwner, upEvent(statefulObserver.mState)); + statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState)); popParentState(); // mState / subling may have been changed recalculate targetState = calculateTargetState(observer); @@ -178,7 +199,7 @@ public class LifecycleRegistry extends Lifecycle { } @Override - public void removeObserver(LifecycleObserver observer) { + public void removeObserver(@NonNull LifecycleObserver observer) { // we consciously decided not to send destruction events here in opposition to addObserver. // Our reasons for that: // 1. These events haven't yet happened at all. In contrast to events in addObservers, that @@ -258,7 +279,7 @@ public class LifecycleRegistry extends Lifecycle { throw new IllegalArgumentException("Unexpected state value " + state); } - private void forwardPass() { + private void forwardPass(LifecycleOwner lifecycleOwner) { Iterator<Entry<LifecycleObserver, ObserverWithState>> ascendingIterator = mObserverMap.iteratorWithAdditions(); while (ascendingIterator.hasNext() && !mNewEventOccurred) { @@ -267,13 +288,13 @@ public class LifecycleRegistry extends Lifecycle { while ((observer.mState.compareTo(mState) < 0 && !mNewEventOccurred && mObserverMap.contains(entry.getKey()))) { pushParentState(observer.mState); - observer.dispatchEvent(mLifecycleOwner, upEvent(observer.mState)); + observer.dispatchEvent(lifecycleOwner, upEvent(observer.mState)); popParentState(); } } } - private void backwardPass() { + private void backwardPass(LifecycleOwner lifecycleOwner) { Iterator<Entry<LifecycleObserver, ObserverWithState>> descendingIterator = mObserverMap.descendingIterator(); while (descendingIterator.hasNext() && !mNewEventOccurred) { @@ -283,7 +304,7 @@ public class LifecycleRegistry extends Lifecycle { && mObserverMap.contains(entry.getKey()))) { Event event = downEvent(observer.mState); pushParentState(getStateAfter(event)); - observer.dispatchEvent(mLifecycleOwner, event); + observer.dispatchEvent(lifecycleOwner, event); popParentState(); } } @@ -292,16 +313,22 @@ public class LifecycleRegistry extends Lifecycle { // happens only on the top of stack (never in reentrance), // so it doesn't have to take in account parents private void sync() { + LifecycleOwner lifecycleOwner = mLifecycleOwner.get(); + if (lifecycleOwner == null) { + Log.w(LOG_TAG, "LifecycleOwner is garbage collected, you shouldn't try dispatch " + + "new events from it."); + return; + } while (!isSynced()) { mNewEventOccurred = false; // no need to check eldest for nullability, because isSynced does it for us. if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) { - backwardPass(); + backwardPass(lifecycleOwner); } Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest(); if (!mNewEventOccurred && newest != null && mState.compareTo(newest.getValue().mState) > 0) { - forwardPass(); + forwardPass(lifecycleOwner); } } mNewEventOccurred = false; diff --git a/android/arch/lifecycle/LifecycleRegistryOwner.java b/android/arch/lifecycle/LifecycleRegistryOwner.java index 38eeb6d3..0c67fefe 100644 --- a/android/arch/lifecycle/LifecycleRegistryOwner.java +++ b/android/arch/lifecycle/LifecycleRegistryOwner.java @@ -16,6 +16,8 @@ package android.arch.lifecycle; +import android.support.annotation.NonNull; + /** * @deprecated Use {@code android.support.v7.app.AppCompatActivity} * which extends {@link LifecycleOwner}, so there are no use cases for this class. @@ -23,6 +25,7 @@ package android.arch.lifecycle; @SuppressWarnings({"WeakerAccess", "unused"}) @Deprecated public interface LifecycleRegistryOwner extends LifecycleOwner { + @NonNull @Override LifecycleRegistry getLifecycle(); } diff --git a/android/arch/lifecycle/LifecycleRegistryTest.java b/android/arch/lifecycle/LifecycleRegistryTest.java index 6506454d..2a7bbad2 100644 --- a/android/arch/lifecycle/LifecycleRegistryTest.java +++ b/android/arch/lifecycle/LifecycleRegistryTest.java @@ -566,6 +566,25 @@ public class LifecycleRegistryTest { verify(observer).onCreate(); } + private static void forceGc() { + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + } + + @Test + public void goneLifecycleOwner() { + fullyInitializeRegistry(); + mLifecycleOwner = null; + forceGc(); + TestObserver observer = mock(TestObserver.class); + mRegistry.addObserver(observer); + verify(observer, never()).onCreate(); + verify(observer, never()).onStart(); + verify(observer, never()).onResume(); + } + private void dispatchEvent(Lifecycle.Event event) { when(mLifecycle.getCurrentState()).thenReturn(LifecycleRegistry.getStateAfter(event)); mRegistry.handleLifecycleEvent(event); diff --git a/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.java b/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.java new file mode 100644 index 00000000..836cfff0 --- /dev/null +++ b/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.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.arch.lifecycle; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import android.app.Instrumentation; +import android.arch.lifecycle.testapp.CollectingSupportActivity; +import android.arch.lifecycle.testapp.CollectingSupportFragment; +import android.arch.lifecycle.testapp.NavigationDialogActivity; +import android.content.Intent; +import android.os.Build; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.v4.app.FragmentActivity; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.atomic.AtomicInteger; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class LiveDataOnSaveInstanceStateTest { + @Rule + public ActivityTestRule<CollectingSupportActivity> mActivityTestRule = + new ActivityTestRule<>(CollectingSupportActivity.class); + + @Test + @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M) + public void liveData_partiallyObscuredActivity_maxSdkM() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + + liveData_partiallyObscuredLifecycleOwner_maxSdkM(activity); + } + + @Test + @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M) + public void liveData_partiallyObscuredActivityWithFragment_maxSdkM() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + CollectingSupportFragment fragment = new CollectingSupportFragment(); + mActivityTestRule.runOnUiThread(() -> activity.replaceFragment(fragment)); + + liveData_partiallyObscuredLifecycleOwner_maxSdkM(fragment); + } + + @Test + @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M) + public void liveData_partiallyObscuredActivityFragmentInFragment_maxSdkM() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + CollectingSupportFragment fragment = new CollectingSupportFragment(); + CollectingSupportFragment fragment2 = new CollectingSupportFragment(); + mActivityTestRule.runOnUiThread(() -> { + activity.replaceFragment(fragment); + fragment.replaceFragment(fragment2); + }); + + liveData_partiallyObscuredLifecycleOwner_maxSdkM(fragment2); + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) + public void liveData_partiallyObscuredActivity_minSdkN() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + + liveData_partiallyObscuredLifecycleOwner_minSdkN(activity); + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) + public void liveData_partiallyObscuredActivityWithFragment_minSdkN() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + CollectingSupportFragment fragment = new CollectingSupportFragment(); + mActivityTestRule.runOnUiThread(() -> activity.replaceFragment(fragment)); + + liveData_partiallyObscuredLifecycleOwner_minSdkN(fragment); + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) + public void liveData_partiallyObscuredActivityFragmentInFragment_minSdkN() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + CollectingSupportFragment fragment = new CollectingSupportFragment(); + CollectingSupportFragment fragment2 = new CollectingSupportFragment(); + mActivityTestRule.runOnUiThread(() -> { + activity.replaceFragment(fragment); + fragment.replaceFragment(fragment2); + }); + + liveData_partiallyObscuredLifecycleOwner_minSdkN(fragment2); + } + + private void liveData_partiallyObscuredLifecycleOwner_maxSdkM(LifecycleOwner lifecycleOwner) + throws Throwable { + final AtomicInteger atomicInteger = new AtomicInteger(0); + MutableLiveData<Integer> mutableLiveData = new MutableLiveData<>(); + mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(0)); + + TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule); + + mutableLiveData.observe(lifecycleOwner, atomicInteger::set); + + final FragmentActivity dialogActivity = launchDialog(); + + TestUtils.waitTillCreated(lifecycleOwner, mActivityTestRule); + + // Change the LiveData value and assert that the observer is not called given that the + // lifecycle is in the CREATED state. + mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(1)); + assertThat(atomicInteger.get(), is(0)); + + // Finish the dialog Activity, wait for the main activity to be resumed, and assert that + // the observer's onChanged method is called. + mActivityTestRule.runOnUiThread(dialogActivity::finish); + TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule); + assertThat(atomicInteger.get(), is(1)); + } + + private void liveData_partiallyObscuredLifecycleOwner_minSdkN(LifecycleOwner lifecycleOwner) + throws Throwable { + final AtomicInteger atomicInteger = new AtomicInteger(0); + MutableLiveData<Integer> mutableLiveData = new MutableLiveData<>(); + mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(0)); + + TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule); + + mutableLiveData.observe(lifecycleOwner, atomicInteger::set); + + // Launch the NavigationDialogActivity, partially obscuring the activity, and wait for the + // lifecycleOwner to hit onPause (or enter the STARTED state). On API 24 and above, this + // onPause should be the last lifecycle method called (and the STARTED state should be the + // final resting state). + launchDialog(); + TestUtils.waitTillStarted(lifecycleOwner, mActivityTestRule); + + // Change the LiveData's value and verify that the observer's onChanged method is called + // since we are in the STARTED state. + mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(1)); + assertThat(atomicInteger.get(), is(1)); + } + + private FragmentActivity launchDialog() throws Throwable { + Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor( + NavigationDialogActivity.class.getCanonicalName(), null, false); + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + instrumentation.addMonitor(monitor); + + FragmentActivity activity = mActivityTestRule.getActivity(); + // helps with less flaky API 16 tests + Intent intent = new Intent(activity, NavigationDialogActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + activity.startActivity(intent); + FragmentActivity fragmentActivity = (FragmentActivity) monitor.waitForActivity(); + TestUtils.waitTillResumed(fragmentActivity, mActivityTestRule); + return fragmentActivity; + } +} diff --git a/android/arch/lifecycle/LiveDataTest.java b/android/arch/lifecycle/LiveDataTest.java index 9f0b4257..647d5d7a 100644 --- a/android/arch/lifecycle/LiveDataTest.java +++ b/android/arch/lifecycle/LiveDataTest.java @@ -53,18 +53,29 @@ import org.mockito.Mockito; public class LiveDataTest { private PublicLiveData<String> mLiveData; private LifecycleOwner mOwner; + private LifecycleOwner mOwner2; private LifecycleRegistry mRegistry; + private LifecycleRegistry mRegistry2; private MethodExec mActiveObserversChanged; private boolean mInObserver; @Before public void init() { mLiveData = new PublicLiveData<>(); + + mActiveObserversChanged = mock(MethodExec.class); + mLiveData.activeObserversChanged = mActiveObserversChanged; + mOwner = mock(LifecycleOwner.class); + mRegistry = new LifecycleRegistry(mOwner); when(mOwner.getLifecycle()).thenReturn(mRegistry); - mActiveObserversChanged = mock(MethodExec.class); - mLiveData.activeObserversChanged = mActiveObserversChanged; + + mOwner2 = mock(LifecycleOwner.class); + + mRegistry2 = new LifecycleRegistry(mOwner2); + when(mOwner2.getLifecycle()).thenReturn(mRegistry2); + mInObserver = false; } @@ -159,14 +170,11 @@ public class LiveDataTest { @Test public void testAddSameObserverIn2LifecycleOwners() { Observer<String> observer = (Observer<String>) mock(Observer.class); - LifecycleOwner owner2 = mock(LifecycleOwner.class); - LifecycleRegistry registry2 = new LifecycleRegistry(owner2); - when(owner2.getLifecycle()).thenReturn(registry2); mLiveData.observe(mOwner, observer); Throwable throwable = null; try { - mLiveData.observe(owner2, observer); + mLiveData.observe(mOwner2, observer); } catch (Throwable t) { throwable = t; } @@ -456,6 +464,210 @@ public class LiveDataTest { inOrder.verifyNoMoreInteractions(); } + @Test + public void setValue_neverActive_observerOnChangedNotCalled() { + Observer<String> observer = (Observer<String>) mock(Observer.class); + mLiveData.observe(mOwner, observer); + + mLiveData.setValue("1"); + + verify(observer, never()).onChanged(anyString()); + } + + @Test + public void setValue_twoObserversTwoStartedOwners_onChangedCalledOnBoth() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + + mLiveData.setValue("1"); + + verify(observer1).onChanged("1"); + verify(observer2).onChanged("1"); + } + + @Test + public void setValue_twoObserversOneStartedOwner_onChangedCalledOnOneCorrectObserver() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + mLiveData.setValue("1"); + + verify(observer1).onChanged("1"); + verify(observer2, never()).onChanged(anyString()); + } + + @Test + public void setValue_twoObserversBothStartedAfterSetValue_onChangedCalledOnBoth() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mLiveData.setValue("1"); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(observer1).onChanged("1"); + verify(observer1).onChanged("1"); + } + + @Test + public void setValue_twoObserversOneStartedAfterSetValue_onChangedCalledOnCorrectObserver() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mLiveData.setValue("1"); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(observer1).onChanged("1"); + verify(observer2, never()).onChanged(anyString()); + } + + @Test + public void setValue_twoObserversOneStarted_liveDataBecomesActive() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(mActiveObserversChanged).onCall(true); + } + + @Test + public void setValue_twoObserversOneStopped_liveDataStaysActive() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(mActiveObserversChanged).onCall(true); + + reset(mActiveObserversChanged); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + } + + /** + * Verifies that if a lifecycle's state changes without an event, and changes to something that + * LiveData would become inactive in response to, LiveData will detect the change upon new data + * being set and become inactive. Also verifies that once the lifecycle enters into a state + * that LiveData should become active to, that it does indeed become active. + */ + @Test + public void liveDataActiveStateIsManagedCorrectlyWithoutEvent_oneObserver() { + Observer<String> observer = (Observer<String>) mock(Observer.class); + mLiveData.observe(mOwner, observer); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + // Marking state as CREATED should call onInactive. + reset(mActiveObserversChanged); + mRegistry.markState(Lifecycle.State.CREATED); + verify(mActiveObserversChanged).onCall(false); + reset(mActiveObserversChanged); + + // Setting a new value should trigger LiveData to realize the Lifecycle it is observing + // is in a state where the LiveData should be inactive, so the LiveData will call onInactive + // and the Observer shouldn't be affected. + mLiveData.setValue("1"); + + // state is already CREATED so should not call again + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + verify(observer, never()).onChanged(anyString()); + + // Sanity check. Because we've only marked the state as CREATED, sending ON_START + // should re-dispatch events. + reset(mActiveObserversChanged); + reset(observer); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + verify(mActiveObserversChanged).onCall(true); + verify(observer).onChanged("1"); + } + + /** + * This test verifies that LiveData will detect changes in LifecycleState that would make it + * inactive upon the setting of new data, but only if all of the Lifecycles it's observing + * are all in those states. It also makes sure that once it is inactive, that it will become + * active again once one of the lifecycles it's observing moves to an appropriate state. + */ + @Test + public void liveDataActiveStateIsManagedCorrectlyWithoutEvent_twoObservers() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + + // Marking the state to created won't change LiveData to be inactive. + reset(mActiveObserversChanged); + mRegistry.markState(Lifecycle.State.CREATED); + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + + // After setting a value, the LiveData will stay active because there is still a STARTED + // lifecycle being observed. The one Observer associated with the STARTED lifecycle will + // also have been called, but the other Observer will not have been called. + reset(observer1); + reset(observer2); + mLiveData.setValue("1"); + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + verify(observer1, never()).onChanged(anyString()); + verify(observer2).onChanged("1"); + + // Now we set the other Lifecycle to be inactive, live data should become inactive. + reset(observer1); + reset(observer2); + mRegistry2.markState(Lifecycle.State.CREATED); + verify(mActiveObserversChanged).onCall(false); + verify(observer1, never()).onChanged(anyString()); + verify(observer2, never()).onChanged(anyString()); + + // Now we post another value, because both lifecycles are in the Created state, live data + // will not dispatch any values + reset(mActiveObserversChanged); + mLiveData.setValue("2"); + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + verify(observer1, never()).onChanged(anyString()); + verify(observer2, never()).onChanged(anyString()); + + // Now that the first Lifecycle has been moved back to the Resumed state, the LiveData will + // be made active and it's associated Observer will be called with the new value, but the + // Observer associated with the Lifecycle that is still in the Created state won't be + // called. + reset(mActiveObserversChanged); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + verify(mActiveObserversChanged).onCall(true); + verify(observer1).onChanged("2"); + verify(observer2, never()).onChanged(anyString()); + } + @SuppressWarnings("WeakerAccess") static class PublicLiveData<T> extends LiveData<T> { // cannot spy due to internal calls diff --git a/android/arch/lifecycle/MediatorLiveData.java b/android/arch/lifecycle/MediatorLiveData.java index 672b3a3b..58647394 100644 --- a/android/arch/lifecycle/MediatorLiveData.java +++ b/android/arch/lifecycle/MediatorLiveData.java @@ -19,16 +19,49 @@ package android.arch.lifecycle; import android.arch.core.internal.SafeIterableMap; import android.support.annotation.CallSuper; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.Map; /** - * {@link LiveData} subclass which may observer other {@code LiveData} objects and react on + * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on * {@code OnChanged} events from them. * <p> * This class correctly propagates its active/inactive states down to source {@code LiveData} * objects. + * <p> + * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them + * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object: + * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for + * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback + * is called for either of them, we set a new value in {@code liveDataMerger}. + * + * <pre> + * LiveData<Integer> liveData1 = ...; + * LiveData<Integer> liveData2 = ...; + * + * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>(); + * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value)); + * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value)); + * </pre> + * <p> + * Let's consider that we only want 10 values emitted by {@code liveData1}, to be + * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code + * liveData1} and remove it as a source. + * <pre> + * liveDataMerger.addSource(liveData1, new Observer<Integer>() { + * private int count = 1; + * + * {@literal @}Override public void onChanged(@Nullable Integer s) { + * count++; + * liveDataMerger.setValue(s); + * if (count > 10) { + * liveDataMerger.removeSource(liveData1); + * } + * } + * }); + * </pre> * * @param <T> The type of data hold by this instance */ @@ -49,7 +82,7 @@ public class MediatorLiveData<T> extends MutableLiveData<T> { * @param <S> The type of data hold by {@code source} LiveData */ @MainThread - public <S> void addSource(LiveData<S> source, Observer<S> onChanged) { + public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<S> onChanged) { Source<S> e = new Source<>(source, onChanged); Source<?> existing = mSources.putIfAbsent(source, e); if (existing != null && existing.mObserver != onChanged) { @@ -71,7 +104,7 @@ public class MediatorLiveData<T> extends MutableLiveData<T> { * @param <S> the type of data hold by {@code source} LiveData */ @MainThread - public <S> void removeSource(LiveData<S> toRemote) { + public <S> void removeSource(@NonNull LiveData<S> toRemote) { Source<?> source = mSources.remove(toRemote); if (source != null) { source.unplug(); diff --git a/android/arch/lifecycle/MissingClassTest.java b/android/arch/lifecycle/MissingClassTest.java new file mode 100644 index 00000000..81a07564 --- /dev/null +++ b/android/arch/lifecycle/MissingClassTest.java @@ -0,0 +1,44 @@ +/* + * 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.arch.lifecycle; + +import android.app.PictureInPictureParams; +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1) +@SmallTest +public class MissingClassTest { + public static class ObserverWithMissingClasses { + @SuppressWarnings("unused") + public void newApiMethod(PictureInPictureParams params) {} + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() {} + } + + @Test(expected = IllegalArgumentException.class) + public void testMissingApi() { + new ReflectiveGenericLifecycleObserver(new ObserverWithMissingClasses()); + } +} diff --git a/android/arch/lifecycle/PartiallyCoveredActivityTest.java b/android/arch/lifecycle/PartiallyCoveredActivityTest.java new file mode 100644 index 00000000..07a9dc5a --- /dev/null +++ b/android/arch/lifecycle/PartiallyCoveredActivityTest.java @@ -0,0 +1,200 @@ +/* + * 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.arch.lifecycle; + +import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME; +import static android.arch.lifecycle.Lifecycle.Event.ON_START; +import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; +import static android.arch.lifecycle.TestUtils.OrderedTuples.CREATE; +import static android.arch.lifecycle.TestUtils.OrderedTuples.DESTROY; +import static android.arch.lifecycle.TestUtils.OrderedTuples.PAUSE; +import static android.arch.lifecycle.TestUtils.OrderedTuples.RESUME; +import static android.arch.lifecycle.TestUtils.OrderedTuples.START; +import static android.arch.lifecycle.TestUtils.OrderedTuples.STOP; +import static android.arch.lifecycle.TestUtils.flatMap; +import static android.arch.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT; +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +import android.app.Instrumentation; +import android.arch.lifecycle.testapp.CollectingLifecycleOwner; +import android.arch.lifecycle.testapp.CollectingSupportActivity; +import android.arch.lifecycle.testapp.CollectingSupportFragment; +import android.arch.lifecycle.testapp.NavigationDialogActivity; +import android.arch.lifecycle.testapp.TestEvent; +import android.content.Intent; +import android.os.Build; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; +import android.support.v4.app.FragmentActivity; +import android.support.v4.util.Pair; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * Runs tests about the state when an activity is partially covered by another activity. Pre + * API 24, framework behavior changes so the test rely on whether state is saved or not and makes + * assertions accordingly. + */ +@SuppressWarnings("unchecked") +@RunWith(Parameterized.class) +@LargeTest +public class PartiallyCoveredActivityTest { + private static final List[] IF_SAVED = new List[]{ + // when overlaid + flatMap(CREATE, START, RESUME, PAUSE, + singletonList(new Pair<>(LIFECYCLE_EVENT, ON_STOP))), + // post dialog dismiss + asList(new Pair<>(OWNER_CALLBACK, ON_RESUME), + new Pair<>(LIFECYCLE_EVENT, ON_START), + new Pair<>(LIFECYCLE_EVENT, ON_RESUME)), + // post finish + flatMap(PAUSE, STOP, DESTROY)}; + + private static final List[] IF_NOT_SAVED = new List[]{ + // when overlaid + flatMap(CREATE, START, RESUME, PAUSE), + // post dialog dismiss + flatMap(RESUME), + // post finish + flatMap(PAUSE, STOP, DESTROY)}; + + private static final boolean sShouldSave = Build.VERSION.SDK_INT < Build.VERSION_CODES.N; + private static final List<Pair<TestEvent, Lifecycle.Event>>[] EXPECTED = + sShouldSave ? IF_SAVED : IF_NOT_SAVED; + + @Rule + public ActivityTestRule<CollectingSupportActivity> activityRule = + new ActivityTestRule<CollectingSupportActivity>( + CollectingSupportActivity.class) { + @Override + protected Intent getActivityIntent() { + // helps with less flaky API 16 tests + Intent intent = new Intent(InstrumentationRegistry.getTargetContext(), + CollectingSupportActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return intent; + } + }; + private final boolean mDismissDialog; + + @Parameterized.Parameters(name = "dismissDialog_{0}") + public static List<Boolean> dismissDialog() { + return asList(true, false); + } + + public PartiallyCoveredActivityTest(boolean dismissDialog) { + mDismissDialog = dismissDialog; + } + + @Test + public void coveredWithDialog_activity() throws Throwable { + final CollectingSupportActivity activity = activityRule.getActivity(); + runTest(activity); + } + + @Test + public void coveredWithDialog_fragment() throws Throwable { + CollectingSupportFragment fragment = new CollectingSupportFragment(); + activityRule.runOnUiThread(() -> activityRule.getActivity().replaceFragment(fragment)); + runTest(fragment); + } + + @Test + public void coveredWithDialog_childFragment() throws Throwable { + CollectingSupportFragment parentFragment = new CollectingSupportFragment(); + CollectingSupportFragment childFragment = new CollectingSupportFragment(); + activityRule.runOnUiThread(() -> { + activityRule.getActivity().replaceFragment(parentFragment); + parentFragment.replaceFragment(childFragment); + }); + runTest(childFragment); + } + + private void runTest(CollectingLifecycleOwner owner) throws Throwable { + TestUtils.waitTillResumed(owner, activityRule); + FragmentActivity dialog = launchDialog(); + assertStateSaving(); + waitForIdle(); + assertThat(owner.copyCollectedEvents(), is(EXPECTED[0])); + List<Pair<TestEvent, Lifecycle.Event>> expected; + if (mDismissDialog) { + dialog.finish(); + TestUtils.waitTillResumed(activityRule.getActivity(), activityRule); + assertThat(owner.copyCollectedEvents(), is(flatMap(EXPECTED[0], EXPECTED[1]))); + expected = flatMap(EXPECTED[0], EXPECTED[1], EXPECTED[2]); + } else { + expected = flatMap(CREATE, START, RESUME, PAUSE, STOP, DESTROY); + } + CollectingSupportActivity activity = activityRule.getActivity(); + activityRule.finishActivity(); + TestUtils.waitTillDestroyed(activity, activityRule); + assertThat(owner.copyCollectedEvents(), is(expected)); + } + + // test sanity + private void assertStateSaving() throws ExecutionException, InterruptedException { + final CollectingSupportActivity activity = activityRule.getActivity(); + if (sShouldSave) { + // state should be saved. wait for it to be saved + assertThat("test sanity", + activity.waitForStateSave(20), is(true)); + assertThat("test sanity", activity.getSupportFragmentManager() + .isStateSaved(), is(true)); + } else { + // should should not be saved + assertThat("test sanity", activity.getSupportFragmentManager() + .isStateSaved(), is(false)); + } + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private FragmentActivity launchDialog() throws Throwable { + Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor( + NavigationDialogActivity.class.getCanonicalName(), null, false); + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + instrumentation.addMonitor(monitor); + + FragmentActivity activity = activityRule.getActivity(); + + Intent intent = new Intent(activity, NavigationDialogActivity.class); + // disabling animations helps with less flaky API 16 tests + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + activity.startActivity(intent); + FragmentActivity fragmentActivity = (FragmentActivity) monitor.waitForActivity(); + TestUtils.waitTillResumed(fragmentActivity, activityRule); + return fragmentActivity; + } +} diff --git a/android/arch/lifecycle/ProcessLifecycleOwner.java b/android/arch/lifecycle/ProcessLifecycleOwner.java index e2a12563..74ea97f7 100644 --- a/android/arch/lifecycle/ProcessLifecycleOwner.java +++ b/android/arch/lifecycle/ProcessLifecycleOwner.java @@ -22,6 +22,7 @@ import android.arch.lifecycle.ReportFragment.ActivityInitializationListener; import android.content.Context; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; /** @@ -156,7 +157,7 @@ public class ProcessLifecycleOwner implements LifecycleOwner { app.registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - ReportFragment .get(activity).setProcessListener(mInitializationListener); + ReportFragment.get(activity).setProcessListener(mInitializationListener); } @Override @@ -171,6 +172,7 @@ public class ProcessLifecycleOwner implements LifecycleOwner { }); } + @NonNull @Override public Lifecycle getLifecycle() { return mRegistry; diff --git a/android/arch/lifecycle/LifecycleRuntimeTrojanProvider.java b/android/arch/lifecycle/ProcessLifecycleOwnerInitializer.java index ac278c0c..6cf80d2d 100644 --- a/android/arch/lifecycle/LifecycleRuntimeTrojanProvider.java +++ b/android/arch/lifecycle/ProcessLifecycleOwnerInitializer.java @@ -29,7 +29,7 @@ import android.support.annotation.RestrictTo; * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class LifecycleRuntimeTrojanProvider extends ContentProvider { +public class ProcessLifecycleOwnerInitializer extends ContentProvider { @Override public boolean onCreate() { LifecycleDispatcher.init(getContext()); diff --git a/android/arch/lifecycle/ProcessOwnerTest.java b/android/arch/lifecycle/ProcessOwnerTest.java index 37bdcdb4..77baf94c 100644 --- a/android/arch/lifecycle/ProcessOwnerTest.java +++ b/android/arch/lifecycle/ProcessOwnerTest.java @@ -31,6 +31,7 @@ import android.arch.lifecycle.Lifecycle.Event; import android.arch.lifecycle.testapp.NavigationDialogActivity; import android.arch.lifecycle.testapp.NavigationTestActivityFirst; import android.arch.lifecycle.testapp.NavigationTestActivitySecond; +import android.arch.lifecycle.testapp.NonSupportActivity; import android.content.Context; import android.content.Intent; import android.support.test.InstrumentationRegistry; @@ -95,6 +96,22 @@ public class ProcessOwnerTest { } @Test + public void testNavigationToNonSupport() throws Throwable { + FragmentActivity firstActivity = setupObserverOnResume(); + Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor( + NonSupportActivity.class.getCanonicalName(), null, false); + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + instrumentation.addMonitor(monitor); + + Intent intent = new Intent(firstActivity, NonSupportActivity.class); + firstActivity.finish(); + firstActivity.startActivity(intent); + NonSupportActivity secondActivity = (NonSupportActivity) monitor.waitForActivity(); + assertThat("Failed to navigate", secondActivity, notNullValue()); + checkProcessObserverSilent(secondActivity); + } + + @Test public void testRecreation() throws Throwable { FragmentActivity activity = setupObserverOnResume(); FragmentActivity recreated = TestUtils.recreateActivity(activity, activityTestRule); @@ -164,4 +181,11 @@ public class ProcessOwnerTest { activityTestRule.runOnUiThread(() -> ProcessLifecycleOwner.get().getLifecycle().removeObserver(mObserver)); } + + private void checkProcessObserverSilent(NonSupportActivity activity) throws Throwable { + assertThat(activity.awaitResumedState(), is(true)); + assertThat(mObserver.mChangedState, is(false)); + activityTestRule.runOnUiThread(() -> + ProcessLifecycleOwner.get().getLifecycle().removeObserver(mObserver)); + } } diff --git a/android/arch/lifecycle/ReportFragment.java b/android/arch/lifecycle/ReportFragment.java index 3e4ece82..16a89ce8 100644 --- a/android/arch/lifecycle/ReportFragment.java +++ b/android/arch/lifecycle/ReportFragment.java @@ -28,7 +28,6 @@ import android.support.annotation.RestrictTo; */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ReportFragment extends Fragment { - private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle" + ".LifecycleDispatcher.report_fragment_tag"; diff --git a/android/arch/lifecycle/TestUtils.java b/android/arch/lifecycle/TestUtils.java index f0214bfb..f7f9bbe5 100644 --- a/android/arch/lifecycle/TestUtils.java +++ b/android/arch/lifecycle/TestUtils.java @@ -16,16 +16,35 @@ package android.arch.lifecycle; +import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE; +import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY; +import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE; +import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME; +import static android.arch.lifecycle.Lifecycle.Event.ON_START; +import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; +import static android.arch.lifecycle.Lifecycle.State.CREATED; +import static android.arch.lifecycle.Lifecycle.State.DESTROYED; import static android.arch.lifecycle.Lifecycle.State.RESUMED; +import static android.arch.lifecycle.Lifecycle.State.STARTED; +import static android.arch.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT; +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; import android.app.Activity; import android.app.Instrumentation; import android.app.Instrumentation.ActivityMonitor; +import android.arch.lifecycle.testapp.TestEvent; import android.support.test.InstrumentationRegistry; import android.support.test.rule.ActivityTestRule; -import android.support.v4.app.FragmentActivity; +import android.support.v4.util.Pair; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; class TestUtils { @@ -61,23 +80,88 @@ class TestUtils { return result; } - static void waitTillResumed(final FragmentActivity a, ActivityTestRule<?> activityRule) + static void waitTillCreated(final LifecycleOwner owner, ActivityTestRule<?> activityRule) + throws Throwable { + waitTillState(owner, activityRule, CREATED); + } + + static void waitTillStarted(final LifecycleOwner owner, ActivityTestRule<?> activityRule) + throws Throwable { + waitTillState(owner, activityRule, STARTED); + } + + static void waitTillResumed(final LifecycleOwner owner, ActivityTestRule<?> activityRule) + throws Throwable { + waitTillState(owner, activityRule, RESUMED); + } + + static void waitTillDestroyed(final LifecycleOwner owner, ActivityTestRule<?> activityRule) + throws Throwable { + waitTillState(owner, activityRule, DESTROYED); + } + + static void waitTillState(final LifecycleOwner owner, ActivityTestRule<?> activityRule, + Lifecycle.State state) throws Throwable { final CountDownLatch latch = new CountDownLatch(1); activityRule.runOnUiThread(() -> { - Lifecycle.State currentState = a.getLifecycle().getCurrentState(); - if (currentState == RESUMED) { + Lifecycle.State currentState = owner.getLifecycle().getCurrentState(); + if (currentState == state) { latch.countDown(); + } else { + owner.getLifecycle().addObserver(new LifecycleObserver() { + @OnLifecycleEvent(Lifecycle.Event.ON_ANY) + public void onStateChanged(LifecycleOwner provider) { + if (provider.getLifecycle().getCurrentState() == state) { + latch.countDown(); + provider.getLifecycle().removeObserver(this); + } + } + }); } - a.getLifecycle().addObserver(new LifecycleObserver() { - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - public void onStateChanged(LifecycleOwner provider) { - latch.countDown(); - provider.getLifecycle().removeObserver(this); - } - }); }); - latch.await(); + boolean latchResult = latch.await(1, TimeUnit.MINUTES); + assertThat("expected " + state + " never happened. Current state:" + + owner.getLifecycle().getCurrentState(), latchResult, is(true)); + + // wait for another loop to ensure all observers are called + activityRule.runOnUiThread(() -> { + // do nothing + }); } + @SafeVarargs + static <T> List<T> flatMap(List<T>... items) { + ArrayList<T> result = new ArrayList<>(); + for (List<T> item : items) { + result.addAll(item); + } + return result; + } + + /** + * Event tuples of {@link TestEvent} and {@link Lifecycle.Event} + * in the order they should arrive. + */ + @SuppressWarnings("unchecked") + static class OrderedTuples { + static final List<Pair<TestEvent, Lifecycle.Event>> CREATE = + Arrays.asList(new Pair(OWNER_CALLBACK, ON_CREATE), + new Pair(LIFECYCLE_EVENT, ON_CREATE)); + static final List<Pair<TestEvent, Lifecycle.Event>> START = + Arrays.asList(new Pair(OWNER_CALLBACK, ON_START), + new Pair(LIFECYCLE_EVENT, ON_START)); + static final List<Pair<TestEvent, Lifecycle.Event>> RESUME = + Arrays.asList(new Pair(OWNER_CALLBACK, ON_RESUME), + new Pair(LIFECYCLE_EVENT, ON_RESUME)); + static final List<Pair<TestEvent, Lifecycle.Event>> PAUSE = + Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_PAUSE), + new Pair(OWNER_CALLBACK, ON_PAUSE)); + static final List<Pair<TestEvent, Lifecycle.Event>> STOP = + Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_STOP), + new Pair(OWNER_CALLBACK, ON_STOP)); + static final List<Pair<TestEvent, Lifecycle.Event>> DESTROY = + Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_DESTROY), + new Pair(OWNER_CALLBACK, ON_DESTROY)); + } } diff --git a/android/arch/lifecycle/Transformations.java b/android/arch/lifecycle/Transformations.java index 9ce9cbb7..c735f8ba 100644 --- a/android/arch/lifecycle/Transformations.java +++ b/android/arch/lifecycle/Transformations.java @@ -18,6 +18,7 @@ package android.arch.lifecycle; import android.arch.core.util.Function; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; /** @@ -60,7 +61,8 @@ public class Transformations { * @return a LiveData which emits resulting values */ @MainThread - public static <X, Y> LiveData<Y> map(LiveData<X> source, final Function<X, Y> func) { + public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source, + @NonNull final Function<X, Y> func) { final MediatorLiveData<Y> result = new MediatorLiveData<>(); result.addSource(source, new Observer<X>() { @Override @@ -120,8 +122,8 @@ public class Transformations { * @param <Y> a type of resulting LiveData */ @MainThread - public static <X, Y> LiveData<Y> switchMap(LiveData<X> trigger, - final Function<X, LiveData<Y>> func) { + public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger, + @NonNull final Function<X, LiveData<Y>> func) { final MediatorLiveData<Y> result = new MediatorLiveData<>(); result.addSource(trigger, new Observer<X>() { LiveData<Y> mSource; diff --git a/android/arch/lifecycle/ViewModelProvider.java b/android/arch/lifecycle/ViewModelProvider.java index 7ef591f3..29cbab8e 100644 --- a/android/arch/lifecycle/ViewModelProvider.java +++ b/android/arch/lifecycle/ViewModelProvider.java @@ -43,7 +43,8 @@ public class ViewModelProvider { * @param <T> The type parameter for the ViewModel. * @return a newly created ViewModel */ - <T extends ViewModel> T create(Class<T> modelClass); + @NonNull + <T extends ViewModel> T create(@NonNull Class<T> modelClass); } private final Factory mFactory; @@ -70,7 +71,7 @@ public class ViewModelProvider { * @param factory factory a {@code Factory} which will be used to instantiate * new {@code ViewModels} */ - public ViewModelProvider(ViewModelStore store, Factory factory) { + public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) { mFactory = factory; this.mViewModelStore = store; } @@ -88,7 +89,8 @@ public class ViewModelProvider { * @param <T> The type parameter for the ViewModel. * @return A ViewModel that is an instance of the given type {@code T}. */ - public <T extends ViewModel> T get(Class<T> modelClass) { + @NonNull + public <T extends ViewModel> T get(@NonNull Class<T> modelClass) { String canonicalName = modelClass.getCanonicalName(); if (canonicalName == null) { throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels"); @@ -136,8 +138,9 @@ public class ViewModelProvider { */ public static class NewInstanceFactory implements Factory { + @NonNull @Override - public <T extends ViewModel> T create(Class<T> modelClass) { + public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { //noinspection TryWithIdenticalCatches try { return modelClass.newInstance(); diff --git a/android/arch/lifecycle/ViewModelProviders.java b/android/arch/lifecycle/ViewModelProviders.java index 746162a9..b4b20aa4 100644 --- a/android/arch/lifecycle/ViewModelProviders.java +++ b/android/arch/lifecycle/ViewModelProviders.java @@ -139,8 +139,9 @@ public class ViewModelProviders { mApplication = application; } + @NonNull @Override - public <T extends ViewModel> T create(Class<T> modelClass) { + public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { if (AndroidViewModel.class.isAssignableFrom(modelClass)) { //noinspection TryWithIdenticalCatches try { diff --git a/android/arch/lifecycle/ViewModelStoreOwner.java b/android/arch/lifecycle/ViewModelStoreOwner.java index 50583056..e26fa325 100644 --- a/android/arch/lifecycle/ViewModelStoreOwner.java +++ b/android/arch/lifecycle/ViewModelStoreOwner.java @@ -16,6 +16,8 @@ package android.arch.lifecycle; +import android.support.annotation.NonNull; + /** * A scope that owns {@link ViewModelStore}. * <p> @@ -30,5 +32,6 @@ public interface ViewModelStoreOwner { * * @return a {@code ViewModelStore} */ + @NonNull ViewModelStore getViewModelStore(); } diff --git a/android/arch/lifecycle/ViewModelStores.java b/android/arch/lifecycle/ViewModelStores.java index 8c17dd98..d7d769d6 100644 --- a/android/arch/lifecycle/ViewModelStores.java +++ b/android/arch/lifecycle/ViewModelStores.java @@ -19,6 +19,7 @@ package android.arch.lifecycle; import static android.arch.lifecycle.HolderFragment.holderFragmentFor; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; @@ -38,7 +39,7 @@ public class ViewModelStores { * @return a {@code ViewModelStore} */ @MainThread - public static ViewModelStore of(FragmentActivity activity) { + public static ViewModelStore of(@NonNull FragmentActivity activity) { return holderFragmentFor(activity).getViewModelStore(); } @@ -49,7 +50,7 @@ public class ViewModelStores { * @return a {@code ViewModelStore} */ @MainThread - public static ViewModelStore of(Fragment fragment) { + public static ViewModelStore of(@NonNull Fragment fragment) { return holderFragmentFor(fragment).getViewModelStore(); } } diff --git a/android/arch/lifecycle/testapp/CollectingActivity.java b/android/arch/lifecycle/testapp/CollectingLifecycleOwner.java index 6e243b6c..4213cab9 100644 --- a/android/arch/lifecycle/testapp/CollectingActivity.java +++ b/android/arch/lifecycle/testapp/CollectingLifecycleOwner.java @@ -17,21 +17,20 @@ package android.arch.lifecycle.testapp; import android.arch.lifecycle.Lifecycle; -import android.util.Pair; +import android.arch.lifecycle.LifecycleOwner; +import android.support.v4.util.Pair; import java.util.List; /** * For activities that collect their events. */ -public interface CollectingActivity { - long TIMEOUT = 5; - +public interface CollectingLifecycleOwner extends LifecycleOwner { /** - * Return collected events + * Return a copy of currently collected events * * @return The list of collected events. * @throws InterruptedException */ - List<Pair<TestEvent, Lifecycle.Event>> waitForCollectedEvents() throws InterruptedException; + List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents(); } diff --git a/android/arch/lifecycle/testapp/CollectingSupportActivity.java b/android/arch/lifecycle/testapp/CollectingSupportActivity.java new file mode 100644 index 00000000..f38d4224 --- /dev/null +++ b/android/arch/lifecycle/testapp/CollectingSupportActivity.java @@ -0,0 +1,113 @@ +/* + * 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.arch.lifecycle.testapp; + +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; + +import android.arch.lifecycle.Lifecycle.Event; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.util.Pair; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * LifecycleRegistryOwner that extends FragmentActivity. + */ +public class CollectingSupportActivity extends FragmentActivity implements + CollectingLifecycleOwner { + + private final List<Pair<TestEvent, Event>> mCollectedEvents = new ArrayList<>(); + private TestObserver mTestObserver = new TestObserver(mCollectedEvents); + private CountDownLatch mSavedStateLatch = new CountDownLatch(1); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FrameLayout layout = new FrameLayout(this); + layout.setId(R.id.fragment_container); + setContentView(layout); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_CREATE)); + getLifecycle().addObserver(mTestObserver); + } + + /** + * replaces the main content fragment w/ the given fragment. + */ + public void replaceFragment(Fragment fragment) { + getSupportFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, fragment) + .commitNow(); + } + + @Override + protected void onStart() { + super.onStart(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_START)); + } + + @Override + protected void onResume() { + super.onResume(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_RESUME)); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_DESTROY)); + } + + @Override + protected void onStop() { + super.onStop(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_STOP)); + } + + @Override + protected void onPause() { + super.onPause(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_PAUSE)); + // helps with less flaky API 16 tests. + overridePendingTransition(0, 0); + } + + @Override + public List<Pair<TestEvent, Event>> copyCollectedEvents() { + return new ArrayList<>(mCollectedEvents); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mSavedStateLatch.countDown(); + } + + /** + * Waits for onSaveInstanceState to be called. + */ + public boolean waitForStateSave(@SuppressWarnings("SameParameterValue") int seconds) + throws InterruptedException { + return mSavedStateLatch.await(seconds, TimeUnit.SECONDS); + } +} diff --git a/android/arch/lifecycle/testapp/CollectingSupportFragment.java b/android/arch/lifecycle/testapp/CollectingSupportFragment.java new file mode 100644 index 00000000..9bbbe165 --- /dev/null +++ b/android/arch/lifecycle/testapp/CollectingSupportFragment.java @@ -0,0 +1,104 @@ +/* + * 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.arch.lifecycle.testapp; + +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; + +import android.annotation.SuppressLint; +import android.arch.lifecycle.Lifecycle; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.List; + +/** + * A support fragment that collects all of its events. + */ +@SuppressLint("ValidFragment") +public class CollectingSupportFragment extends Fragment implements CollectingLifecycleOwner { + private final List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents = + new ArrayList<>(); + private TestObserver mTestObserver = new TestObserver(mCollectedEvents); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_CREATE)); + getLifecycle().addObserver(mTestObserver); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + //noinspection ConstantConditions + FrameLayout layout = new FrameLayout(container.getContext()); + layout.setId(R.id.child_fragment_container); + return layout; + } + + /** + * Runs a replace fragment transaction with 'fragment' on this Fragment. + */ + public void replaceFragment(Fragment fragment) { + getChildFragmentManager() + .beginTransaction() + .add(R.id.child_fragment_container, fragment) + .commitNow(); + } + + @Override + public void onStart() { + super.onStart(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_START)); + } + + @Override + public void onResume() { + super.onResume(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_RESUME)); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_DESTROY)); + } + + @Override + public void onStop() { + super.onStop(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_STOP)); + } + + @Override + public void onPause() { + super.onPause(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_PAUSE)); + } + + @Override + public List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents() { + return new ArrayList<>(mCollectedEvents); + } +} diff --git a/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java b/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java index d8f4fb39..cdf577c1 100644 --- a/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java +++ b/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java @@ -16,27 +16,29 @@ package android.arch.lifecycle.testapp; -import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; import android.app.Activity; import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleRegistry; import android.arch.lifecycle.LifecycleRegistryOwner; import android.os.Bundle; -import android.util.Pair; +import android.support.annotation.NonNull; +import android.support.v4.util.Pair; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; /** * LifecycleRegistryOwner that extends framework activity. */ +@SuppressWarnings("deprecation") public class FrameworkLifecycleRegistryActivity extends Activity implements - LifecycleRegistryOwner, CollectingActivity { + LifecycleRegistryOwner, CollectingLifecycleOwner { private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); + @NonNull @Override public LifecycleRegistry getLifecycle() { return mLifecycleRegistry; @@ -49,49 +51,43 @@ public class FrameworkLifecycleRegistryActivity extends Activity implements @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_CREATE)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_CREATE)); getLifecycle().addObserver(mTestObserver); } @Override protected void onStart() { super.onStart(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_START)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_START)); } @Override protected void onResume() { super.onResume(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_RESUME)); - finish(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_RESUME)); } @Override protected void onDestroy() { super.onDestroy(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_DESTROY)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_DESTROY)); mLatch.countDown(); } @Override protected void onStop() { super.onStop(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_STOP)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_STOP)); } @Override protected void onPause() { super.onPause(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_PAUSE)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_PAUSE)); } - /** - * awaits for all events and returns them. - */ @Override - public List<Pair<TestEvent, Lifecycle.Event>> waitForCollectedEvents() - throws InterruptedException { - mLatch.await(TIMEOUT, TimeUnit.SECONDS); - return mCollectedEvents; + public List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents() { + return new ArrayList<>(mCollectedEvents); } } diff --git a/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java b/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java deleted file mode 100644 index 5f33c282..00000000 --- a/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2016 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.arch.lifecycle.testapp; - -import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; - -import android.arch.lifecycle.Lifecycle; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.util.Pair; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * Activity for testing full lifecycle - */ -public class FullLifecycleTestActivity extends FragmentActivity implements CollectingActivity { - - private List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents = new ArrayList<>(); - private TestObserver mTestObserver = new TestObserver(mCollectedEvents); - private CountDownLatch mLatch = new CountDownLatch(1); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_CREATE)); - getLifecycle().addObserver(mTestObserver); - } - - @Override - protected void onStart() { - super.onStart(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_START)); - } - - @Override - protected void onResume() { - super.onResume(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_RESUME)); - finish(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_DESTROY)); - mLatch.countDown(); - } - - @Override - protected void onStop() { - super.onStop(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_STOP)); - } - - @Override - protected void onPause() { - super.onPause(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_PAUSE)); - } - - /** - * awaits for all events and returns them. - */ - @Override - public List<Pair<TestEvent, Lifecycle.Event>> waitForCollectedEvents() - throws InterruptedException { - mLatch.await(TIMEOUT, TimeUnit.SECONDS); - return mCollectedEvents; - } -} diff --git a/android/arch/lifecycle/testapp/MainActivity.java b/android/arch/lifecycle/testapp/MainActivity.java deleted file mode 100644 index b9d59142..00000000 --- a/android/arch/lifecycle/testapp/MainActivity.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2016 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.arch.lifecycle.testapp; - -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; - -/** - * Simple test activity - */ -public class MainActivity extends FragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity); - } -} diff --git a/android/arch/lifecycle/testapp/NavigationDialogActivity.java b/android/arch/lifecycle/testapp/NavigationDialogActivity.java index 0ae94033..7d53528f 100644 --- a/android/arch/lifecycle/testapp/NavigationDialogActivity.java +++ b/android/arch/lifecycle/testapp/NavigationDialogActivity.java @@ -22,4 +22,10 @@ import android.support.v4.app.FragmentActivity; * an activity with Dialog theme. */ public class NavigationDialogActivity extends FragmentActivity { + @Override + protected void onPause() { + super.onPause(); + // helps with less flaky API 16 tests + overridePendingTransition(0, 0); + } } diff --git a/android/arch/lifecycle/testapp/NonSupportActivity.java b/android/arch/lifecycle/testapp/NonSupportActivity.java new file mode 100644 index 00000000..835d846a --- /dev/null +++ b/android/arch/lifecycle/testapp/NonSupportActivity.java @@ -0,0 +1,85 @@ +/* + * 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.arch.lifecycle.testapp; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Activity which doesn't extend FragmentActivity, to test ProcessLifecycleOwner because it + * should work anyway. + */ +public class NonSupportActivity extends Activity { + + private static final int TIMEOUT = 1; //secs + private final Lock mLock = new ReentrantLock(); + private Condition mIsResumedCondition = mLock.newCondition(); + private boolean mIsResumed = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onResume() { + super.onResume(); + mLock.lock(); + try { + mIsResumed = true; + mIsResumedCondition.signalAll(); + } finally { + mLock.unlock(); + } + } + + @Override + protected void onPause() { + super.onPause(); + mLock.lock(); + try { + mIsResumed = false; + } finally { + mLock.unlock(); + } + } + + /** + * awaits resumed state + * @return + * @throws InterruptedException + */ + public boolean awaitResumedState() throws InterruptedException { + mLock.lock(); + try { + while (!mIsResumed) { + if (!mIsResumedCondition.await(TIMEOUT, TimeUnit.SECONDS)) { + return false; + } + } + return true; + } finally { + mLock.unlock(); + } + } +} diff --git a/android/arch/lifecycle/testapp/SupportLifecycleRegistryActivity.java b/android/arch/lifecycle/testapp/SupportLifecycleRegistryActivity.java deleted file mode 100644 index c46c6d3e..00000000 --- a/android/arch/lifecycle/testapp/SupportLifecycleRegistryActivity.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.lifecycle.testapp; - -import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; - -import android.arch.lifecycle.Lifecycle.Event; -import android.arch.lifecycle.LifecycleRegistry; -import android.arch.lifecycle.LifecycleRegistryOwner; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.util.Pair; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * LifecycleRegistryOwner that extends FragmentActivity. - */ -public class SupportLifecycleRegistryActivity extends FragmentActivity implements - LifecycleRegistryOwner, CollectingActivity { - private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); - @Override - public LifecycleRegistry getLifecycle() { - return mLifecycleRegistry; - } - - private List<Pair<TestEvent, Event>> mCollectedEvents = new ArrayList<>(); - private TestObserver mTestObserver = new TestObserver(mCollectedEvents); - private CountDownLatch mLatch = new CountDownLatch(1); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_CREATE)); - getLifecycle().addObserver(mTestObserver); - } - - @Override - protected void onStart() { - super.onStart(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_START)); - } - - @Override - protected void onResume() { - super.onResume(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_RESUME)); - finish(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_DESTROY)); - mLatch.countDown(); - } - - @Override - protected void onStop() { - super.onStop(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_STOP)); - } - - @Override - protected void onPause() { - super.onPause(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_PAUSE)); - } - - /** - * awaits for all events and returns them. - */ - @Override - public List<Pair<TestEvent, Event>> waitForCollectedEvents() throws InterruptedException { - mLatch.await(TIMEOUT, TimeUnit.SECONDS); - return mCollectedEvents; - } -} diff --git a/android/arch/lifecycle/testapp/TestEvent.java b/android/arch/lifecycle/testapp/TestEvent.java index 0929f84a..788045a2 100644 --- a/android/arch/lifecycle/testapp/TestEvent.java +++ b/android/arch/lifecycle/testapp/TestEvent.java @@ -17,6 +17,6 @@ package android.arch.lifecycle.testapp; public enum TestEvent { - ACTIVITY_CALLBACK, - LIFECYCLE_EVENT + OWNER_CALLBACK, + LIFECYCLE_EVENT, } diff --git a/android/arch/lifecycle/testapp/TestObserver.java b/android/arch/lifecycle/testapp/TestObserver.java index c6112396..00b8e16d 100644 --- a/android/arch/lifecycle/testapp/TestObserver.java +++ b/android/arch/lifecycle/testapp/TestObserver.java @@ -28,7 +28,7 @@ import android.arch.lifecycle.Lifecycle.Event; import android.arch.lifecycle.LifecycleObserver; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.OnLifecycleEvent; -import android.util.Pair; +import android.support.v4.util.Pair; import java.util.List; diff --git a/android/arch/paging/BoundedDataSource.java b/android/arch/paging/BoundedDataSource.java index 664ab16c..06564907 100644 --- a/android/arch/paging/BoundedDataSource.java +++ b/android/arch/paging/BoundedDataSource.java @@ -21,7 +21,6 @@ import android.support.annotation.RestrictTo; import android.support.annotation.WorkerThread; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -75,7 +74,6 @@ public abstract class BoundedDataSource<Value> extends PositionalDataSource<Valu if (result.size() != loadSize) { throw new IllegalStateException("invalid number of items returned."); } - Collections.reverse(result); } return result; } diff --git a/android/arch/paging/ContiguousDataSource.java b/android/arch/paging/ContiguousDataSource.java index afcc208c..be9da200 100644 --- a/android/arch/paging/ContiguousDataSource.java +++ b/android/arch/paging/ContiguousDataSource.java @@ -26,21 +26,65 @@ import java.util.List; /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> { - /** - * Number of items that this DataSource can provide in total, or COUNT_UNDEFINED. - * - * @return number of items that this DataSource can provide in total, or COUNT_UNDEFINED - * if difficult or undesired to compute. - */ - public int countItems() { - return COUNT_UNDEFINED; - } - @Override boolean isContiguous() { return true; } + void loadInitial(Key key, int pageSize, boolean enablePlaceholders, + PageResult.Receiver<Key, Value> receiver) { + NullPaddedList<Value> initial = loadInitial(key, pageSize, enablePlaceholders); + if (initial != null) { + receiver.onPageResult(new PageResult<>( + PageResult.INIT, + new Page<Key, Value>(initial.mList), + initial.getLeadingNullCount(), + initial.getTrailingNullCount(), + initial.getPositionOffset())); + } else { + receiver.onPageResult(new PageResult<Key, Value>( + PageResult.INIT, null, 0, 0, 0)); + } + } + + void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, + PageResult.Receiver<Key, Value> receiver) { + List<Value> list = loadAfter(currentEndIndex, currentEndItem, pageSize); + + Page<Key, Value> page = list != null + ? new Page<Key, Value>(list) : null; + + receiver.postOnPageResult(new PageResult<>( + PageResult.APPEND, page, 0, 0, 0)); + } + + void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize, + PageResult.Receiver<Key, Value> receiver) { + List<Value> list = loadBefore(currentBeginIndex, currentBeginItem, pageSize); + + Page<Key, Value> page = list != null + ? new Page<Key, Value>(list) : null; + + receiver.postOnPageResult(new PageResult<>( + PageResult.PREPEND, page, 0, 0, 0)); + } + + /** + * Get the key from either the position, or item, or null if position/item invalid. + * <p> + * Position may not match passed item's position - if trying to query the key from a position + * that isn't yet loaded, a fallback item (last loaded item accessed) will be passed. + */ + abstract Key getKey(int position, Value item); + + @Nullable + abstract List<Value> loadAfterImpl(int currentEndIndex, + @NonNull Value currentEndItem, int pageSize); + + @Nullable + abstract List<Value> loadBeforeImpl(int currentBeginIndex, + @NonNull Value currentBeginItem, int pageSize); + /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @@ -48,21 +92,7 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V public abstract NullPaddedList<Value> loadInitial( Key key, int initialLoadSize, boolean enablePlaceholders); - /** - * Load data after the given position / item. - * <p> - * It's valid to return a different list size than the page size, if it's easier for this data - * source. It is generally safer to increase number loaded than reduce. - * - * @param currentEndIndex Load items after this index, starting with currentEndIndex + 1. - * @param currentEndItem Load items after this item, can be used for precise querying based on - * item contents. - * @param pageSize Suggested number of items to load. - * @return List of items, starting at position currentEndIndex + 1. Null if the data source is - * no longer valid, and should not be queried again. - * - * @hide - */ + /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable @@ -78,24 +108,7 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V return list; } - @Nullable - abstract List<Value> loadAfterImpl(int currentEndIndex, - @NonNull Value currentEndItem, int pageSize); - - /** - * Load data before the given position / item. - * <p> - * It's valid to return a different list size than the page size, if it's easier for this data - * source. It is generally safer to increase number loaded than reduce. - * - * @param currentBeginIndex Load items before this index, starting with currentBeginIndex - 1. - * @param currentBeginItem Load items after this item, can be used for precise querying based - * on item contents. - * @param pageSize Suggested number of items to load. - * @return List of items, in descending order, starting at position currentBeginIndex - 1. - * - * @hide - */ + /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable @@ -111,15 +124,4 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V return list; } - - @Nullable - abstract List<Value> loadBeforeImpl(int currentBeginIndex, - @NonNull Value currentBeginItem, int pageSize); - - /** - * Get the key from either the position, or item. Position may not match passed item's position, - * if trying to query the key from a position that isn't yet loaded, so a fallback item must be - * used. - */ - abstract Key getKey(int position, Value item); } diff --git a/android/arch/paging/ContiguousDiffHelperTest.java b/android/arch/paging/ContiguousDiffHelperTest.java deleted file mode 100644 index 4f221b34..00000000 --- a/android/arch/paging/ContiguousDiffHelperTest.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; - -import android.support.annotation.NonNull; -import android.support.test.filters.SmallTest; -import android.support.v7.recyclerview.extensions.DiffCallback; -import android.support.v7.util.DiffUtil; -import android.support.v7.util.ListUpdateCallback; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.Mockito; - -@SmallTest -@RunWith(JUnit4.class) -public class ContiguousDiffHelperTest { - private interface CallbackValidator { - void validate(ListUpdateCallback callback); - } - - private static final DiffCallback<String> DIFF_CALLBACK = new DiffCallback<String>() { - @Override - public boolean areItemsTheSame(@NonNull String oldItem, @NonNull String newItem) { - // first char means same item - return oldItem.charAt(0) == newItem.charAt(0); - } - - @Override - public boolean areContentsTheSame(@NonNull String oldItem, @NonNull String newItem) { - return oldItem.equals(newItem); - } - }; - - private void validateTwoListDiff(StringPagedList oldList, StringPagedList newList, - CallbackValidator callbackValidator) { - DiffUtil.DiffResult diffResult = ContiguousDiffHelper.computeDiff(oldList, newList, - DIFF_CALLBACK, false); - - ListUpdateCallback listUpdateCallback = Mockito.mock(ListUpdateCallback.class); - ContiguousDiffHelper.dispatchDiff(listUpdateCallback, oldList, newList, diffResult); - - callbackValidator.validate(listUpdateCallback); - } - - @Test - public void sameListNoUpdates() { - validateTwoListDiff( - new StringPagedList(5, 5, "a", "b", "c"), - new StringPagedList(5, 5, "a", "b", "c"), - new CallbackValidator() { - @Override - public void validate(ListUpdateCallback callback) { - verifyZeroInteractions(callback); - } - } - ); - } - - @Test - public void appendFill() { - validateTwoListDiff( - new StringPagedList(5, 5, "a", "b"), - new StringPagedList(5, 4, "a", "b", "c"), - new CallbackValidator() { - @Override - public void validate(ListUpdateCallback callback) { - verify(callback).onRemoved(11, 1); - verify(callback).onInserted(7, 1); - // NOTE: ideally would be onChanged(7, 1, null) - verifyNoMoreInteractions(callback); - } - } - ); - } - - @Test - public void prependFill() { - validateTwoListDiff( - new StringPagedList(5, 5, "b", "c"), - new StringPagedList(4, 5, "a", "b", "c"), - new CallbackValidator() { - @Override - public void validate(ListUpdateCallback callback) { - verify(callback).onRemoved(0, 1); - verify(callback).onInserted(4, 1); - //NOTE: ideally would be onChanged(4, 1, null); - verifyNoMoreInteractions(callback); - } - } - ); - } - - @Test - public void change() { - validateTwoListDiff( - new StringPagedList(5, 5, "a1", "b1", "c1"), - new StringPagedList(5, 5, "a2", "b1", "c2"), - new CallbackValidator() { - @Override - public void validate(ListUpdateCallback callback) { - verify(callback).onChanged(5, 1, null); - verify(callback).onChanged(7, 1, null); - verifyNoMoreInteractions(callback); - } - } - ); - } -} diff --git a/android/arch/paging/ContiguousPagedList.java b/android/arch/paging/ContiguousPagedList.java index d8907c3b..2a5cd42f 100644 --- a/android/arch/paging/ContiguousPagedList.java +++ b/android/arch/paging/ContiguousPagedList.java @@ -16,101 +16,136 @@ package android.arch.paging; +import android.support.annotation.AnyThread; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.RestrictTo; -import android.support.annotation.WorkerThread; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; - -/** @hide */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class ContiguousPagedList<T> extends NullPaddedList<T> { - - private final ContiguousDataSource<?, T> mDataSource; - private final Executor mMainThreadExecutor; - private final Executor mBackgroundThreadExecutor; - private final Config mConfig; +class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Callback { + private final ContiguousDataSource<K, V> mDataSource; private boolean mPrependWorkerRunning = false; private boolean mAppendWorkerRunning = false; private int mPrependItemsRequested = 0; private int mAppendItemsRequested = 0; - private int mLastLoad = 0; - private T mLastItem = null; + @SuppressWarnings("unchecked") + private final PagedStorage<K, V> mKeyedStorage = (PagedStorage<K, V>) mStorage; + + private final PageResult.Receiver<K, V> mReceiver = new PageResult.Receiver<K, V>() { + @AnyThread + @Override + public void postOnPageResult(@NonNull final PageResult<K, V> pageResult) { + // NOTE: if we're already on main thread, this can delay page receive by a frame + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + onPageResult(pageResult); + } + }); + } - private AtomicBoolean mDetached = new AtomicBoolean(false); + @MainThread + @Override + public void onPageResult(@NonNull PageResult<K, V> pageResult) { + if (pageResult.page == null) { + detach(); + return; + } - private ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>(); + if (isDetached()) { + // No op, have detached + return; + } - @WorkerThread - <K> ContiguousPagedList(@NonNull ContiguousDataSource<K, T> dataSource, + Page<K, V> page = pageResult.page; + if (pageResult.type == PageResult.INIT) { + mKeyedStorage.init(pageResult.leadingNulls, page, pageResult.trailingNulls, + pageResult.positionOffset, ContiguousPagedList.this); + notifyInserted(0, mKeyedStorage.size()); + } else if (pageResult.type == PageResult.APPEND) { + mKeyedStorage.appendPage(page, ContiguousPagedList.this); + } else if (pageResult.type == PageResult.PREPEND) { + mKeyedStorage.prependPage(page, ContiguousPagedList.this); + } + } + }; + + ContiguousPagedList( + @NonNull ContiguousDataSource<K, V> dataSource, @NonNull Executor mainThreadExecutor, @NonNull Executor backgroundThreadExecutor, - Config config, - @Nullable K key) { - super(); - + @NonNull Config config, + final @Nullable K key) { + super(new PagedStorage<K, V>(), mainThreadExecutor, backgroundThreadExecutor, config); mDataSource = dataSource; - mMainThreadExecutor = mainThreadExecutor; - mBackgroundThreadExecutor = backgroundThreadExecutor; - mConfig = config; - NullPaddedList<T> initialState = dataSource.loadInitial( - key, config.mInitialLoadSizeHint, config.mEnablePlaceholders); - - if (initialState != null) { - mPositionOffset = initialState.getPositionOffset(); - - mLeadingNullCount = initialState.getLeadingNullCount(); - mList = new ArrayList<>(initialState.mList); - mTrailingNullCount = initialState.getTrailingNullCount(); - - if (initialState.getLeadingNullCount() == 0 - && initialState.getTrailingNullCount() == 0 - && config.mPrefetchDistance < 1) { - throw new IllegalArgumentException("Null padding is required to support the 0" - + " prefetch case - require either null items or prefetching to fetch" - + " beyond initial load."); - } - if (initialState.size() != 0) { - mLastLoad = mLeadingNullCount + mList.size() / 2; - mLastItem = mList.get(mList.size() / 2); - } - } else { - mList = new ArrayList<>(); - detach(); - } - if (mList.size() == 0) { - // Empty initial state, so don't try and fetch data. - mPrependWorkerRunning = true; - mAppendWorkerRunning = true; - } + // blocking init just triggers the initial load on the construction thread - + // Could still be posted with callback, if desired. + mDataSource.loadInitial(key, + mConfig.initialLoadSizeHint, + mConfig.enablePlaceholders, + mReceiver); } + @MainThread @Override - public T get(int index) { - T item = super.get(index); - if (item != null) { - mLastItem = item; + void dispatchUpdatesSinceSnapshot( + @NonNull PagedList<V> pagedListSnapshot, @NonNull Callback callback) { + + final PagedStorage<?, V> snapshot = pagedListSnapshot.mStorage; + + final int newlyAppended = mStorage.getNumberAppended() - snapshot.getNumberAppended(); + final int newlyPrepended = mStorage.getNumberPrepended() - snapshot.getNumberPrepended(); + + final int previousTrailing = snapshot.getTrailingNullCount(); + final int previousLeading = snapshot.getLeadingNullCount(); + + // Validate that the snapshot looks like a previous version of this list - if it's not, + // we can't be sure we'll dispatch callbacks safely + if (newlyAppended < 0 + || newlyPrepended < 0 + || mStorage.getTrailingNullCount() != Math.max(previousTrailing - newlyAppended, 0) + || mStorage.getLeadingNullCount() != Math.max(previousLeading - newlyPrepended, 0) + || (mStorage.getStorageCount() + != snapshot.getStorageCount() + newlyAppended + newlyPrepended)) { + throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear" + + " to be a snapshot of this PagedList"); + } + + if (newlyAppended != 0) { + final int changedCount = Math.min(previousTrailing, newlyAppended); + final int addedCount = newlyAppended - changedCount; + + final int endPosition = snapshot.getLeadingNullCount() + snapshot.getStorageCount(); + if (changedCount != 0) { + callback.onChanged(endPosition, changedCount); + } + if (addedCount != 0) { + callback.onInserted(endPosition + changedCount, addedCount); + } + } + if (newlyPrepended != 0) { + final int changedCount = Math.min(previousLeading, newlyPrepended); + final int addedCount = newlyPrepended - changedCount; + + if (changedCount != 0) { + callback.onChanged(previousLeading, changedCount); + } + if (addedCount != 0) { + callback.onInserted(0, addedCount); + } } - return item; } + @MainThread @Override - public void loadAround(int index) { - mLastLoad = index + mPositionOffset; - - int prependItems = mConfig.mPrefetchDistance - (index - mLeadingNullCount); - int appendItems = index + mConfig.mPrefetchDistance - (mLeadingNullCount + mList.size()); + protected void loadAroundInternal(int index) { + int prependItems = mConfig.prefetchDistance - (index - mStorage.getLeadingNullCount()); + int appendItems = index + mConfig.prefetchDistance + - (mStorage.getLeadingNullCount() + mStorage.getStorageCount()); mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested); if (mPrependItemsRequested > 0) { @@ -123,21 +158,6 @@ class ContiguousPagedList<T> extends NullPaddedList<T> { } } - @Override - public int getLoadedCount() { - return mList.size(); - } - - @Override - public int getLeadingNullCount() { - return mLeadingNullCount; - } - - @Override - public int getTrailingNullCount() { - return mTrailingNullCount; - } - @MainThread private void schedulePrepend() { if (mPrependWorkerRunning) { @@ -145,29 +165,17 @@ class ContiguousPagedList<T> extends NullPaddedList<T> { } mPrependWorkerRunning = true; - final int position = mLeadingNullCount + mPositionOffset; - final T item = mList.get(0); + final int position = mStorage.getLeadingNullCount() + mStorage.getPositionOffset(); + + // safe to access first item here - mStorage can't be empty if we're prepending + final V item = mStorage.getFirstContiguousItem(); mBackgroundThreadExecutor.execute(new Runnable() { @Override public void run() { - if (mDetached.get()) { + if (isDetached()) { return; } - - final List<T> data = mDataSource.loadBefore(position, item, mConfig.mPageSize); - if (data != null) { - mMainThreadExecutor.execute(new Runnable() { - @Override - public void run() { - if (mDetached.get()) { - return; - } - prependImpl(data); - } - }); - } else { - detach(); - } + mDataSource.loadBefore(position, item, mConfig.pageSize, mReceiver); } }); } @@ -179,56 +187,44 @@ class ContiguousPagedList<T> extends NullPaddedList<T> { } mAppendWorkerRunning = true; - final int position = mLeadingNullCount + mList.size() - 1 + mPositionOffset; - final T item = mList.get(mList.size() - 1); + final int position = mStorage.getLeadingNullCount() + + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset(); + + // safe to access first item here - mStorage can't be empty if we're appending + final V item = mStorage.getLastContiguousItem(); mBackgroundThreadExecutor.execute(new Runnable() { @Override public void run() { - if (mDetached.get()) { + if (isDetached()) { return; } - - final List<T> data = mDataSource.loadAfter(position, item, mConfig.mPageSize); - if (data != null) { - mMainThreadExecutor.execute(new Runnable() { - @Override - public void run() { - if (mDetached.get()) { - return; - } - appendImpl(data); - } - }); - } else { - detach(); - } + mDataSource.loadAfter(position, item, mConfig.pageSize, mReceiver); } }); } - @MainThread - private void prependImpl(List<T> before) { - final int count = before.size(); - if (count == 0) { - // Nothing returned from source, stop loading in this direction - return; - } - - Collections.reverse(before); - mList.addAll(0, before); - - final int changedCount = Math.min(mLeadingNullCount, count); - final int addedCount = count - changedCount; + @Override + boolean isContiguous() { + return true; + } - if (changedCount != 0) { - mLeadingNullCount -= changedCount; - } - mPositionOffset -= addedCount; - mNumberPrepended += count; + @Nullable + @Override + public Object getLastKey() { + return mDataSource.getKey(mLastLoad, mLastItem); + } + @MainThread + @Override + public void onInitialized(int count) { + notifyInserted(0, count); + } - // only try to post more work after fully prepended (with offsets / null counts updated) - mPrependItemsRequested -= count; + @MainThread + @Override + public void onPagePrepended(int leadingNulls, int changedCount, int addedCount) { + // consider whether to post more work, now that a page is fully prepended + mPrependItemsRequested = mPrependItemsRequested - changedCount - addedCount; mPrependWorkerRunning = false; if (mPrependItemsRequested > 0) { // not done prepending, keep going @@ -236,39 +232,16 @@ class ContiguousPagedList<T> extends NullPaddedList<T> { } // finally dispatch callbacks, after prepend may have already been scheduled - for (WeakReference<Callback> weakRef : mCallbacks) { - Callback callback = weakRef.get(); - if (callback != null) { - if (changedCount != 0) { - callback.onChanged(mLeadingNullCount, changedCount); - } - if (addedCount != 0) { - callback.onInserted(0, addedCount); - } - } - } + notifyChanged(leadingNulls, changedCount); + notifyInserted(0, addedCount); } @MainThread - private void appendImpl(List<T> after) { - final int count = after.size(); - if (count == 0) { - // Nothing returned from source, stop loading in this direction - return; - } - - mList.addAll(after); - - final int changedCount = Math.min(mTrailingNullCount, count); - final int addedCount = count - changedCount; - - if (changedCount != 0) { - mTrailingNullCount -= changedCount; - } - mNumberAppended += count; + @Override + public void onPageAppended(int endPosition, int changedCount, int addedCount) { + // consider whether to post more work, now that a page is fully appended - // only try to post more work after fully appended (with null counts updated) - mAppendItemsRequested -= count; + mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount; mAppendWorkerRunning = false; if (mAppendItemsRequested > 0) { // not done appending, keep going @@ -276,100 +249,19 @@ class ContiguousPagedList<T> extends NullPaddedList<T> { } // finally dispatch callbacks, after append may have already been scheduled - for (WeakReference<Callback> weakRef : mCallbacks) { - Callback callback = weakRef.get(); - if (callback != null) { - final int endPosition = mLeadingNullCount + mList.size() - count; - if (changedCount != 0) { - callback.onChanged(endPosition, changedCount); - } - if (addedCount != 0) { - callback.onInserted(endPosition + changedCount, addedCount); - } - } - } - } - - @Override - public boolean isImmutable() { - // TODO: return true if had nulls, and now getLoadedCount() == size(). Is that safe? - // Currently we don't prevent DataSources from returning more items than their null counts - return isDetached(); - } - - @Override - public void addWeakCallback(@Nullable PagedList<T> previousSnapshot, - @NonNull Callback callback) { - NullPaddedList<T> snapshot = (NullPaddedList<T>) previousSnapshot; - if (snapshot != this && snapshot != null) { - final int newlyAppended = mNumberAppended - snapshot.getNumberAppended(); - final int newlyPrepended = mNumberPrepended - snapshot.getNumberPrepended(); - - final int previousTrailing = snapshot.getTrailingNullCount(); - final int previousLeading = snapshot.getLeadingNullCount(); - - // Validate that the snapshot looks like a previous version of this list - if it's not, - // we can't be sure we'll dispatch callbacks safely - if (newlyAppended < 0 - || newlyPrepended < 0 - || mTrailingNullCount != Math.max(previousTrailing - newlyAppended, 0) - || mLeadingNullCount != Math.max(previousLeading - newlyPrepended, 0) - || snapshot.getLoadedCount() + newlyAppended + newlyPrepended != mList.size()) { - throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear" - + " to be a snapshot of this list"); - } - - if (newlyAppended != 0) { - final int changedCount = Math.min(previousTrailing, newlyAppended); - final int addedCount = newlyAppended - changedCount; - - final int endPosition = - snapshot.getLeadingNullCount() + snapshot.getLoadedCount(); - if (changedCount != 0) { - callback.onChanged(endPosition, changedCount); - } - if (addedCount != 0) { - callback.onInserted(endPosition + changedCount, addedCount); - } - } - if (newlyPrepended != 0) { - final int changedCount = Math.min(previousLeading, newlyPrepended); - final int addedCount = newlyPrepended - changedCount; - - if (changedCount != 0) { - callback.onChanged(previousLeading, changedCount); - } - if (addedCount != 0) { - callback.onInserted(0, addedCount); - } - } - } - mCallbacks.add(new WeakReference<>(callback)); - } - - @Override - public void removeWeakCallback(@NonNull Callback callback) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - Callback currentCallback = mCallbacks.get(i).get(); - if (currentCallback == null || currentCallback == callback) { - mCallbacks.remove(i); - } - } + notifyChanged(endPosition, changedCount); + notifyInserted(endPosition + changedCount, addedCount); } + @MainThread @Override - public boolean isDetached() { - return mDetached.get(); + public void onPagePlaceholderInserted(int pageIndex) { + throw new IllegalStateException("Tiled callback on ContiguousPagedList"); } - @SuppressWarnings("WeakerAccess") - public void detach() { - mDetached.set(true); - } - - @Nullable + @MainThread @Override - public Object getLastKey() { - return mDataSource.getKey(mLastLoad, mLastItem); + public void onPageInserted(int start, int count) { + throw new IllegalStateException("Tiled callback on ContiguousPagedList"); } } diff --git a/android/arch/paging/ContiguousPagedListTest.java b/android/arch/paging/ContiguousPagedListTest.java deleted file mode 100644 index ee7ea6a4..00000000 --- a/android/arch/paging/ContiguousPagedListTest.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; - -import android.support.annotation.Nullable; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -@RunWith(Parameterized.class) -public class ContiguousPagedListTest { - - @Parameterized.Parameters(name = "counted:{0}") - public static List<Object[]> parameters() { - return Arrays.asList(new Object[][]{{true}, {false}}); - } - - public ContiguousPagedListTest(boolean counted) { - mCounted = counted; - } - - private final boolean mCounted; - private TestExecutor mMainThread = new TestExecutor(); - private TestExecutor mBackgroundThread = new TestExecutor(); - - private static final ArrayList<Item> ITEMS = new ArrayList<>(); - - static { - for (int i = 0; i < 100; i++) { - ITEMS.add(new Item(i)); - } - } - - @SuppressWarnings("WeakerAccess") - private static class Item { - private Item(int position) { - this.position = position; - this.name = "Item " + position; - } - - public final int position; - public final String name; - - @Override - public String toString() { - return name; - } - } - - private class TestSource extends PositionalDataSource<Item> { - @Override - public int countItems() { - if (mCounted) { - return ITEMS.size(); - } else { - return COUNT_UNDEFINED; - } - } - - private List<Item> getClampedRange(int startInc, int endExc, boolean reverse) { - startInc = Math.max(0, startInc); - endExc = Math.min(ITEMS.size(), endExc); - List<Item> list = ITEMS.subList(startInc, endExc); - if (reverse) { - Collections.reverse(list); - } - return list; - } - - @Nullable - @Override - public List<Item> loadAfter(int startIndex, int pageSize) { - return getClampedRange(startIndex, startIndex + pageSize, false); - } - - @Nullable - @Override - public List<Item> loadBefore(int startIndex, int pageSize) { - return getClampedRange(startIndex - pageSize + 1, startIndex + 1, true); - } - } - - private void verifyRange(int start, int count, NullPaddedList<Item> actual) { - if (mCounted) { - int expectedLeading = start; - int expectedTrailing = ITEMS.size() - start - count; - assertEquals(ITEMS.size(), actual.size()); - assertEquals(ITEMS.size() - expectedLeading - expectedTrailing, - actual.getLoadedCount()); - assertEquals(expectedLeading, actual.getLeadingNullCount()); - assertEquals(expectedTrailing, actual.getTrailingNullCount()); - - for (int i = 0; i < actual.getLoadedCount(); i++) { - assertSame(ITEMS.get(i + start), actual.get(i + start)); - } - } else { - assertEquals(count, actual.size()); - assertEquals(actual.size(), actual.getLoadedCount()); - assertEquals(0, actual.getLeadingNullCount()); - assertEquals(0, actual.getTrailingNullCount()); - - for (int i = 0; i < actual.getLoadedCount(); i++) { - assertSame(ITEMS.get(i + start), actual.get(i)); - } - } - } - - private void verifyCallback(PagedList.Callback callback, int countedPosition, - int uncountedPosition) { - if (mCounted) { - verify(callback).onChanged(countedPosition, 20); - } else { - verify(callback).onInserted(uncountedPosition, 20); - } - } - - @Test - public void initialLoad() { - verifyRange(30, 40, - new TestSource().loadInitial(50, 40, true)); - - verifyRange(0, 10, - new TestSource().loadInitial(5, 10, true)); - - verifyRange(90, 10, - new TestSource().loadInitial(95, 10, true)); - } - - - private ContiguousPagedList<Item> createCountedPagedList( - PagedList.Config config, int initialPosition) { - TestSource source = new TestSource(); - return new ContiguousPagedList<>( - source, mMainThread, mBackgroundThread, - config, - initialPosition); - } - - private ContiguousPagedList<Item> createCountedPagedList(int initialPosition) { - return createCountedPagedList( - new PagedList.Config.Builder() - .setInitialLoadSizeHint(40) - .setPageSize(20) - .setPrefetchDistance(20) - .build(), - initialPosition); - } - - @Test - public void append() { - ContiguousPagedList<Item> pagedList = createCountedPagedList(0); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(0, 40, pagedList); - verifyZeroInteractions(callback); - - pagedList.loadAround(35); - drain(); - - verifyRange(0, 60, pagedList); - verifyCallback(callback, 40, 40); - verifyNoMoreInteractions(callback); - } - - - @Test - public void prepend() { - ContiguousPagedList<Item> pagedList = createCountedPagedList(80); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(60, 40, pagedList); - verifyZeroInteractions(callback); - - pagedList.loadAround(mCounted ? 65 : 5); - drain(); - - verifyRange(40, 60, pagedList); - verifyCallback(callback, 40, 0); - verifyNoMoreInteractions(callback); - } - - @Test - public void outwards() { - ContiguousPagedList<Item> pagedList = createCountedPagedList(50); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(30, 40, pagedList); - verifyZeroInteractions(callback); - - pagedList.loadAround(mCounted ? 65 : 35); - drain(); - - verifyRange(30, 60, pagedList); - verifyCallback(callback, 70, 40); - verifyNoMoreInteractions(callback); - - pagedList.loadAround(mCounted ? 35 : 5); - drain(); - - verifyRange(10, 80, pagedList); - verifyCallback(callback, 10, 0); - verifyNoMoreInteractions(callback); - } - - @Test - public void multiAppend() { - ContiguousPagedList<Item> pagedList = createCountedPagedList(0); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(0, 40, pagedList); - verifyZeroInteractions(callback); - - pagedList.loadAround(55); - drain(); - - verifyRange(0, 80, pagedList); - verifyCallback(callback, 40, 40); - verifyCallback(callback, 60, 60); - verifyNoMoreInteractions(callback); - } - - @Test - public void distantPrefetch() { - ContiguousPagedList<Item> pagedList = createCountedPagedList( - new PagedList.Config.Builder() - .setInitialLoadSizeHint(10) - .setPageSize(10) - .setPrefetchDistance(30) - .build(), - 0); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(0, 10, pagedList); - verifyZeroInteractions(callback); - - pagedList.loadAround(5); - drain(); - - verifyRange(0, 40, pagedList); - - pagedList.loadAround(6); - drain(); - - // although our prefetch window moves forward, no new load triggered - verifyRange(0, 40, pagedList); - } - - @Test - public void appendCallbackAddedLate() { - ContiguousPagedList<Item> pagedList = createCountedPagedList(0); - verifyRange(0, 40, pagedList); - - pagedList.loadAround(35); - drain(); - verifyRange(0, 60, pagedList); - - // snapshot at 60 items - NullPaddedList<Item> snapshot = (NullPaddedList<Item>) pagedList.snapshot(); - verifyRange(0, 60, snapshot); - - - pagedList.loadAround(55); - drain(); - verifyRange(0, 80, pagedList); - verifyRange(0, 60, snapshot); - - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(snapshot, callback); - verifyCallback(callback, 60, 60); - verifyNoMoreInteractions(callback); - } - - - @Test - public void prependCallbackAddedLate() { - ContiguousPagedList<Item> pagedList = createCountedPagedList(80); - verifyRange(60, 40, pagedList); - - pagedList.loadAround(mCounted ? 65 : 5); - drain(); - verifyRange(40, 60, pagedList); - - // snapshot at 60 items - NullPaddedList<Item> snapshot = (NullPaddedList<Item>) pagedList.snapshot(); - verifyRange(40, 60, snapshot); - - - pagedList.loadAround(mCounted ? 45 : 5); - drain(); - verifyRange(20, 80, pagedList); - verifyRange(40, 60, snapshot); - - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(snapshot, callback); - verifyCallback(callback, 40, 0); - verifyNoMoreInteractions(callback); - } - - private void drain() { - boolean executed; - do { - executed = mBackgroundThread.executeAll(); - executed |= mMainThread.executeAll(); - } while (executed); - } -} diff --git a/android/arch/paging/DataSource.java b/android/arch/paging/DataSource.java index 48fbec5f..524e570a 100644 --- a/android/arch/paging/DataSource.java +++ b/android/arch/paging/DataSource.java @@ -17,6 +17,7 @@ package android.arch.paging; import android.support.annotation.AnyThread; +import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; import java.util.concurrent.CopyOnWriteArrayList; @@ -60,15 +61,6 @@ public abstract class DataSource<Key, Value> { public static int COUNT_UNDEFINED = -1; /** - * Number of items that this DataSource can provide in total, or {@link #COUNT_UNDEFINED}. - * - * @return number of items that this DataSource can provide in total, or - * {@link #COUNT_UNDEFINED} if expensive or undesired to compute. - */ - @WorkerThread - public abstract int countItems(); - - /** * Returns true if the data source guaranteed to produce a contiguous set of items, * never producing gaps. */ @@ -111,7 +103,7 @@ public abstract class DataSource<Key, Value> { */ @AnyThread @SuppressWarnings("WeakerAccess") - public void addInvalidatedCallback(InvalidatedCallback onInvalidatedCallback) { + public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { mOnInvalidatedCallbacks.add(onInvalidatedCallback); } @@ -122,7 +114,7 @@ public abstract class DataSource<Key, Value> { */ @AnyThread @SuppressWarnings("WeakerAccess") - public void removeInvalidatedCallback(InvalidatedCallback onInvalidatedCallback) { + public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) { mOnInvalidatedCallbacks.remove(onInvalidatedCallback); } diff --git a/android/arch/paging/KeyedDataSource.java b/android/arch/paging/KeyedDataSource.java index 8cf6829c..0d452946 100644 --- a/android/arch/paging/KeyedDataSource.java +++ b/android/arch/paging/KeyedDataSource.java @@ -103,10 +103,6 @@ import java.util.List; * @param <Value> Type of items being loaded by the DataSource. */ public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> { - @Override - public final int countItems() { - return 0; // method not called, can't be overridden - } @Nullable @Override @@ -118,7 +114,14 @@ public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<K @Override List<Value> loadBeforeImpl( int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize) { - return loadBefore(getKey(currentBeginItem), pageSize); + List<Value> list = loadBefore(getKey(currentBeginItem), pageSize); + + if (list != null && list.size() > 1) { + // TODO: move out of keyed entirely, into the DB DataSource. + list = new ArrayList<>(list); + Collections.reverse(list); + } + return list; } @Nullable @@ -191,6 +194,8 @@ public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<K /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @WorkerThread + @Override public NullPaddedList<Value> loadInitial( @Nullable Key key, int initialLoadSize, boolean enablePlaceholders) { if (isInvalid()) { diff --git a/android/arch/paging/ListDataSource.java b/android/arch/paging/ListDataSource.java deleted file mode 100644 index f3e83d03..00000000 --- a/android/arch/paging/ListDataSource.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import java.util.List; - -public class ListDataSource<T> extends TiledDataSource<T> { - private List<T> mList; - - ListDataSource(List<T> data) { - mList = data; - } - - @Override - public int countItems() { - return mList.size(); - } - - @Override - public List<T> loadRange(int startPosition, int count) { - int endExclusive = Math.min(mList.size(), startPosition + count); - return mList.subList(startPosition, endExclusive); - } -} diff --git a/android/arch/paging/LivePagedListProvider.java b/android/arch/paging/LivePagedListProvider.java index b7c68dd6..07dd84bf 100644 --- a/android/arch/paging/LivePagedListProvider.java +++ b/android/arch/paging/LivePagedListProvider.java @@ -16,5 +16,133 @@ package android.arch.paging; -abstract public class LivePagedListProvider<K, T> { -}
\ No newline at end of file +import android.arch.core.executor.ArchTaskExecutor; +import android.arch.lifecycle.ComputableLiveData; +import android.arch.lifecycle.LiveData; +import android.support.annotation.AnyThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +/** + * Provides a {@code LiveData<PagedList>}, given a means to construct a DataSource. + * <p> + * Return type for data-loading system of an application or library to produce a + * {@code LiveData<PagedList>}, while leaving the details of the paging mechanism up to the + * consumer. + * <p> + * If you're using Room, it can generate a LivePagedListProvider from a query: + * <pre> + * {@literal @}Dao + * interface UserDao { + * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC") + * public abstract LivePagedListProvider<Integer, User> usersByLastName(); + * }</pre> + * In the above sample, {@code Integer} is used because it is the {@code Key} type of + * {@link TiledDataSource}. Currently, Room can only generate a {@code LIMIT}/{@code OFFSET}, + * position based loader that uses TiledDataSource under the hood, and specifying {@code Integer} + * here lets you pass an initial loading position as an integer. + * <p> + * In the future, Room plans to offer other key types to support paging content with a + * {@link KeyedDataSource}. + * + * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if + * you're using TiledDataSource. + * @param <Value> Data type produced by the DataSource, and held by the PagedLists. + * + * @see PagedListAdapter + * @see DataSource + * @see PagedList + */ +public abstract class LivePagedListProvider<Key, Value> { + + /** + * Construct a new data source to be wrapped in a new PagedList, which will be returned + * through the LiveData. + * + * @return The data source. + */ + @WorkerThread + protected abstract DataSource<Key, Value> createDataSource(); + + /** + * Creates a LiveData of PagedLists, given the page size. + * <p> + * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a + * {@link android.support.v7.widget.RecyclerView}. + * + * @param initialLoadKey Initial key used to load initial data from the data source. + * @param pageSize Page size defining how many items are loaded from a data source at a time. + * Recommended to be multiple times the size of item displayed at once. + * + * @return The LiveData of PagedLists. + */ + @AnyThread + @NonNull + public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey, int pageSize) { + return create(initialLoadKey, + new PagedList.Config.Builder() + .setPageSize(pageSize) + .build()); + } + + /** + * Creates a LiveData of PagedLists, given the PagedList.Config. + * <p> + * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a + * {@link android.support.v7.widget.RecyclerView}. + * + * @param initialLoadKey Initial key to pass to the data source to initialize data with. + * @param config PagedList.Config to use with created PagedLists. This specifies how the + * lists will load data. + * + * @return The LiveData of PagedLists. + */ + @AnyThread + @NonNull + public LiveData<PagedList<Value>> create(@Nullable final Key initialLoadKey, + final PagedList.Config config) { + return new ComputableLiveData<PagedList<Value>>() { + @Nullable + private PagedList<Value> mList; + @Nullable + private DataSource<Key, Value> mDataSource; + + private final DataSource.InvalidatedCallback mCallback = + new DataSource.InvalidatedCallback() { + @Override + public void onInvalidated() { + invalidate(); + } + }; + + @Override + protected PagedList<Value> compute() { + @Nullable Key initializeKey = initialLoadKey; + if (mList != null) { + //noinspection unchecked + initializeKey = (Key) mList.getLastKey(); + } + + do { + if (mDataSource != null) { + mDataSource.removeInvalidatedCallback(mCallback); + } + + mDataSource = createDataSource(); + mDataSource.addInvalidatedCallback(mCallback); + + mList = new PagedList.Builder<Key, Value>() + .setDataSource(mDataSource) + .setMainThreadExecutor(ArchTaskExecutor.getMainThreadExecutor()) + .setBackgroundThreadExecutor( + ArchTaskExecutor.getIOThreadExecutor()) + .setConfig(config) + .setInitialKey(initializeKey) + .build(); + } while (mList.isDetached()); + return mList; + } + }.getLiveData(); + } +} diff --git a/android/arch/paging/NullPaddedList.java b/android/arch/paging/NullPaddedList.java index 43000302..c7b0b231 100644 --- a/android/arch/paging/NullPaddedList.java +++ b/android/arch/paging/NullPaddedList.java @@ -16,11 +16,9 @@ package android.arch.paging; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; -import java.util.ArrayList; +import java.util.AbstractList; import java.util.List; /** @@ -31,18 +29,11 @@ import java.util.List; * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class NullPaddedList<Type> extends PagedList<Type> { +public class NullPaddedList<Type> extends AbstractList<Type> { List<Type> mList; - int mTrailingNullCount; - int mLeadingNullCount; - int mPositionOffset; - - // track the items prepended/appended since the PagedList was initialized - int mNumberPrepended; - int mNumberAppended; - - NullPaddedList() { - } + private int mTrailingNullCount; + private int mLeadingNullCount; + private int mPositionOffset; @Override public String toString() { @@ -91,20 +82,6 @@ public class NullPaddedList<Type> extends PagedList<Type> { mPositionOffset = positionOffset; } - /** - * Create a copy of the passed NullPaddedList. - * - * @param other Other list to copy. - */ - NullPaddedList(NullPaddedList<Type> other) { - mLeadingNullCount = other.getLeadingNullCount(); - mList = other.isImmutable() ? other.mList : new ArrayList<>(other.mList); - mTrailingNullCount = other.getTrailingNullCount(); - - mNumberPrepended = other.getNumberPrepended(); - mNumberAppended = other.getNumberAppended(); - } - // --------------- PagedList API --------------- @Override @@ -124,46 +101,12 @@ public class NullPaddedList<Type> extends PagedList<Type> { } @Override - public void loadAround(int index) { - // do nothing - immutable, so no fetching will be done - } - - @Override public final int size() { return getLoadedCount() + getLeadingNullCount() + getTrailingNullCount(); } - public boolean isImmutable() { - return true; - } - - @Override - public PagedList<Type> snapshot() { - if (isImmutable()) { - return this; - } - return new NullPaddedList<>(this); - } - - @Override - boolean isContiguous() { - return true; - } - - @Override - public void addWeakCallback(@Nullable PagedList<Type> previousSnapshot, - @NonNull Callback callback) { - // no op, immutable - } - - @Override - public void removeWeakCallback(Callback callback) { - // no op, immutable - } - // --------------- Contiguous API --------------- - @Override public int getPositionOffset() { return mPositionOffset; } @@ -194,12 +137,4 @@ public class NullPaddedList<Type> extends PagedList<Type> { public int getTrailingNullCount() { return mTrailingNullCount; } - - int getNumberPrepended() { - return mNumberPrepended; - } - - int getNumberAppended() { - return mNumberAppended; - } } diff --git a/android/arch/paging/NullPaddedListTest.java b/android/arch/paging/NullPaddedListTest.java deleted file mode 100644 index 0c38485c..00000000 --- a/android/arch/paging/NullPaddedListTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -@RunWith(JUnit4.class) -public class NullPaddedListTest { - @Test - public void simple() { - List<String> data = Arrays.asList("A", "B", "C", "D", "E", "F"); - NullPaddedList<String> list = new NullPaddedList<>( - 2, data.subList(2, 4), 2); - - assertNull(list.get(0)); - assertNull(list.get(1)); - assertSame(data.get(2), list.get(2)); - assertSame(data.get(3), list.get(3)); - assertNull(list.get(4)); - assertNull(list.get(5)); - - assertEquals(6, list.size()); - assertEquals(2, list.getLeadingNullCount()); - assertEquals(2, list.getTrailingNullCount()); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getEmpty() { - NullPaddedList<String> list = new NullPaddedList<>(0, new ArrayList<String>(), 0); - list.get(0); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getNegative() { - NullPaddedList<String> list = new NullPaddedList<>(0, Arrays.asList("a", "b"), 0); - list.get(-1); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getPastEnd() { - NullPaddedList<String> list = new NullPaddedList<>(0, Arrays.asList("a", "b"), 0); - list.get(2); - } -} diff --git a/android/arch/paging/Page.java b/android/arch/paging/Page.java new file mode 100644 index 00000000..e9890ed4 --- /dev/null +++ b/android/arch/paging/Page.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.arch.paging; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.List; + +/** + * Immutable class representing a page of data loaded from a DataSource. + * <p> + * Optionally stores before/after keys for cases where they cannot be computed, but the DataSource + * can provide them as part of loading a page. + * <p> + * A page's list must never be modified. + */ +class Page<K, V> { + @SuppressWarnings("WeakerAccess") + @Nullable + public final K beforeKey; + @NonNull + public final List<V> items; + @SuppressWarnings("WeakerAccess") + @Nullable + public K afterKey; + + Page(@NonNull List<V> items) { + this(null, items, null); + } + + Page(@Nullable K beforeKey, @NonNull List<V> items, @Nullable K afterKey) { + this.beforeKey = beforeKey; + this.items = items; + this.afterKey = afterKey; + } +} diff --git a/android/arch/paging/PageArrayList.java b/android/arch/paging/PageArrayList.java deleted file mode 100644 index b90d055a..00000000 --- a/android/arch/paging/PageArrayList.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.RestrictTo; - -import java.util.ArrayList; -import java.util.List; - -/** @hide */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class PageArrayList<T> extends PagedList<T> { - // partial list of pages, doesn't include pages below the lowest accessed, or above the highest - final ArrayList<List<T>> mPages; - - // to access page at index N, do mPages.get(N - mPageIndexOffset), but do bounds checking first! - int mPageIndexOffset; - - final int mPageSize; - final int mCount; - final int mMaxPageCount; - - PageArrayList(int pageSize, int count) { - mPages = new ArrayList<>(); - mPageSize = pageSize; - mCount = count; - mMaxPageCount = (mCount + mPageSize - 1) / mPageSize; - } - - private PageArrayList(PageArrayList<T> other) { - mPages = other.isImmutable() ? other.mPages : new ArrayList<>(other.mPages); - mPageIndexOffset = other.mPageIndexOffset; - mPageSize = other.mPageSize; - mCount = other.size(); - mMaxPageCount = other.mMaxPageCount; - } - - @Override - public T get(int index) { - if (index < 0 || index >= mCount) { - throw new IllegalArgumentException(); - } - - int localPageIndex = getLocalPageIndex(index); - - List<T> page = getPage(localPageIndex); - - if (page == null) { - // page empty - return null; - } - - return page.get(index % mPageSize); - } - - @Nullable - private List<T> getPage(int localPageIndex) { - if (localPageIndex < 0 || localPageIndex >= mPages.size()) { - // page not present - return null; - } - - return mPages.get(localPageIndex); - } - - private int getLocalPageIndex(int index) { - return index / mPageSize - mPageIndexOffset; - } - - @Override - public void loadAround(int index) { - // do nothing - immutable, so no fetching will be done - } - - @Override - public int size() { - return mCount; - } - - @Override - public boolean isImmutable() { - return true; - } - - boolean hasPage(int pageIndex) { - final int localPageIndex = pageIndex - mPageIndexOffset; - List<T> page = getPage(localPageIndex); - return page != null && page.size() != 0; - } - - @Override - public PagedList<T> snapshot() { - if (isImmutable()) { - return this; - } - return new PageArrayList<>(this); - } - - @Override - boolean isContiguous() { - return false; - } - - @Override - public void addWeakCallback(@Nullable PagedList<T> previousSnapshot, - @NonNull Callback callback) { - // no op, immutable - } - - @Override - public void removeWeakCallback(Callback callback) { - // no op, immutable - } -} diff --git a/android/arch/paging/PageArrayListTest.java b/android/arch/paging/PageArrayListTest.java deleted file mode 100644 index 135e640d..00000000 --- a/android/arch/paging/PageArrayListTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.util.Arrays; -import java.util.List; - -@RunWith(JUnit4.class) -public class PageArrayListTest { - @Test - public void simple() { - List<String> data = Arrays.asList("A", "B", "C", "D", "E", "F"); - PageArrayList<String> list = new PageArrayList<>(2, data.size()); - - assertEquals(2, list.mPageSize); - assertEquals(data.size(), list.size()); - assertEquals(3, list.mMaxPageCount); - - for (int i = 0; i < data.size(); i++) { - assertEquals(null, list.get(i)); - } - for (int i = 0; i < data.size(); i += list.mPageSize) { - list.mPages.add(data.subList(i, i + 2)); - } - for (int i = 0; i < data.size(); i++) { - assertEquals(data.get(i), list.get(i)); - } - } -} diff --git a/android/arch/paging/PageResult.java b/android/arch/paging/PageResult.java new file mode 100644 index 00000000..a4090f61 --- /dev/null +++ b/android/arch/paging/PageResult.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.arch.paging; + +import android.support.annotation.AnyThread; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; + +class PageResult<K, V> { + static final int INIT = 0; + + // contiguous results + static final int APPEND = 1; + static final int PREPEND = 2; + + // non-contiguous, tile result + static final int TILE = 3; + + public final int type; + public final Page<K, V> page; + @SuppressWarnings("WeakerAccess") + public final int leadingNulls; + @SuppressWarnings("WeakerAccess") + public final int trailingNulls; + @SuppressWarnings("WeakerAccess") + public final int positionOffset; + + PageResult(int type, Page<K, V> page, int leadingNulls, int trailingNulls, int positionOffset) { + this.type = type; + this.page = page; + this.leadingNulls = leadingNulls; + this.trailingNulls = trailingNulls; + this.positionOffset = positionOffset; + } + + interface Receiver<K, V> { + @AnyThread + void postOnPageResult(@NonNull PageResult<K, V> pageResult); + @MainThread + void onPageResult(@NonNull PageResult<K, V> pageResult); + } +} diff --git a/android/arch/paging/PagedList.java b/android/arch/paging/PagedList.java index 6a31b689..51f524af 100644 --- a/android/arch/paging/PagedList.java +++ b/android/arch/paging/PagedList.java @@ -20,9 +20,12 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; +import java.lang.ref.WeakReference; import java.util.AbstractList; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; /** * Lazy loading list that pages in content from a {@link DataSource}. @@ -90,9 +93,30 @@ import java.util.concurrent.Executor; * @param <T> The type of the entries in the list. */ public abstract class PagedList<T> extends AbstractList<T> { - // Since we currently rely on implementation details of two implementations, - // prevent external subclassing - PagedList() { + @NonNull + final Executor mMainThreadExecutor; + @NonNull + final Executor mBackgroundThreadExecutor; + @NonNull + final Config mConfig; + @NonNull + final PagedStorage<?, T> mStorage; + + int mLastLoad = 0; + T mLastItem = null; + + private final AtomicBoolean mDetached = new AtomicBoolean(false); + + protected final ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>(); + + PagedList(@NonNull PagedStorage<?, T> storage, + @NonNull Executor mainThreadExecutor, + @NonNull Executor backgroundThreadExecutor, + @NonNull Config config) { + mStorage = storage; + mMainThreadExecutor = mainThreadExecutor; + mBackgroundThreadExecutor = backgroundThreadExecutor; + mConfig = config; } /** @@ -117,7 +141,7 @@ public abstract class PagedList<T> extends AbstractList<T> { @NonNull Executor backgroundThreadExecutor, @NonNull Config config, @Nullable K key) { - if (dataSource.isContiguous() || !config.mEnablePlaceholders) { + if (dataSource.isContiguous() || !config.enablePlaceholders) { if (!dataSource.isContiguous()) { //noinspection unchecked dataSource = (DataSource<K, T>) ((TiledDataSource<T>) dataSource).getAsContiguous(); @@ -280,7 +304,13 @@ public abstract class PagedList<T> extends AbstractList<T> { */ @Override @Nullable - public abstract T get(int index); + public T get(int index) { + T item = mStorage.get(index); + if (item != null) { + mLastItem = item; + } + return item; + } /** @@ -288,7 +318,10 @@ public abstract class PagedList<T> extends AbstractList<T> { * * @param index Index at which to load. */ - public abstract void loadAround(int index); + public void loadAround(int index) { + mLastLoad = index + getPositionOffset(); + loadAroundInternal(index); + } /** @@ -297,7 +330,9 @@ public abstract class PagedList<T> extends AbstractList<T> { * @return Current total size of the list. */ @Override - public abstract int size(); + public int size() { + return mStorage.size(); + } /** * Returns whether the list is immutable. Immutable lists may not become mutable again, and may @@ -305,19 +340,39 @@ public abstract class PagedList<T> extends AbstractList<T> { * * @return True if the PagedList is immutable. */ - public abstract boolean isImmutable(); + @SuppressWarnings("WeakerAccess") + public boolean isImmutable() { + return isDetached(); + } /** * Returns an immutable snapshot of the PagedList. If this PagedList is already * immutable, it will be returned. * - * @return Immutable snapshot of PagedList, which may be the PagedList itself. + * @return Immutable snapshot of PagedList data. */ - public abstract List<T> snapshot(); + @NonNull + public List<T> snapshot() { + if (isImmutable()) { + return this; + } + + return new SnapshotPagedList<>(this); + } abstract boolean isContiguous(); /** + * Return the Config used to construct this PagedList. + * + * @return the Config of this PagedList + */ + @NonNull + public Config getConfig() { + return mConfig; + } + + /** * Return the key for the position passed most recently to {@link #loadAround(int)}. * <p> * When a PagedList is invalidated, you can pass the key returned by this function to initialize @@ -328,9 +383,7 @@ public abstract class PagedList<T> extends AbstractList<T> { * @return Key of position most recently passed to {@link #loadAround(int)}. */ @Nullable - public Object getLastKey() { - return null; - } + public abstract Object getLastKey(); /** * True if the PagedList has detached the DataSource it was loading from, and will no longer @@ -338,8 +391,9 @@ public abstract class PagedList<T> extends AbstractList<T> { * * @return True if the data source is detached. */ + @SuppressWarnings("WeakerAccess") public boolean isDetached() { - return true; + return mDetached.get(); } /** @@ -349,7 +403,9 @@ public abstract class PagedList<T> extends AbstractList<T> { * signal to stop loading. The PagedList will continue to present existing data, but will not * initiate new loads. */ + @SuppressWarnings("WeakerAccess") public void detach() { + mDetached.set(true); } /** @@ -361,7 +417,7 @@ public abstract class PagedList<T> extends AbstractList<T> { * If the DataSource is a {@link KeyedDataSource}, and thus doesn't use positions, returns 0. */ public int getPositionOffset() { - return 0; + return mStorage.getPositionOffset(); } /** @@ -385,16 +441,69 @@ public abstract class PagedList<T> extends AbstractList<T> { * @param callback Callback to dispatch to. * @see #removeWeakCallback(Callback) */ - public abstract void addWeakCallback(@Nullable PagedList<T> previousSnapshot, - @NonNull Callback callback); + @SuppressWarnings("WeakerAccess") + public void addWeakCallback(@Nullable List<T> previousSnapshot, @NonNull Callback callback) { + if (previousSnapshot != null && previousSnapshot != this) { + PagedList<T> storageSnapshot = (PagedList<T>) previousSnapshot; + //noinspection unchecked + dispatchUpdatesSinceSnapshot(storageSnapshot, callback); + } + + // first, clean up any empty weak refs + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + Callback currentCallback = mCallbacks.get(i).get(); + if (currentCallback == null) { + mCallbacks.remove(i); + } + } + // then add the new one + mCallbacks.add(new WeakReference<>(callback)); + } /** * Removes a previously added callback. * * @param callback Callback, previously added. - * @see #addWeakCallback(PagedList, Callback) + * @see #addWeakCallback(List, Callback) */ - public abstract void removeWeakCallback(Callback callback); + @SuppressWarnings("WeakerAccess") + public void removeWeakCallback(@NonNull Callback callback) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + Callback currentCallback = mCallbacks.get(i).get(); + if (currentCallback == null || currentCallback == callback) { + // found callback, or empty weak ref + mCallbacks.remove(i); + } + } + } + + void notifyInserted(int position, int count) { + if (count != 0) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + Callback callback = mCallbacks.get(i).get(); + if (callback != null) { + callback.onInserted(position, count); + } + } + } + } + + void notifyChanged(int position, int count) { + if (count != 0) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + Callback callback = mCallbacks.get(i).get(); + + if (callback != null) { + callback.onChanged(position, count); + } + } + } + } + + abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot, + @NonNull Callback callback); + + abstract void loadAroundInternal(int index); /** * Callback signaling when content is loaded into the list. @@ -442,17 +551,41 @@ public abstract class PagedList<T> extends AbstractList<T> { * {@link Builder#setPageSize(int)}, which defines number of items loaded at a time}. */ public static class Config { - final int mPageSize; - final int mPrefetchDistance; - final boolean mEnablePlaceholders; - final int mInitialLoadSizeHint; + /** + * Size of each page loaded by the PagedList. + */ + public final int pageSize; + + /** + * Prefetch distance which defines how far ahead to load. + * <p> + * If this value is set to 50, the paged list will attempt to load 50 items in advance of + * data that's already been accessed. + * + * @see PagedList#loadAround(int) + */ + @SuppressWarnings("WeakerAccess") + public final int prefetchDistance; + + /** + * Defines whether the PagedList may display null placeholders, if the DataSource provides + * them. + */ + @SuppressWarnings("WeakerAccess") + public final boolean enablePlaceholders; + + /** + * Size hint for initial load of PagedList, often larger than a regular page. + */ + @SuppressWarnings("WeakerAccess") + public final int initialLoadSizeHint; private Config(int pageSize, int prefetchDistance, boolean enablePlaceholders, int initialLoadSizeHint) { - mPageSize = pageSize; - mPrefetchDistance = prefetchDistance; - mEnablePlaceholders = enablePlaceholders; - mInitialLoadSizeHint = initialLoadSizeHint; + this.pageSize = pageSize; + this.prefetchDistance = prefetchDistance; + this.enablePlaceholders = enablePlaceholders; + this.initialLoadSizeHint = initialLoadSizeHint; } /** @@ -545,10 +678,15 @@ public abstract class PagedList<T> extends AbstractList<T> { * Defines how many items to load when first load occurs, if you are using a * {@link KeyedDataSource}. * <p> - * If you are using an {@link TiledDataSource}, this value is currently ignored. - * Otherwise, this value will be passed to - * {@link KeyedDataSource#loadInitial(int)} to load a (typically) larger amount - * of data on first load. + * This value is typically larger than page size, so on first load data there's a large + * enough range of content loaded to cover small scrolls. + * <p> + * If used with a {@link TiledDataSource}, this value is rounded to the nearest number + * of pages, with a minimum of two pages, and loaded with a single call to + * {@link TiledDataSource#loadRange(int, int)}. + * <p> + * If used with a {@link KeyedDataSource}, this value will be passed to + * {@link KeyedDataSource#loadInitial(int)}. * <p> * If not set, defaults to three times page size. * diff --git a/android/arch/paging/PagedListAdapterHelper.java b/android/arch/paging/PagedListAdapterHelper.java index c7b61d9f..abcff415 100644 --- a/android/arch/paging/PagedListAdapterHelper.java +++ b/android/arch/paging/PagedListAdapterHelper.java @@ -25,8 +25,6 @@ import android.support.v7.util.DiffUtil; import android.support.v7.util.ListUpdateCallback; import android.support.v7.widget.RecyclerView; -import java.util.List; - /** * Helper object for mapping a {@link PagedList} into a * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}. @@ -120,15 +118,15 @@ import java.util.List; * @param <T> Type of the PagedLists this helper will receive. */ public class PagedListAdapterHelper<T> { + // updateCallback notifications must only be notified *after* new data and item count are stored + // this ensures Adapter#notifyItemRangeInserted etc are accessing the new data private final ListUpdateCallback mUpdateCallback; private final ListAdapterConfig<T> mConfig; - // true if our listener is detached from mList, because it's been snapshotted - private boolean mUpdateScheduled; - private boolean mIsContiguous; - private PagedList<T> mList; + private PagedList<T> mPagedList; + private PagedList<T> mSnapshot; // Max generation of currently scheduled runnable private int mMaxScheduledGeneration; @@ -182,12 +180,17 @@ public class PagedListAdapterHelper<T> { @SuppressWarnings("WeakerAccess") @Nullable public T getItem(int index) { - if (mList == null) { - throw new IndexOutOfBoundsException("Item count is zero, getItem() call is invalid"); + if (mPagedList == null) { + if (mSnapshot == null) { + throw new IndexOutOfBoundsException( + "Item count is zero, getItem() call is invalid"); + } else { + return mSnapshot.get(index); + } } - mList.loadAround(index); - return mList.get(index); + mPagedList.loadAround(index); + return mPagedList.get(index); } /** @@ -198,7 +201,11 @@ public class PagedListAdapterHelper<T> { */ @SuppressWarnings("WeakerAccess") public int getItemCount() { - return mList == null ? 0 : mList.size(); + if (mPagedList != null) { + return mPagedList.size(); + } + + return mSnapshot == null ? 0 : mSnapshot.size(); } /** @@ -212,7 +219,7 @@ public class PagedListAdapterHelper<T> { */ public void setList(final PagedList<T> pagedList) { if (pagedList != null) { - if (mList == null) { + if (mPagedList == null && mSnapshot == null) { mIsContiguous = pagedList.isContiguous(); } else { if (pagedList.isContiguous() != mIsContiguous) { @@ -222,7 +229,7 @@ public class PagedListAdapterHelper<T> { } } - if (pagedList == mList) { + if (pagedList == mPagedList) { // nothing to do return; } @@ -231,49 +238,55 @@ public class PagedListAdapterHelper<T> { final int runGeneration = ++mMaxScheduledGeneration; if (pagedList == null) { - mUpdateCallback.onRemoved(0, mList.size()); - mList.removeWeakCallback(mPagedListCallback); - mList = null; + int removedCount = getItemCount(); + if (mPagedList != null) { + mPagedList.removeWeakCallback(mPagedListCallback); + mPagedList = null; + } else if (mSnapshot != null) { + mSnapshot = null; + } + // dispatch update callback after updating mPagedList/mSnapshot + mUpdateCallback.onRemoved(0, removedCount); return; } - if (mList == null) { + if (mPagedList == null && mSnapshot == null) { // fast simple first insert - mUpdateCallback.onInserted(0, pagedList.size()); - mList = pagedList; + mPagedList = pagedList; pagedList.addWeakCallback(null, mPagedListCallback); + + // dispatch update callback after updating mPagedList/mSnapshot + mUpdateCallback.onInserted(0, pagedList.size()); return; } - if (!mList.isImmutable()) { + if (mPagedList != null) { // first update scheduled on this list, so capture mPages as a snapshot, removing // callbacks so we don't have resolve updates against a moving target - mList.removeWeakCallback(mPagedListCallback); - mList = (PagedList<T>) mList.snapshot(); + mPagedList.removeWeakCallback(mPagedListCallback); + mSnapshot = (PagedList<T>) mPagedList.snapshot(); + mPagedList = null; + } + + if (mSnapshot == null || mPagedList != null) { + throw new IllegalStateException("must be in snapshot state to diff"); } - final PagedList<T> oldSnapshot = mList; - final List<T> newSnapshot = pagedList.snapshot(); - mUpdateScheduled = true; + final PagedList<T> oldSnapshot = mSnapshot; + final PagedList<T> newSnapshot = (PagedList<T>) pagedList.snapshot(); mConfig.getBackgroundThreadExecutor().execute(new Runnable() { @Override public void run() { final DiffUtil.DiffResult result; - if (mIsContiguous) { - result = ContiguousDiffHelper.computeDiff( - (NullPaddedList<T>) oldSnapshot, (NullPaddedList<T>) newSnapshot, - mConfig.getDiffCallback(), true); - } else { - result = SparseDiffHelper.computeDiff( - (PageArrayList<T>) oldSnapshot, (PageArrayList<T>) newSnapshot, - mConfig.getDiffCallback(), true); - } + result = PagedStorageDiffHelper.computeDiff( + oldSnapshot.mStorage, + newSnapshot.mStorage, + mConfig.getDiffCallback()); mConfig.getMainThreadExecutor().execute(new Runnable() { @Override public void run() { if (mMaxScheduledGeneration == runGeneration) { - mUpdateScheduled = false; latchPagedList(pagedList, newSnapshot, result); } } @@ -283,16 +296,21 @@ public class PagedListAdapterHelper<T> { } private void latchPagedList( - PagedList<T> newList, List<T> diffSnapshot, + PagedList<T> newList, PagedList<T> diffSnapshot, DiffUtil.DiffResult diffResult) { - if (mIsContiguous) { - ContiguousDiffHelper.dispatchDiff(mUpdateCallback, - (NullPaddedList<T>) mList, (ContiguousPagedList<T>) newList, diffResult); - } else { - SparseDiffHelper.dispatchDiff(mUpdateCallback, diffResult); + if (mSnapshot == null || mPagedList != null) { + throw new IllegalStateException("must be in snapshot state to apply diff"); } - mList = newList; - newList.addWeakCallback((PagedList<T>) diffSnapshot, mPagedListCallback); + + PagedList<T> previousSnapshot = mSnapshot; + mPagedList = newList; + mSnapshot = null; + + // dispatch update callback after updating mPagedList/mSnapshot + PagedStorageDiffHelper.dispatchDiff(mUpdateCallback, + previousSnapshot.mStorage, newList.mStorage, diffResult); + + newList.addWeakCallback(diffSnapshot, mPagedListCallback); } /** @@ -307,6 +325,9 @@ public class PagedListAdapterHelper<T> { @SuppressWarnings("WeakerAccess") @Nullable public PagedList<T> getCurrentList() { - return mList; + if (mSnapshot != null) { + return mSnapshot; + } + return mPagedList; } } diff --git a/android/arch/paging/PagedListAdapterHelperTest.java b/android/arch/paging/PagedListAdapterHelperTest.java deleted file mode 100644 index 3518540c..00000000 --- a/android/arch/paging/PagedListAdapterHelperTest.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; - -import android.support.annotation.NonNull; -import android.support.test.filters.SmallTest; -import android.support.v7.recyclerview.extensions.DiffCallback; -import android.support.v7.recyclerview.extensions.ListAdapterConfig; -import android.support.v7.util.ListUpdateCallback; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.util.ArrayList; -import java.util.List; - -@SmallTest -@RunWith(JUnit4.class) -public class PagedListAdapterHelperTest { - private TestExecutor mMainThread = new TestExecutor(); - private TestExecutor mDiffThread = new TestExecutor(); - private TestExecutor mPageLoadingThread = new TestExecutor(); - - private static final ArrayList<String> ALPHABET_LIST = new ArrayList<>(); - static { - for (int i = 0; i < 26; i++) { - ALPHABET_LIST.add("" + 'a' + i); - } - } - - private static final DiffCallback<String> STRING_DIFF_CALLBACK = new DiffCallback<String>() { - @Override - public boolean areItemsTheSame(@NonNull String oldItem, @NonNull String newItem) { - return oldItem.equals(newItem); - } - - @Override - public boolean areContentsTheSame(@NonNull String oldItem, @NonNull String newItem) { - return oldItem.equals(newItem); - } - }; - - private static final ListUpdateCallback IGNORE_CALLBACK = new ListUpdateCallback() { - @Override - public void onInserted(int position, int count) { - } - - @Override - public void onRemoved(int position, int count) { - } - - @Override - public void onMoved(int fromPosition, int toPosition) { - } - - @Override - public void onChanged(int position, int count, Object payload) { - } - }; - - - private <T> PagedListAdapterHelper<T> createHelper( - ListUpdateCallback listUpdateCallback, DiffCallback<T> diffCallback) { - return new PagedListAdapterHelper<T>(listUpdateCallback, - new ListAdapterConfig.Builder<T>() - .setDiffCallback(diffCallback) - .setMainThreadExecutor(mMainThread) - .setBackgroundThreadExecutor(mDiffThread) - .build()); - } - - private <V> PagedList<V> createPagedListFromListAndPos( - PagedList.Config config, List<V> data, int initialKey) { - return new PagedList.Builder<Integer, V>() - .setInitialKey(initialKey) - .setConfig(config) - .setMainThreadExecutor(mMainThread) - .setBackgroundThreadExecutor(mPageLoadingThread) - .setDataSource(new ListDataSource<>(data)) - .build(); - } - - @Test - public void initialState() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - PagedListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - assertEquals(null, helper.getCurrentList()); - assertEquals(0, helper.getItemCount()); - verifyZeroInteractions(callback); - } - - @Test - public void setFullList() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - PagedListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - helper.setList(new StringPagedList(0, 0, "a", "b")); - - assertEquals(2, helper.getItemCount()); - assertEquals("a", helper.getItem(0)); - assertEquals("b", helper.getItem(1)); - - verify(callback).onInserted(0, 2); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getEmpty() { - PagedListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.getItem(0); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getNegative() { - PagedListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.setList(new StringPagedList(0, 0, "a", "b")); - helper.getItem(-1); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getPastEnd() { - PagedListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.setList(new StringPagedList(0, 0, "a", "b")); - helper.getItem(2); - } - - @Test - public void simpleStatic() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - PagedListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - - assertEquals(0, helper.getItemCount()); - - helper.setList(new StringPagedList(2, 2, "a", "b")); - - verify(callback).onInserted(0, 6); - verifyNoMoreInteractions(callback); - assertEquals(6, helper.getItemCount()); - - assertNull(helper.getItem(0)); - assertNull(helper.getItem(1)); - assertEquals("a", helper.getItem(2)); - assertEquals("b", helper.getItem(3)); - assertNull(helper.getItem(4)); - assertNull(helper.getItem(5)); - } - - @Test - public void pagingInContent() { - PagedList.Config config = new PagedList.Config.Builder() - .setInitialLoadSizeHint(4) - .setPageSize(2) - .setPrefetchDistance(2) - .build(); - - final ListUpdateCallback callback = mock(ListUpdateCallback.class); - PagedListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - - helper.setList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2)); - verify(callback).onInserted(0, ALPHABET_LIST.size()); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - - // get without triggering prefetch... - helper.getItem(1); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - - // get triggering prefetch... - helper.getItem(2); - verifyNoMoreInteractions(callback); - drain(); - verify(callback).onChanged(4, 2, null); - verifyNoMoreInteractions(callback); - - // get with no data loaded nearby... - helper.getItem(12); - verifyNoMoreInteractions(callback); - drain(); - verify(callback).onChanged(10, 2, null); - verify(callback).onChanged(12, 2, null); - verify(callback).onChanged(14, 2, null); - verifyNoMoreInteractions(callback); - - // finally, clear - helper.setList(null); - verify(callback).onRemoved(0, 26); - drain(); - verifyNoMoreInteractions(callback); - } - - @Test - public void simpleSwap() { - // Page size large enough to load - PagedList.Config config = new PagedList.Config.Builder() - .setPageSize(50) - .build(); - - final ListUpdateCallback callback = mock(ListUpdateCallback.class); - PagedListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - - // initial list missing one item (immediate) - helper.setList(createPagedListFromListAndPos(config, ALPHABET_LIST.subList(0, 25), 0)); - verify(callback).onInserted(0, 25); - verifyNoMoreInteractions(callback); - assertEquals(helper.getItemCount(), 25); - drain(); - verifyNoMoreInteractions(callback); - - // pass second list with full data - helper.setList(createPagedListFromListAndPos(config, ALPHABET_LIST, 0)); - verifyNoMoreInteractions(callback); - drain(); - verify(callback).onInserted(25, 1); - verifyNoMoreInteractions(callback); - assertEquals(helper.getItemCount(), 26); - - // finally, clear (immediate) - helper.setList(null); - verify(callback).onRemoved(0, 26); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - } - - @Test - public void newPageWhileDiffing() { - PagedList.Config config = new PagedList.Config.Builder() - .setInitialLoadSizeHint(4) - .setPageSize(2) - .setPrefetchDistance(2) - .build(); - - final ListUpdateCallback callback = mock(ListUpdateCallback.class); - PagedListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - - helper.setList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2)); - verify(callback).onInserted(0, ALPHABET_LIST.size()); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - assertNotNull(helper.getCurrentList()); - assertFalse(helper.getCurrentList().isImmutable()); - - // trigger page loading - helper.getItem(10); - helper.setList(createPagedListFromListAndPos(config, ALPHABET_LIST, 2)); - verifyNoMoreInteractions(callback); - - // drain page fetching, but list became immutable, page will be ignored - drainExceptDiffThread(); - verifyNoMoreInteractions(callback); - assertNotNull(helper.getCurrentList()); - assertTrue(helper.getCurrentList().isImmutable()); - - // finally full drain, which signals nothing, since 1st pagedlist == 2nd pagedlist - drain(); - verifyNoMoreInteractions(callback); - assertNotNull(helper.getCurrentList()); - assertFalse(helper.getCurrentList().isImmutable()); - } - - private void drainExceptDiffThread() { - boolean executed; - do { - executed = mPageLoadingThread.executeAll(); - executed |= mMainThread.executeAll(); - } while (executed); - } - - private void drain() { - boolean executed; - do { - executed = mPageLoadingThread.executeAll(); - executed |= mDiffThread.executeAll(); - executed |= mMainThread.executeAll(); - } while (executed); - } -} diff --git a/android/arch/paging/PagedStorage.java b/android/arch/paging/PagedStorage.java new file mode 100644 index 00000000..7f91290d --- /dev/null +++ b/android/arch/paging/PagedStorage.java @@ -0,0 +1,433 @@ +/* + * 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.arch.paging; + +import android.support.annotation.NonNull; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class PagedStorage<K, V> extends AbstractList<V> { + // Always set + private int mLeadingNullCount; + /** + * List of pages in storage. + * + * Two storage modes: + * + * Contiguous - all content in mPages is valid and loaded, but may return false from isTiled(). + * Safe to access any item in any page. + * + * Non-contiguous - mPages may have nulls or a placeholder page, isTiled() always returns true. + * mPages may have nulls, or placeholder (empty) pages while content is loading. + */ + private final ArrayList<Page<K, V>> mPages; + private int mTrailingNullCount; + + private int mPositionOffset; + /** + * Number of items represented by {@link #mPages}. If tiling is enabled, unloaded items in + * {@link #mPages} may be null, but this value still counts them. + */ + private int mStorageCount; + + // If mPageSize > 0, tiling is enabled, 'mPages' may have gaps, and leadingPages is set + private int mPageSize; + + private int mNumberPrepended; + private int mNumberAppended; + + // only used in tiling case + private Page<K, V> mPlaceholderPage; + + PagedStorage() { + mLeadingNullCount = 0; + mPages = new ArrayList<>(); + mTrailingNullCount = 0; + mPositionOffset = 0; + mStorageCount = 0; + mPageSize = 1; + mNumberPrepended = 0; + mNumberAppended = 0; + } + + PagedStorage(int leadingNulls, Page<K, V> page, int trailingNulls) { + this(); + init(leadingNulls, page, trailingNulls, 0); + } + + private PagedStorage(PagedStorage<K, V> other) { + mLeadingNullCount = other.mLeadingNullCount; + mPages = new ArrayList<>(other.mPages); + mTrailingNullCount = other.mTrailingNullCount; + mPositionOffset = other.mPositionOffset; + mStorageCount = other.mStorageCount; + mPageSize = other.mPageSize; + mNumberPrepended = other.mNumberPrepended; + mNumberAppended = other.mNumberAppended; + + // preserve placeholder page so we can locate placeholder pages if needed later + mPlaceholderPage = other.mPlaceholderPage; + } + + PagedStorage<K, V> snapshot() { + return new PagedStorage<>(this); + } + + private void init(int leadingNulls, Page<K, V> page, int trailingNulls, int positionOffset) { + mLeadingNullCount = leadingNulls; + mPages.clear(); + mPages.add(page); + mTrailingNullCount = trailingNulls; + + mPositionOffset = positionOffset; + mStorageCount = page.items.size(); + + // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled + // even if it will break if nulls convert. + mPageSize = page.items.size(); + + mNumberPrepended = 0; + mNumberAppended = 0; + } + + void init(int leadingNulls, Page<K, V> page, int trailingNulls, int positionOffset, + @NonNull Callback callback) { + init(leadingNulls, page, trailingNulls, positionOffset); + callback.onInitialized(size()); + } + + @Override + public V get(int i) { + if (i < 0 || i >= size()) { + throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size()); + } + + // is it definitely outside 'mPages'? + int localIndex = i - mLeadingNullCount; + if (localIndex < 0 || localIndex >= mStorageCount) { + return null; + } + + int localPageIndex; + int pageInternalIndex; + + if (isTiled()) { + // it's inside mPages, and we're tiled. Jump to correct tile. + localPageIndex = localIndex / mPageSize; + pageInternalIndex = localIndex % mPageSize; + } else { + // it's inside mPages, but page sizes aren't regular. Walk to correct tile. + // Pages can only be null while tiled, so accessing page count is safe. + pageInternalIndex = localIndex; + final int localPageCount = mPages.size(); + for (localPageIndex = 0; localPageIndex < localPageCount; localPageIndex++) { + int pageSize = mPages.get(localPageIndex).items.size(); + if (pageSize > pageInternalIndex) { + // stop, found the page + break; + } + pageInternalIndex -= pageSize; + } + } + + Page<?, V> page = mPages.get(localPageIndex); + if (page == null || page.items.size() == 0) { + // can only occur in tiled case, with untouched inner/placeholder pages + return null; + } + return page.items.get(pageInternalIndex); + } + + /** + * Returns true if all pages are the same size, except for the last, which may be smaller + */ + boolean isTiled() { + return mPageSize > 0; + } + + int getLeadingNullCount() { + return mLeadingNullCount; + } + + int getTrailingNullCount() { + return mTrailingNullCount; + } + + int getStorageCount() { + return mStorageCount; + } + + int getNumberAppended() { + return mNumberAppended; + } + + int getNumberPrepended() { + return mNumberPrepended; + } + + int getPageCount() { + return mPages.size(); + } + + interface Callback { + void onInitialized(int count); + void onPagePrepended(int leadingNulls, int changed, int added); + void onPageAppended(int endPosition, int changed, int added); + void onPagePlaceholderInserted(int pageIndex); + void onPageInserted(int start, int count); + } + + int getPositionOffset() { + return mPositionOffset; + } + + @Override + public int size() { + return mLeadingNullCount + mStorageCount + mTrailingNullCount; + } + + int computeLeadingNulls() { + int total = mLeadingNullCount; + final int pageCount = mPages.size(); + for (int i = 0; i < pageCount; i++) { + Page page = mPages.get(i); + if (page != null && page != mPlaceholderPage) { + break; + } + total += mPageSize; + } + return total; + } + + int computeTrailingNulls() { + int total = mTrailingNullCount; + for (int i = mPages.size() - 1; i >= 0; i--) { + Page page = mPages.get(i); + if (page != null && page != mPlaceholderPage) { + break; + } + total += mPageSize; + } + return total; + } + + // ---------------- Contiguous API ------------------- + + V getFirstContiguousItem() { + // safe to access first page's first item here: + // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty + return mPages.get(0).items.get(0); + } + + V getLastContiguousItem() { + // safe to access last page's last item here: + // If contiguous, mPages can't be empty, can't hold null Pages, and items can't be empty + Page<K, V> page = mPages.get(mPages.size() - 1); + return page.items.get(page.items.size() - 1); + } + + public void prependPage(@NonNull Page<K, V> page, @NonNull Callback callback) { + final int count = page.items.size(); + if (count == 0) { + // Nothing returned from source, stop loading in this direction + return; + } + if (mPageSize > 0 && count != mPageSize) { + if (mPages.size() == 1 && count > mPageSize) { + // prepending to a single item - update current page size to that of 'inner' page + mPageSize = count; + } else { + // no longer tiled + mPageSize = -1; + } + } + + mPages.add(0, page); + mStorageCount += count; + + final int changedCount = Math.min(mLeadingNullCount, count); + final int addedCount = count - changedCount; + + if (changedCount != 0) { + mLeadingNullCount -= changedCount; + } + mPositionOffset -= addedCount; + mNumberPrepended += count; + + callback.onPagePrepended(mLeadingNullCount, changedCount, addedCount); + } + + public void appendPage(@NonNull Page<K, V> page, @NonNull Callback callback) { + final int count = page.items.size(); + if (count == 0) { + // Nothing returned from source, stop loading in this direction + return; + } + + if (mPageSize > 0) { + // if the previous page was smaller than mPageSize, + // or if this page is larger than the previous, disable tiling + if (mPages.get(mPages.size() - 1).items.size() != mPageSize + || count > mPageSize) { + mPageSize = -1; + } + } + + mPages.add(page); + mStorageCount += count; + + final int changedCount = Math.min(mTrailingNullCount, count); + final int addedCount = count - changedCount; + + if (changedCount != 0) { + mTrailingNullCount -= changedCount; + } + mNumberAppended += count; + callback.onPageAppended(mLeadingNullCount + mStorageCount - count, + changedCount, addedCount); + } + + // ------------------ Non-Contiguous API (tiling required) ---------------------- + + public void insertPage(int position, @NonNull Page<K, V> page, Callback callback) { + final int newPageSize = page.items.size(); + if (newPageSize != mPageSize) { + // differing page size is OK in 2 cases, when the page is being added: + // 1) to the end (in which case, ignore new smaller size) + // 2) only the last page has been added so far (in which case, adopt new bigger size) + + int size = size(); + boolean addingLastPage = position == (size - size % mPageSize) + && newPageSize < mPageSize; + boolean onlyEndPagePresent = mTrailingNullCount == 0 && mPages.size() == 1 + && newPageSize > mPageSize; + + // OK only if existing single page, and it's the last one + if (!onlyEndPagePresent && !addingLastPage) { + throw new IllegalArgumentException("page introduces incorrect tiling"); + } + if (onlyEndPagePresent) { + mPageSize = newPageSize; + } + } + + int pageIndex = position / mPageSize; + + allocatePageRange(pageIndex, pageIndex); + + int localPageIndex = pageIndex - mLeadingNullCount / mPageSize; + + Page<K, V> oldPage = mPages.get(localPageIndex); + if (oldPage != null && oldPage != mPlaceholderPage) { + throw new IllegalArgumentException( + "Invalid position " + position + ": data already loaded"); + } + mPages.set(localPageIndex, page); + callback.onPageInserted(position, page.items.size()); + } + + private Page<K, V> getPlaceholderPage() { + if (mPlaceholderPage == null) { + @SuppressWarnings("unchecked") + List<V> list = Collections.emptyList(); + mPlaceholderPage = new Page<>(null, list, null); + } + return mPlaceholderPage; + } + + private void allocatePageRange(final int minimumPage, final int maximumPage) { + int leadingNullPages = mLeadingNullCount / mPageSize; + + if (minimumPage < leadingNullPages) { + for (int i = 0; i < leadingNullPages - minimumPage; i++) { + mPages.add(0, null); + } + int newStorageAllocated = (leadingNullPages - minimumPage) * mPageSize; + mStorageCount += newStorageAllocated; + mLeadingNullCount -= newStorageAllocated; + + leadingNullPages = minimumPage; + } + if (maximumPage >= leadingNullPages + mPages.size()) { + int newStorageAllocated = Math.min(mTrailingNullCount, + (maximumPage + 1 - (leadingNullPages + mPages.size())) * mPageSize); + for (int i = mPages.size(); i <= maximumPage - leadingNullPages; i++) { + mPages.add(mPages.size(), null); + } + mStorageCount += newStorageAllocated; + mTrailingNullCount -= newStorageAllocated; + } + } + + public void allocatePlaceholders(int index, int prefetchDistance, + int pageSize, Callback callback) { + if (pageSize != mPageSize) { + if (pageSize < mPageSize) { + throw new IllegalArgumentException("Page size cannot be reduced"); + } + if (mPages.size() != 1 || mTrailingNullCount != 0) { + // not in single, last page allocated case - can't change page size + throw new IllegalArgumentException( + "Page size can change only if last page is only one present"); + } + mPageSize = pageSize; + } + + final int maxPageCount = (size() + mPageSize - 1) / mPageSize; + int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0); + int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1); + + allocatePageRange(minimumPage, maximumPage); + int leadingNullPages = mLeadingNullCount / mPageSize; + for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) { + int localPageIndex = pageIndex - leadingNullPages; + if (mPages.get(localPageIndex) == null) { + mPages.set(localPageIndex, getPlaceholderPage()); + callback.onPagePlaceholderInserted(pageIndex); + } + } + } + + public boolean hasPage(int pageSize, int index) { + // NOTE: we pass pageSize here to avoid in case mPageSize + // not fully initialized (when last page only one loaded) + int leadingNullPages = mLeadingNullCount / pageSize; + + if (index < leadingNullPages || index >= leadingNullPages + mPages.size()) { + return false; + } + + Page<K, V> page = mPages.get(index - leadingNullPages); + + return page != null && page != mPlaceholderPage; + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder("leading " + mLeadingNullCount + + ", storage " + mStorageCount + + ", trailing " + getTrailingNullCount()); + + for (int i = 0; i < mPages.size(); i++) { + ret.append(" ").append(mPages.get(i)); + } + return ret.toString(); + } +} diff --git a/android/arch/paging/ContiguousDiffHelper.java b/android/arch/paging/PagedStorageDiffHelper.java index 7dd194b2..6fc70390 100644 --- a/android/arch/paging/ContiguousDiffHelper.java +++ b/android/arch/paging/PagedStorageDiffHelper.java @@ -16,36 +16,31 @@ package android.arch.paging; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.RestrictTo; import android.support.v7.recyclerview.extensions.DiffCallback; import android.support.v7.util.DiffUtil; import android.support.v7.util.ListUpdateCallback; -/** @hide */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class ContiguousDiffHelper { - private ContiguousDiffHelper() { +class PagedStorageDiffHelper { + private PagedStorageDiffHelper() { } - @NonNull static <T> DiffUtil.DiffResult computeDiff( - final NullPaddedList<T> oldList, final NullPaddedList<T> newList, - final DiffCallback<T> diffCallback, boolean detectMoves) { + final PagedStorage<?, T> oldList, + final PagedStorage<?, T> newList, + final DiffCallback<T> diffCallback) { + final int oldOffset = oldList.computeLeadingNulls(); + final int newOffset = newList.computeLeadingNulls(); + + final int oldSize = oldList.size() - oldOffset - oldList.computeTrailingNulls(); + final int newSize = newList.size() - newOffset - newList.computeTrailingNulls(); - if (!oldList.isImmutable()) { - throw new IllegalArgumentException("list must be immutable to safely perform diff"); - } - if (!newList.isImmutable()) { - throw new IllegalArgumentException("list must be immutable to safely perform diff"); - } return DiffUtil.calculateDiff(new DiffUtil.Callback() { @Nullable @Override public Object getChangePayload(int oldItemPosition, int newItemPosition) { - T oldItem = oldList.mList.get(oldItemPosition); - T newItem = newList.mList.get(newItemPosition); + T oldItem = oldList.get(oldItemPosition + oldOffset); + T newItem = newList.get(newItemPosition + newList.getLeadingNullCount()); if (oldItem == null || newItem == null) { return null; } @@ -54,21 +49,22 @@ class ContiguousDiffHelper { @Override public int getOldListSize() { - return oldList.mList.size(); + return oldSize; } @Override public int getNewListSize() { - return newList.mList.size(); + return newSize; } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - T oldItem = oldList.mList.get(oldItemPosition); - T newItem = newList.mList.get(newItemPosition); + T oldItem = oldList.get(oldItemPosition + oldOffset); + T newItem = newList.get(newItemPosition + newList.getLeadingNullCount()); if (oldItem == newItem) { return true; } + //noinspection SimplifiableIfStatement if (oldItem == null || newItem == null) { return false; } @@ -77,18 +73,19 @@ class ContiguousDiffHelper { @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - T oldItem = oldList.mList.get(oldItemPosition); - T newItem = newList.mList.get(newItemPosition); + T oldItem = oldList.get(oldItemPosition + oldOffset); + T newItem = newList.get(newItemPosition + newList.getLeadingNullCount()); if (oldItem == newItem) { return true; } + //noinspection SimplifiableIfStatement if (oldItem == null || newItem == null) { return false; } return diffCallback.areContentsTheSame(oldItem, newItem); } - }, detectMoves); + }, true); } private static class OffsettingListUpdateCallback implements ListUpdateCallback { @@ -134,21 +131,25 @@ class ContiguousDiffHelper { * immediately after dispatching this diff. */ static <T> void dispatchDiff(ListUpdateCallback callback, - final NullPaddedList<T> oldList, final NullPaddedList<T> newList, + final PagedStorage<?, T> oldList, + final PagedStorage<?, T> newList, final DiffUtil.DiffResult diffResult) { - if (oldList.getLeadingNullCount() == 0 - && oldList.getTrailingNullCount() == 0 - && newList.getLeadingNullCount() == 0 - && newList.getTrailingNullCount() == 0) { + final int trailingOld = oldList.computeTrailingNulls(); + final int trailingNew = newList.computeTrailingNulls(); + final int leadingOld = oldList.computeLeadingNulls(); + final int leadingNew = newList.computeLeadingNulls(); + + if (trailingOld == 0 + && trailingNew == 0 + && leadingOld == 0 + && leadingNew == 0) { // Simple case, dispatch & return diffResult.dispatchUpdatesTo(callback); return; } // First, remove or insert trailing nulls - final int trailingOld = oldList.getTrailingNullCount(); - final int trailingNew = newList.getTrailingNullCount(); if (trailingOld > trailingNew) { int count = trailingOld - trailingNew; callback.onRemoved(oldList.size() - count, count); @@ -157,8 +158,6 @@ class ContiguousDiffHelper { } // Second, remove or insert leading nulls - final int leadingOld = oldList.getLeadingNullCount(); - final int leadingNew = newList.getLeadingNullCount(); if (leadingOld > leadingNew) { callback.onRemoved(0, leadingOld - leadingNew); } else if (leadingOld < leadingNew) { diff --git a/android/arch/paging/PositionalDataSource.java b/android/arch/paging/PositionalDataSource.java index deb51e94..c538cb60 100644 --- a/android/arch/paging/PositionalDataSource.java +++ b/android/arch/paging/PositionalDataSource.java @@ -42,6 +42,17 @@ import java.util.List; */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class PositionalDataSource<Value> extends ContiguousDataSource<Integer, Value> { + + /** + * Number of items that this DataSource can provide in total, or COUNT_UNDEFINED. + * + * @return number of items that this DataSource can provide in total, or COUNT_UNDEFINED + * if difficult or undesired to compute. + */ + public int countItems() { + return COUNT_UNDEFINED; + } + @Nullable @Override List<Value> loadAfterImpl(int currentEndIndex, @NonNull Value currentEndItem, int pageSize) { @@ -55,16 +66,7 @@ public abstract class PositionalDataSource<Value> extends ContiguousDataSource<I return loadBefore(currentBeginIndex - 1, pageSize); } - - /** - * Load initial data, starting after the passed position. - * - * @param position Index just before the data to be loaded. - * @param initialLoadSize Suggested number of items to load. - * @return List of initial items, representing data starting at position. Null if the - * DataSource is no longer valid, and should not be queried again. - * @hide - */ + /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable @@ -118,6 +120,9 @@ public abstract class PositionalDataSource<Value> extends ContiguousDataSource<I @Override Integer getKey(int position, Value item) { + if (position < 0) { + return null; + } return position; } } diff --git a/android/arch/paging/SnapshotPagedList.java b/android/arch/paging/SnapshotPagedList.java new file mode 100644 index 00000000..7e965a0f --- /dev/null +++ b/android/arch/paging/SnapshotPagedList.java @@ -0,0 +1,64 @@ +/* + * 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.arch.paging; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +class SnapshotPagedList<T> extends PagedList<T> { + private final boolean mContiguous; + private final Object mLastKey; + + SnapshotPagedList(@NonNull PagedList<T> pagedList) { + super(pagedList.mStorage.snapshot(), + pagedList.mMainThreadExecutor, + pagedList.mBackgroundThreadExecutor, + pagedList.mConfig); + mContiguous = pagedList.isContiguous(); + mLastKey = pagedList.getLastKey(); + } + + @Override + public boolean isImmutable() { + return true; + } + + @Override + public boolean isDetached() { + return true; + } + + @Override + boolean isContiguous() { + return mContiguous; + } + + @Nullable + @Override + public Object getLastKey() { + return mLastKey; + } + + @Override + void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> storageSnapshot, + @NonNull Callback callback) { + } + + @Override + void loadAroundInternal(int index) { + } +} diff --git a/android/arch/paging/SparseDiffHelper.java b/android/arch/paging/SparseDiffHelper.java deleted file mode 100644 index fe478973..00000000 --- a/android/arch/paging/SparseDiffHelper.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.RestrictTo; -import android.support.v7.recyclerview.extensions.DiffCallback; -import android.support.v7.util.DiffUtil; -import android.support.v7.util.ListUpdateCallback; - -/** @hide */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class SparseDiffHelper { - private SparseDiffHelper() { - } - - @NonNull - static <T> DiffUtil.DiffResult computeDiff( - final PageArrayList<T> oldList, final PageArrayList<T> newList, - final DiffCallback<T> diffCallback, boolean detectMoves) { - - if (!oldList.isImmutable()) { - throw new IllegalArgumentException("list must be immutable to safely perform diff"); - } - if (!newList.isImmutable()) { - throw new IllegalArgumentException("list must be immutable to safely perform diff"); - } - return DiffUtil.calculateDiff(new DiffUtil.Callback() { - @Nullable - @Override - public Object getChangePayload(int oldItemPosition, int newItemPosition) { - T oldItem = oldList.get(oldItemPosition); - T newItem = newList.get(newItemPosition); - if (oldItem == null || newItem == null) { - return null; - } - return diffCallback.getChangePayload(oldItem, newItem); - } - - @Override - public int getOldListSize() { - return oldList.size(); - } - - @Override - public int getNewListSize() { - return newList.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - T oldItem = oldList.get(oldItemPosition); - T newItem = newList.get(newItemPosition); - if (oldItem == newItem) { - return true; - } - if (oldItem == null || newItem == null) { - return false; - } - return diffCallback.areItemsTheSame(oldItem, newItem); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - T oldItem = oldList.get(oldItemPosition); - T newItem = newList.get(newItemPosition); - if (oldItem == newItem) { - return true; - } - if (oldItem == null || newItem == null) { - return false; - } - - return diffCallback.areContentsTheSame(oldItem, newItem); - } - }, detectMoves); - } - - static <T> void dispatchDiff(ListUpdateCallback callback, - final DiffUtil.DiffResult diffResult) { - // Simple case, dispatch & return - diffResult.dispatchUpdatesTo(callback); - } -} diff --git a/android/arch/paging/StringPagedList.java b/android/arch/paging/StringPagedList.java deleted file mode 100644 index 5318d38c..00000000 --- a/android/arch/paging/StringPagedList.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import java.util.Arrays; - -public class StringPagedList extends NullPaddedList<String> { - public StringPagedList(int leadingNulls, int trailingNulls, String... items) { - super(leadingNulls, Arrays.asList(items), trailingNulls); - } -} diff --git a/android/arch/paging/TestExecutor.java b/android/arch/paging/TestExecutor.java deleted file mode 100644 index 30809c3e..00000000 --- a/android/arch/paging/TestExecutor.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import android.support.annotation.NonNull; - -import java.util.LinkedList; -import java.util.Queue; -import java.util.concurrent.Executor; - -public class TestExecutor implements Executor { - private Queue<Runnable> mTasks = new LinkedList<>(); - - @Override - public void execute(@NonNull Runnable command) { - mTasks.add(command); - } - - boolean executeAll() { - boolean consumed = !mTasks.isEmpty(); - Runnable task; - while ((task = mTasks.poll()) != null) { - task.run(); - } - return consumed; - } -} diff --git a/android/arch/paging/TiledDataSource.java b/android/arch/paging/TiledDataSource.java index 36be423d..61dead3a 100644 --- a/android/arch/paging/TiledDataSource.java +++ b/android/arch/paging/TiledDataSource.java @@ -19,6 +19,7 @@ package android.arch.paging; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; +import java.util.Collections; import java.util.List; /** @@ -92,7 +93,6 @@ public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> { * @return Number of items this DataSource can provide. Must be <code>0</code> or greater. */ @WorkerThread - @Override public abstract int countItems(); @Override @@ -118,7 +118,61 @@ public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> { @WorkerThread public abstract List<Type> loadRange(int startPosition, int count); - final List<Type> loadRangeWrapper(int startPosition, int count) { + /** + * blocking, and splits pages + */ + void loadRangeInitial(int startPosition, int count, int pageSize, int itemCount, + PageResult.Receiver<Integer, Type> receiver) { + + if (itemCount == 0) { + // no data to load, just immediately return empty + receiver.onPageResult(new PageResult<>( + PageResult.INIT, new Page<Integer, Type>(Collections.<Type>emptyList()), + 0, 0, startPosition)); + return; + } + + + List<Type> list = loadRangeWrapper(startPosition, count); + + count = Math.min(count, itemCount - startPosition); + + if (list == null) { + // invalid data, pass to receiver + receiver.onPageResult(new PageResult<Integer, Type>( + PageResult.INIT, null, 0, 0, startPosition)); + return; + } + + if (list.size() != count) { + throw new IllegalStateException("Invalid list, requested size: " + count + + ", returned size: " + list.size()); + } + + // emit the results as multiple pages + int pageCount = (count + (pageSize - 1)) / pageSize; + for (int i = 0; i < pageCount; i++) { + int beginInclusive = i * pageSize; + int endExclusive = Math.min(count, (i + 1) * pageSize); + + Page<Integer, Type> page = new Page<>(list.subList(beginInclusive, endExclusive)); + + int leadingNulls = startPosition + beginInclusive; + int trailingNulls = itemCount - leadingNulls - page.items.size(); + receiver.onPageResult(new PageResult<>( + PageResult.INIT, page, leadingNulls, trailingNulls, 0)); + } + } + + void loadRange(int startPosition, int count, PageResult.Receiver<Integer, Type> receiver) { + List<Type> list = loadRangeWrapper(startPosition, count); + + Page<Integer, Type> page = list != null ? new Page<Integer, Type>(list) : null; + receiver.postOnPageResult(new PageResult<>( + PageResult.TILE, page, startPosition, 0, 0)); + } + + private List<Type> loadRangeWrapper(int startPosition, int count) { if (isInvalid()) { return null; } diff --git a/android/arch/paging/TiledPagedList.java b/android/arch/paging/TiledPagedList.java index a2fc064b..934a0dd0 100644 --- a/android/arch/paging/TiledPagedList.java +++ b/android/arch/paging/TiledPagedList.java @@ -16,219 +16,173 @@ package android.arch.paging; +import android.support.annotation.AnyThread; +import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.RestrictTo; import android.support.annotation.WorkerThread; -import java.lang.ref.WeakReference; -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; -/** @hide */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class TiledPagedList<T> extends PageArrayList<T> { +class TiledPagedList<T> extends PagedList<T> + implements PagedStorage.Callback { private final TiledDataSource<T> mDataSource; - private final Executor mMainThreadExecutor; - private final Executor mBackgroundThreadExecutor; - private final Config mConfig; - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final List<T> mLoadingPlaceholder = new AbstractList<T>() { - @Override - public T get(int i) { - return null; - } + @SuppressWarnings("unchecked") + private final PagedStorage<Integer, T> mKeyedStorage = (PagedStorage<Integer, T>) mStorage; + private final PageResult.Receiver<Integer, T> mReceiver = + new PageResult.Receiver<Integer, T>() { + @AnyThread @Override - public int size() { - return 0; + public void postOnPageResult(@NonNull final PageResult<Integer, T> pageResult) { + // NOTE: if we're already on main thread, this can delay page receive by a frame + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + onPageResult(pageResult); + } + }); } - }; - private int mLastLoad = -1; + @MainThread + @Override + public void onPageResult(@NonNull PageResult<Integer, T> pageResult) { + if (pageResult.page == null) { + detach(); + return; + } - private AtomicBoolean mDetached = new AtomicBoolean(false); + if (isDetached()) { + // No op, have detached + return; + } - private ArrayList<WeakReference<Callback>> mCallbacks = new ArrayList<>(); + if (mStorage.getPageCount() == 0) { + mKeyedStorage.init( + pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls, + pageResult.positionOffset, TiledPagedList.this); + } else { + mKeyedStorage.insertPage(pageResult.leadingNulls, pageResult.page, + TiledPagedList.this); + } + } + }; @WorkerThread TiledPagedList(@NonNull TiledDataSource<T> dataSource, @NonNull Executor mainThreadExecutor, @NonNull Executor backgroundThreadExecutor, - Config config, + @NonNull Config config, int position) { - super(config.mPageSize, dataSource.countItems()); - + super(new PagedStorage<Integer, T>(), + mainThreadExecutor, backgroundThreadExecutor, config); mDataSource = dataSource; - mMainThreadExecutor = mainThreadExecutor; - mBackgroundThreadExecutor = backgroundThreadExecutor; - mConfig = config; - - position = Math.min(Math.max(0, position), mCount); - - int firstPage = position / mPageSize; - List<T> firstPageData = dataSource.loadRangeWrapper(firstPage * mPageSize, mPageSize); - if (firstPageData != null) { - mPageIndexOffset = firstPage; - mPages.add(firstPageData); - mLastLoad = position; - } else { - detach(); - return; - } - int secondPage = (position % mPageSize < mPageSize / 2) ? firstPage - 1 : firstPage + 1; - if (secondPage < 0 || secondPage > mMaxPageCount) { - // no second page to load - return; - } - List<T> secondPageData = dataSource.loadRangeWrapper(secondPage * mPageSize, mPageSize); - if (secondPageData != null) { - boolean before = secondPage < firstPage; - mPages.add(before ? 0 : 1, secondPageData); - if (before) { - mPageIndexOffset--; - } - return; - } - detach(); - } + final int pageSize = mConfig.pageSize; - @Override - public void loadAround(int index) { - mLastLoad = index; - - int minimumPage = Math.max((index - mConfig.mPrefetchDistance) / mPageSize, 0); - int maximumPage = Math.min((index + mConfig.mPrefetchDistance) / mPageSize, - mMaxPageCount - 1); + final int itemCount = mDataSource.countItems(); + final int firstLoadSize = Math.min(itemCount, + (Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize); + final int firstLoadPosition = computeFirstLoadPosition( + position, firstLoadSize, pageSize, itemCount); - if (minimumPage < mPageIndexOffset) { - for (int i = 0; i < mPageIndexOffset - minimumPage; i++) { - mPages.add(0, null); - } - mPageIndexOffset = minimumPage; - } - if (maximumPage >= mPageIndexOffset + mPages.size()) { - for (int i = mPages.size(); i <= maximumPage - mPageIndexOffset; i++) { - mPages.add(mPages.size(), null); - } - } - for (int i = minimumPage; i <= maximumPage; i++) { - scheduleLoadPage(i); - } + mDataSource.loadRangeInitial(firstLoadPosition, firstLoadSize, pageSize, + itemCount, mReceiver); } - private void scheduleLoadPage(final int pageIndex) { - final int localPageIndex = pageIndex - mPageIndexOffset; + static int computeFirstLoadPosition(int position, int firstLoadSize, int pageSize, int size) { + int idealStart = position - firstLoadSize / 2; - if (mPages.get(localPageIndex) != null) { - // page is present in list, and non-null - don't need to load - return; - } - mPages.set(localPageIndex, mLoadingPlaceholder); + int roundedPageStart = Math.round(idealStart / pageSize) * pageSize; - mBackgroundThreadExecutor.execute(new Runnable() { - @Override - public void run() { - if (mDetached.get()) { - return; - } - final List<T> data = mDataSource.loadRangeWrapper( - pageIndex * mPageSize, mPageSize); - if (data != null) { - mMainThreadExecutor.execute(new Runnable() { - @Override - public void run() { - if (mDetached.get()) { - return; - } - loadPageImpl(pageIndex, data); - } - }); - } else { - detach(); - } - } - }); + // minimum start position is 0 + roundedPageStart = Math.max(0, roundedPageStart); - } + // maximum start pos is that which will encompass end of list + int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize; + roundedPageStart = Math.min(maximumLoadPage, roundedPageStart); - private void loadPageImpl(int pageIndex, List<T> data) { - int localPageIndex = pageIndex - mPageIndexOffset; + return roundedPageStart; + } - if (mPages.get(localPageIndex) != mLoadingPlaceholder) { - throw new IllegalStateException("Data inserted before requested."); - } - mPages.set(localPageIndex, data); - for (WeakReference<Callback> weakRef : mCallbacks) { - Callback callback = weakRef.get(); - if (callback != null) { - callback.onChanged(pageIndex * mPageSize, data.size()); - } - } + @Override + boolean isContiguous() { + return false; } + @Nullable @Override - public boolean isImmutable() { - // TODO: consider counting loaded pages, return true if mLoadedPages == mMaxPageCount - // Note: could at some point want to support growing past max count, or grow dynamically - return isDetached(); + public Object getLastKey() { + return mLastLoad; } @Override - public void addWeakCallback(@Nullable PagedList<T> previousSnapshot, + protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> pagedListSnapshot, @NonNull Callback callback) { - PageArrayList<T> snapshot = (PageArrayList<T>) previousSnapshot; - if (snapshot != this && snapshot != null) { - // loop through each page and signal the callback for any pages that are present now, - // but not in the snapshot. - for (int i = 0; i < mPages.size(); i++) { - int pageIndex = i + mPageIndexOffset; - int pageCount = 0; - // count number of consecutive pages that were added since the snapshot... - while (pageCount < mPages.size() - && hasPage(pageIndex + pageCount) - && !snapshot.hasPage(pageIndex + pageCount)) { - pageCount++; - } - // and signal them all at once to the callback - if (pageCount > 0) { - callback.onChanged(pageIndex * mPageSize, mPageSize * pageCount); - i += pageCount - 1; - } + //noinspection UnnecessaryLocalVariable + final PagedStorage<?, T> snapshot = pagedListSnapshot.mStorage; + + // loop through each page and signal the callback for any pages that are present now, + // but not in the snapshot. + final int pageSize = mConfig.pageSize; + final int leadingNullPages = mStorage.getLeadingNullCount() / pageSize; + final int pageCount = mStorage.getPageCount(); + for (int i = 0; i < pageCount; i++) { + int pageIndex = i + leadingNullPages; + int updatedPages = 0; + // count number of consecutive pages that were added since the snapshot... + while (updatedPages < mStorage.getPageCount() + && mStorage.hasPage(pageSize, pageIndex + updatedPages) + && !snapshot.hasPage(pageSize, pageIndex + updatedPages)) { + updatedPages++; + } + // and signal them all at once to the callback + if (updatedPages > 0) { + callback.onChanged(pageIndex * pageSize, pageSize * updatedPages); + i += updatedPages - 1; } } - mCallbacks.add(new WeakReference<>(callback)); } @Override - public void removeWeakCallback(@NonNull Callback callback) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - Callback currentCallback = mCallbacks.get(i).get(); - if (currentCallback == null || currentCallback == callback) { - mCallbacks.remove(i); - } - } + protected void loadAroundInternal(int index) { + mStorage.allocatePlaceholders(index, mConfig.prefetchDistance, mConfig.pageSize, this); } @Override - public boolean isDetached() { - return mDetached.get(); + public void onInitialized(int count) { + notifyInserted(0, count); } @Override - public void detach() { - mDetached.set(true); + public void onPagePrepended(int leadingNulls, int changed, int added) { + throw new IllegalStateException("Contiguous callback on TiledPagedList"); } - @Nullable @Override - public Object getLastKey() { - return mLastLoad; + public void onPageAppended(int endPosition, int changed, int added) { + throw new IllegalStateException("Contiguous callback on TiledPagedList"); + } + + @Override + public void onPagePlaceholderInserted(final int pageIndex) { + // placeholder means initialize a load + mBackgroundThreadExecutor.execute(new Runnable() { + @Override + public void run() { + if (isDetached()) { + return; + } + final int pageSize = mConfig.pageSize; + mDataSource.loadRange(pageIndex * pageSize, pageSize, mReceiver); + } + }); + } + + @Override + public void onPageInserted(int start, int count) { + notifyChanged(start, count); } } diff --git a/android/arch/paging/TiledPagedListTest.java b/android/arch/paging/TiledPagedListTest.java deleted file mode 100644 index 4ad02e12..00000000 --- a/android/arch/paging/TiledPagedListTest.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.paging; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -@RunWith(JUnit4.class) -public class TiledPagedListTest { - - private TestExecutor mMainThread = new TestExecutor(); - private TestExecutor mBackgroundThread = new TestExecutor(); - - private static final ArrayList<Item> ITEMS = new ArrayList<>(); - - static { - for (int i = 0; i < 45; i++) { - ITEMS.add(new Item(i)); - } - } - - // use a page size that's not an even divisor of ITEMS.size() to test end conditions - private static final int PAGE_SIZE = 10; - - private static class Item { - private Item(int position) { - this.position = position; - this.name = "Item " + position; - } - - @SuppressWarnings("WeakerAccess") - public final int position; - public final String name; - - @Override - public String toString() { - return name; - } - } - - private static class TestTiledSource extends TiledDataSource<Item> { - @Override - public int countItems() { - return ITEMS.size(); - } - - @Override - public List<Item> loadRange(int startPosition, int count) { - int endPosition = Math.min(ITEMS.size(), startPosition + count); - return ITEMS.subList(startPosition, endPosition); - } - } - - private void verifyRange(PageArrayList<Item> list, Integer... loadedPages) { - List<Integer> loadedPageList = Arrays.asList(loadedPages); - assertEquals(ITEMS.size(), list.size()); - for (int i = 0; i < list.size(); i++) { - if (loadedPageList.contains(i / PAGE_SIZE)) { - assertSame(ITEMS.get(i), list.get(i)); - } else { - assertEquals(null, list.get(i)); - } - } - } - private TiledPagedList<Item> createTiledPagedList(int loadPosition) { - return createTiledPagedList(loadPosition, PAGE_SIZE); - } - - private TiledPagedList<Item> createTiledPagedList(int loadPosition, int prefetchDistance) { - TestTiledSource source = new TestTiledSource(); - return new TiledPagedList<>( - source, mMainThread, mBackgroundThread, - new PagedList.Config.Builder() - .setPageSize(PAGE_SIZE) - .setPrefetchDistance(prefetchDistance) - .build(), - loadPosition); - } - - @Test - public void initialLoad() { - TiledPagedList<Item> pagedList = createTiledPagedList(0); - verifyRange(pagedList, 0); - } - - @Test - public void initialLoad_end() { - TiledPagedList<Item> pagedList = createTiledPagedList(44); - verifyRange(pagedList, 3, 4); - } - - @Test - public void initialLoad_multiple() { - TiledPagedList<Item> pagedList = createTiledPagedList(9); - verifyRange(pagedList, 0, 1); - } - - @Test - public void initialLoad_offset() { - TiledPagedList<Item> pagedList = createTiledPagedList(41); - verifyRange(pagedList, 3, 4); - } - - @Test - public void append() { - TiledPagedList<Item> pagedList = createTiledPagedList(0); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(pagedList, 0); - verifyZeroInteractions(callback); - - pagedList.loadAround(5); - drain(); - - verifyRange(pagedList, 0, 1); - verify(callback).onChanged(10, 10); - verifyNoMoreInteractions(callback); - } - - @Test - public void prepend() { - TiledPagedList<Item> pagedList = createTiledPagedList(44); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(pagedList, 3, 4); - verifyZeroInteractions(callback); - - pagedList.loadAround(35); - drain(); - - verifyRange(pagedList, 2, 3, 4); - verify(callback).onChanged(20, 10); - verifyNoMoreInteractions(callback); - } - - @Test - public void loadWithGap() { - TiledPagedList<Item> pagedList = createTiledPagedList(0); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(pagedList, 0); - verifyZeroInteractions(callback); - - pagedList.loadAround(44); - drain(); - - verifyRange(pagedList, 0, 3, 4); - verify(callback).onChanged(30, 10); - verify(callback).onChanged(40, 5); - verifyNoMoreInteractions(callback); - } - - @Test - public void tinyPrefetchTest() { - TiledPagedList<Item> pagedList = createTiledPagedList(0, 1); - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(null, callback); - verifyRange(pagedList, 0); // just 4 loaded - verifyZeroInteractions(callback); - - pagedList.loadAround(23); - drain(); - - verifyRange(pagedList, 0, 2); - verify(callback).onChanged(20, 10); - verifyNoMoreInteractions(callback); - - pagedList.loadAround(44); - drain(); - - verifyRange(pagedList, 0, 2, 4); - verify(callback).onChanged(40, 5); - verifyNoMoreInteractions(callback); - } - - @Test - public void appendCallbackAddedLate() { - TiledPagedList<Item> pagedList = createTiledPagedList(0, 0); - verifyRange(pagedList, 0); - - pagedList.loadAround(15); - drain(); - verifyRange(pagedList, 0, 1); - - // snapshot at 20 items - PageArrayList<Item> snapshot = (PageArrayList<Item>) pagedList.snapshot(); - verifyRange(snapshot, 0, 1); - - - pagedList.loadAround(25); - pagedList.loadAround(35); - drain(); - verifyRange(pagedList, 0, 1, 2, 3); - verifyRange(snapshot, 0, 1); - - PagedList.Callback callback = mock( - PagedList.Callback.class); - pagedList.addWeakCallback(snapshot, callback); - verify(callback).onChanged(20, 20); - verifyNoMoreInteractions(callback); - } - - - @Test - public void prependCallbackAddedLate() { - TiledPagedList<Item> pagedList = createTiledPagedList(44, 0); - verifyRange(pagedList, 3, 4); - - pagedList.loadAround(25); - drain(); - verifyRange(pagedList, 2, 3, 4); - - // snapshot at 30 items - PageArrayList<Item> snapshot = (PageArrayList<Item>) pagedList.snapshot(); - verifyRange(snapshot, 2, 3, 4); - - - pagedList.loadAround(15); - pagedList.loadAround(5); - drain(); - verifyRange(pagedList, 0, 1, 2, 3, 4); - verifyRange(snapshot, 2, 3, 4); - - PagedList.Callback callback = mock(PagedList.Callback.class); - pagedList.addWeakCallback(snapshot, callback); - verify(callback).onChanged(0, 20); - verifyNoMoreInteractions(callback); - } - - @Test - public void placeholdersDisabled() { - // disable placeholders with config, so we create a contiguous version of the pagedlist - PagedList<Item> pagedList = new PagedList.Builder<Integer, Item>() - .setDataSource(new TestTiledSource()) - .setMainThreadExecutor(mMainThread) - .setBackgroundThreadExecutor(mBackgroundThread) - .setConfig(new PagedList.Config.Builder() - .setPageSize(PAGE_SIZE) - .setPrefetchDistance(PAGE_SIZE) - .setInitialLoadSizeHint(PAGE_SIZE) - .setEnablePlaceholders(false) - .build()) - .setInitialKey(20) - .build(); - - assertTrue(pagedList.isContiguous()); - - ContiguousPagedList<Item> contiguousPagedList = (ContiguousPagedList<Item>) pagedList; - assertEquals(0, contiguousPagedList.getLeadingNullCount()); - assertEquals(PAGE_SIZE, contiguousPagedList.mList.size()); - assertEquals(0, contiguousPagedList.getTrailingNullCount()); - } - - private void drain() { - boolean executed; - do { - executed = mBackgroundThread.executeAll(); - executed |= mMainThread.executeAll(); - } while (executed); - } -} diff --git a/android/arch/persistence/room/InvalidationTracker.java b/android/arch/persistence/room/InvalidationTracker.java index 45ec0289..b31dc13a 100644 --- a/android/arch/persistence/room/InvalidationTracker.java +++ b/android/arch/persistence/room/InvalidationTracker.java @@ -219,7 +219,7 @@ public class InvalidationTracker { * * @param observer The observer which listens the database for changes. */ - public void addObserver(Observer observer) { + public void addObserver(@NonNull Observer observer) { final String[] tableNames = observer.mTables; int[] tableIds = new int[tableNames.length]; final int size = tableNames.length; @@ -265,7 +265,7 @@ public class InvalidationTracker { * @param observer The observer to remove. */ @SuppressWarnings("WeakerAccess") - public void removeObserver(final Observer observer) { + public void removeObserver(@NonNull final Observer observer) { ObserverWrapper wrapper; synchronized (mObserverMap) { wrapper = mObserverMap.remove(observer); diff --git a/android/arch/persistence/room/Relation.java b/android/arch/persistence/room/Relation.java index 72066992..d55bbfe8 100644 --- a/android/arch/persistence/room/Relation.java +++ b/android/arch/persistence/room/Relation.java @@ -28,6 +28,8 @@ import java.lang.annotation.Target; * <pre> * {@literal @}Entity * public class Pet { + * {@literal @} PrimaryKey + * int id; * int userId; * String name; * // other fields @@ -41,8 +43,8 @@ import java.lang.annotation.Target; * * {@literal @}Dao * public interface UserPetDao { - * {@literal @}Query("SELECT id, name from User WHERE age > :minAge") - * public List<UserNameAndAllPets> loadUserAndPets(int minAge); + * {@literal @}Query("SELECT id, name from User") + * public List<UserNameAndAllPets> loadUserAndPets(); * } * </pre> * <p> @@ -63,16 +65,16 @@ import java.lang.annotation.Target; * {@literal @}Embedded * public User user; * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class) - * public List<PetNameAndId> pets; + * public List<PetNameAndId> pets; * } * {@literal @}Dao * public interface UserPetDao { - * {@literal @}Query("SELECT * from User WHERE age > :minAge") - * public List<UserAllPets> loadUserAndPets(int minAge); + * {@literal @}Query("SELECT * from User") + * public List<UserAllPets> loadUserAndPets(); * } * </pre> * <p> - * In the example above, {@code PetNameAndId} is a regular but all of fields are fetched + * In the example above, {@code PetNameAndId} is a regular Pojo but all of fields are fetched * from the {@code entity} defined in the {@code @Relation} annotation (<i>Pet</i>). * {@code PetNameAndId} could also define its own relations all of which would also be fetched * automatically. @@ -85,7 +87,7 @@ import java.lang.annotation.Target; * public User user; * {@literal @}Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class, * projection = {"name"}) - * public List<String> petNames; + * public List<String> petNames; * } * </pre> * <p> @@ -93,7 +95,7 @@ import java.lang.annotation.Target; * cannot have relations. This is a design decision to avoid common pitfalls in {@link Entity} * setups. You can read more about it in the main Room documentation. When loading data, you can * simply work around this limitation by creating Pojo classes that extend the {@link Entity}. - * + * <p> * Note that the {@code @Relation} annotated field cannot be a constructor parameter, it must be * public or have a public setter. */ diff --git a/android/arch/persistence/room/Room.java b/android/arch/persistence/room/Room.java index 8ce4be0c..2850b55e 100644 --- a/android/arch/persistence/room/Room.java +++ b/android/arch/persistence/room/Room.java @@ -43,6 +43,7 @@ public class Room { * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database. */ @SuppressWarnings("WeakerAccess") + @NonNull public static <T extends RoomDatabase> RoomDatabase.Builder<T> databaseBuilder( @NonNull Context context, @NonNull Class<T> klass, @NonNull String name) { //noinspection ConstantConditions @@ -65,6 +66,7 @@ public class Room { * @param <T> The type of the database class. * @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database. */ + @NonNull public static <T extends RoomDatabase> RoomDatabase.Builder<T> inMemoryDatabaseBuilder( @NonNull Context context, @NonNull Class<T> klass) { return new RoomDatabase.Builder<>(context, klass, null); diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java index cdad868d..8c940246 100644 --- a/android/arch/persistence/room/RoomDatabase.java +++ b/android/arch/persistence/room/RoomDatabase.java @@ -49,7 +49,7 @@ import java.util.concurrent.locks.ReentrantLock; * * @see Database */ -@SuppressWarnings({"unused", "WeakerAccess"}) +//@SuppressWarnings({"unused", "WeakerAccess"}) public abstract class RoomDatabase { private static final String DB_IMPL_SUFFIX = "_Impl"; // set by the generated open helper. @@ -153,7 +153,9 @@ public abstract class RoomDatabase { * * @hide */ + @SuppressWarnings("WeakerAccess") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + // used in generated code public void assertNotMainThread() { if (mAllowMainThreadQueries) { return; @@ -298,6 +300,7 @@ public abstract class RoomDatabase { * @return True if there is an active transaction in current thread, false otherwise. * @see SupportSQLiteDatabase#inTransaction() */ + @SuppressWarnings("WeakerAccess") public boolean inTransaction() { return mOpenHelper.getWritableDatabase().inTransaction(); } @@ -307,7 +310,6 @@ public abstract class RoomDatabase { * * @param <T> The type of the abstract database class. */ - @SuppressWarnings("unused") public static class Builder<T extends RoomDatabase> { private final Class<T> mDatabaseClass; private final String mName; @@ -337,7 +339,8 @@ public abstract class RoomDatabase { * @param factory The factory to use to access the database. * @return this */ - public Builder<T> openHelperFactory(SupportSQLiteOpenHelper.Factory factory) { + @NonNull + public Builder<T> openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory) { mFactory = factory; return this; } @@ -361,6 +364,7 @@ public abstract class RoomDatabase { * changes. * @return this */ + @NonNull public Builder<T> addMigrations(Migration... migrations) { mMigrationContainer.addMigrations(migrations); return this; @@ -378,6 +382,7 @@ public abstract class RoomDatabase { * * @return this */ + @NonNull public Builder<T> allowMainThreadQueries() { mAllowMainThreadQueries = true; return this; @@ -400,6 +405,7 @@ public abstract class RoomDatabase { * * @return this */ + @NonNull public Builder<T> fallbackToDestructiveMigration() { mRequireMigration = false; return this; @@ -411,6 +417,7 @@ public abstract class RoomDatabase { * @param callback The callback. * @return this */ + @NonNull public Builder<T> addCallback(@NonNull Callback callback) { if (mCallbacks == null) { mCallbacks = new ArrayList<>(); @@ -427,6 +434,7 @@ public abstract class RoomDatabase { * * @return A new database instance. */ + @NonNull public T build() { //noinspection ConstantConditions if (mContext == null) { @@ -493,6 +501,7 @@ public abstract class RoomDatabase { * @return An ordered list of {@link Migration} objects that should be run to migrate * between the given versions. If a migration path cannot be found, returns {@code null}. */ + @SuppressWarnings("WeakerAccess") @Nullable public List<Migration> findMigrationPath(int start, int end) { if (start == end) { diff --git a/android/arch/persistence/room/RoomWarnings.java b/android/arch/persistence/room/RoomWarnings.java index c64be967..f05e6be2 100644 --- a/android/arch/persistence/room/RoomWarnings.java +++ b/android/arch/persistence/room/RoomWarnings.java @@ -125,4 +125,12 @@ public class RoomWarnings { * annotation. */ public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR"; + + /** + * Reported when a @Query method returns a Pojo that has relations but the method is not + * annotated with @Transaction. Relations are run as separate queries and if the query is not + * run inside a transaction, it might return inconsistent results from the database. + */ + public static final String RELATION_QUERY_WITHOUT_TRANSACTION = + "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"; } diff --git a/android/arch/persistence/room/Transaction.java b/android/arch/persistence/room/Transaction.java index 914e4f41..3b6ede9c 100644 --- a/android/arch/persistence/room/Transaction.java +++ b/android/arch/persistence/room/Transaction.java @@ -22,9 +22,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Marks a method in an abstract {@link Dao} class as a transaction method. + * Marks a method in a {@link Dao} class as a transaction method. * <p> - * The derived implementation of the method will execute the super method in a database transaction. + * When used on a non-abstract method of an abstract {@link Dao} class, + * the derived implementation of the method will execute the super method in a database transaction. * All the parameters and return types are preserved. The transaction will be marked as successful * unless an exception is thrown in the method body. * <p> @@ -44,6 +45,38 @@ import java.lang.annotation.Target; * } * } * </pre> + * <p> + * When used on a {@link Query} method that has a {@code Select} statement, the generated code for + * the Query will be run in a transaction. There are 2 main cases where you may want to do that: + * <ol> + * <li>If the result of the query is fairly big, it is better to run it inside a transaction + * to receive a consistent result. Otherwise, if the query result does not fit into a single + * {@link android.database.CursorWindow CursorWindow}, the query result may be corrupted due to + * changes in the database in between cursor window swaps. + * <li>If the result of the query is a Pojo with {@link Relation} fields, these fields are + * queried separately. To receive consistent results between these queries, you probably want + * to run them in a single transaction. + * </ol> + * Example: + * <pre> + * class ProductWithReviews extends Product { + * {@literal @}Relation(parentColumn = "id", entityColumn = "productId", entity = Review.class) + * public List<Review> reviews; + * } + * {@literal @}Dao + * public interface ProductDao { + * {@literal @}Transaction {@literal @}Query("SELECT * from products") + * public List<ProductWithReviews> loadAll(); + * } + * </pre> + * If the query is an async query (e.g. returns a {@link android.arch.lifecycle.LiveData LiveData} + * or RxJava Flowable, the transaction is properly handled when the query is run, not when the + * method is called. + * <p> + * Putting this annotation on an {@link Insert}, {@link Update} or {@link Delete} method has no + * impact because they are always run inside a transaction. Similarly, if it is annotated with + * {@link Query} but runs an update or delete statement, it is automatically wrapped in a + * transaction. */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.CLASS) diff --git a/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java b/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java index 818c46b4..cdd464e4 100644 --- a/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java +++ b/android/arch/persistence/room/integration/testapp/RoomPagedListActivity.java @@ -86,6 +86,7 @@ public class RoomPagedListActivity extends AppCompatActivity { @Override protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); PagedList<Customer> list = mAdapter.getCurrentList(); if (list == null) { // Can't find anything to restore diff --git a/android/arch/persistence/room/integration/testapp/database/CustomerDao.java b/android/arch/persistence/room/integration/testapp/database/CustomerDao.java index 9d402370..b5df914a 100644 --- a/android/arch/persistence/room/integration/testapp/database/CustomerDao.java +++ b/android/arch/persistence/room/integration/testapp/database/CustomerDao.java @@ -59,7 +59,7 @@ public interface CustomerDao { // Keyed - @Query("SELECT * from customer ORDER BY mLastName ASC LIMIT :limit") + @Query("SELECT * from customer ORDER BY mLastName DESC LIMIT :limit") List<Customer> customerNameInitial(int limit); @Query("SELECT * from customer WHERE mLastName < :key ORDER BY mLastName DESC LIMIT :limit") diff --git a/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java b/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java deleted file mode 100644 index 3cbffc8b..00000000 --- a/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.persistence.room.integration.testapp.db; - -import android.arch.persistence.db.SupportSQLiteDatabase; -import android.arch.persistence.db.SupportSQLiteOpenHelper; - -public class JDBCOpenHelper implements SupportSQLiteOpenHelper { - @Override - public String getDatabaseName() { - return null; - } - - @Override - public void setWriteAheadLoggingEnabled(boolean enabled) { - - } - - @Override - public SupportSQLiteDatabase getWritableDatabase() { - return null; - } - - @Override - public SupportSQLiteDatabase getReadableDatabase() { - return null; - } - - @Override - public void close() { - - } -} diff --git a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java index 84f20ec5..33f40183 100644 --- a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java +++ b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java @@ -17,20 +17,17 @@ package android.arch.persistence.room.integration.testapp.test; import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import android.arch.core.executor.ArchTaskExecutor; -import android.arch.core.executor.TaskExecutor; +import android.arch.core.executor.testing.CountingTaskExecutorRule; import android.arch.persistence.room.InvalidationTracker; import android.arch.persistence.room.Room; import android.arch.persistence.room.integration.testapp.TestDatabase; import android.arch.persistence.room.integration.testapp.dao.UserDao; import android.arch.persistence.room.integration.testapp.vo.User; import android.content.Context; -import android.os.Handler; -import android.os.Looper; import android.support.annotation.NonNull; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; @@ -38,17 +35,13 @@ import android.support.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * Tests invalidation tracking. @@ -56,138 +49,97 @@ import java.util.concurrent.TimeUnit; @SmallTest @RunWith(AndroidJUnit4.class) public class InvalidationTest { + @Rule + public CountingTaskExecutorRule executorRule = new CountingTaskExecutorRule(); private UserDao mUserDao; private TestDatabase mDb; @Before - public void createDb() { + public void createDb() throws TimeoutException, InterruptedException { Context context = InstrumentationRegistry.getTargetContext(); mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build(); mUserDao = mDb.getUserDao(); - } - - @Before - public void setSingleThreadedIO() { - ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { - ExecutorService mIOExecutor = Executors.newSingleThreadExecutor(); - Handler mHandler = new Handler(Looper.getMainLooper()); - - @Override - public void executeOnDiskIO(Runnable runnable) { - mIOExecutor.execute(runnable); - } - - @Override - public void postToMainThread(Runnable runnable) { - mHandler.post(runnable); - } - - @Override - public boolean isMainThread() { - return Thread.currentThread() == Looper.getMainLooper().getThread(); - } - }); + drain(); } @After - public void clearExecutor() { - ArchTaskExecutor.getInstance().setDelegate(null); + public void closeDb() throws TimeoutException, InterruptedException { + mDb.close(); + drain(); } - private void waitUntilIOThreadIsIdle() { - FutureTask<Void> future = new FutureTask<>(new Callable<Void>() { - @Override - public Void call() throws Exception { - return null; - } - }); - ArchTaskExecutor.getInstance().executeOnDiskIO(future); - //noinspection TryWithIdenticalCatches - try { - future.get(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } + private void drain() throws TimeoutException, InterruptedException { + executorRule.drainTasks(1, TimeUnit.MINUTES); } @Test - public void testInvalidationOnUpdate() throws InterruptedException { + public void testInvalidationOnUpdate() throws InterruptedException, TimeoutException { User user = TestUtil.createUser(3); mUserDao.insert(user); - LatchObserver observer = new LatchObserver(1, "User"); + LoggingObserver observer = new LoggingObserver("User"); mDb.getInvalidationTracker().addObserver(observer); + drain(); mUserDao.updateById(3, "foo2"); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(true)); + drain(); assertThat(observer.getInvalidatedTables(), hasSize(1)); assertThat(observer.getInvalidatedTables(), hasItem("User")); } @Test - public void testInvalidationOnDelete() throws InterruptedException { + public void testInvalidationOnDelete() throws InterruptedException, TimeoutException { User user = TestUtil.createUser(3); mUserDao.insert(user); - LatchObserver observer = new LatchObserver(1, "User"); + LoggingObserver observer = new LoggingObserver("User"); mDb.getInvalidationTracker().addObserver(observer); + drain(); mUserDao.delete(user); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(true)); + drain(); assertThat(observer.getInvalidatedTables(), hasSize(1)); assertThat(observer.getInvalidatedTables(), hasItem("User")); } @Test - public void testInvalidationOnInsert() throws InterruptedException { - LatchObserver observer = new LatchObserver(1, "User"); + public void testInvalidationOnInsert() throws InterruptedException, TimeoutException { + LoggingObserver observer = new LoggingObserver("User"); mDb.getInvalidationTracker().addObserver(observer); + drain(); mUserDao.insert(TestUtil.createUser(3)); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(true)); + drain(); assertThat(observer.getInvalidatedTables(), hasSize(1)); assertThat(observer.getInvalidatedTables(), hasItem("User")); } @Test - public void testDontInvalidateOnLateInsert() throws InterruptedException { - LatchObserver observer = new LatchObserver(1, "User"); + public void testDontInvalidateOnLateInsert() throws InterruptedException, TimeoutException { + LoggingObserver observer = new LoggingObserver("User"); mUserDao.insert(TestUtil.createUser(3)); - waitUntilIOThreadIsIdle(); + drain(); mDb.getInvalidationTracker().addObserver(observer); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(false)); + drain(); + assertThat(observer.getInvalidatedTables(), nullValue()); } @Test - public void testMultipleTables() throws InterruptedException { - LatchObserver observer = new LatchObserver(1, "User", "Pet"); + public void testMultipleTables() throws InterruptedException, TimeoutException { + LoggingObserver observer = new LoggingObserver("User", "Pet"); mDb.getInvalidationTracker().addObserver(observer); + drain(); mUserDao.insert(TestUtil.createUser(3)); - waitUntilIOThreadIsIdle(); - assertThat(observer.await(), is(true)); + drain(); assertThat(observer.getInvalidatedTables(), hasSize(1)); assertThat(observer.getInvalidatedTables(), hasItem("User")); } - private static class LatchObserver extends InvalidationTracker.Observer { - CountDownLatch mLatch; - + private static class LoggingObserver extends InvalidationTracker.Observer { private Set<String> mInvalidatedTables; - LatchObserver(int permits, String... tables) { + LoggingObserver(String... tables) { super(tables); - mLatch = new CountDownLatch(permits); - } - - boolean await() throws InterruptedException { - return mLatch.await(5, TimeUnit.SECONDS); } @Override public void onInvalidated(@NonNull Set<String> tables) { mInvalidatedTables = tables; - mLatch.countDown(); } Set<String> getInvalidatedTables() { diff --git a/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java b/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java index e11117e4..2735c05a 100644 --- a/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java +++ b/android/arch/persistence/room/integration/testapp/test/QueryDataSourceTest.java @@ -166,17 +166,13 @@ public class QueryDataSourceTest extends TestDatabaseTest { p = dataSource.loadBefore(15, list.get(0), 10); assertNotNull(p); - for (User u : p) { - list.add(0, u); - } + list.addAll(0, p); assertArrayEquals(Arrays.copyOfRange(expected, 5, 35), list.toArray()); p = dataSource.loadBefore(5, list.get(0), 10); assertNotNull(p); - for (User u : p) { - list.add(0, u); - } + list.addAll(0, p); assertArrayEquals(Arrays.copyOfRange(expected, 0, 35), list.toArray()); } diff --git a/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java new file mode 100644 index 00000000..854c8627 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java @@ -0,0 +1,471 @@ +/* + * 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.arch.persistence.room.integration.testapp.test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import android.arch.core.executor.ArchTaskExecutor; +import android.arch.core.executor.testing.CountingTaskExecutorRule; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Observer; +import android.arch.paging.LivePagedListProvider; +import android.arch.paging.PagedList; +import android.arch.paging.TiledDataSource; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Database; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Ignore; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.PrimaryKey; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Relation; +import android.arch.persistence.room.Room; +import android.arch.persistence.room.RoomDatabase; +import android.arch.persistence.room.RoomWarnings; +import android.arch.persistence.room.Transaction; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Single; +import io.reactivex.observers.TestObserver; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subscribers.TestSubscriber; + +@SmallTest +@RunWith(Parameterized.class) +public class QueryTransactionTest { + @Rule + public CountingTaskExecutorRule countingTaskExecutorRule = new CountingTaskExecutorRule(); + private static final AtomicInteger sStartedTransactionCount = new AtomicInteger(0); + private TransactionDb mDb; + private final boolean mUseTransactionDao; + private Entity1Dao mDao; + private final LiveDataQueryTest.TestLifecycleOwner mLifecycleOwner = new LiveDataQueryTest + .TestLifecycleOwner(); + + @NonNull + @Parameterized.Parameters(name = "useTransaction_{0}") + public static Boolean[] getParams() { + return new Boolean[]{false, true}; + } + + public QueryTransactionTest(boolean useTransactionDao) { + mUseTransactionDao = useTransactionDao; + } + + @Before + public void initDb() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START); + } + }); + + resetTransactionCount(); + mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), + TransactionDb.class).build(); + mDao = mUseTransactionDao ? mDb.transactionDao() : mDb.dao(); + drain(); + } + + @After + public void closeDb() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + mLifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY); + } + }); + drain(); + mDb.close(); + } + + @Test + public void readList() { + mDao.insert(new Entity1(1, "foo")); + resetTransactionCount(); + + int expectedTransactionCount = mUseTransactionDao ? 1 : 0; + List<Entity1> allEntities = mDao.allEntities(); + assertTransactionCount(allEntities, expectedTransactionCount); + } + + @Test + public void liveData() { + LiveData<List<Entity1>> listLiveData = mDao.liveData(); + observeForever(listLiveData); + drain(); + assertThat(listLiveData.getValue(), is(Collections.<Entity1>emptyList())); + + resetTransactionCount(); + mDao.insert(new Entity1(1, "foo")); + drain(); + + //noinspection ConstantConditions + assertThat(listLiveData.getValue().size(), is(1)); + int expectedTransactionCount = mUseTransactionDao ? 2 : 1; + assertTransactionCount(listLiveData.getValue(), expectedTransactionCount); + } + + @Test + public void flowable() { + Flowable<List<Entity1>> flowable = mDao.flowable(); + TestSubscriber<List<Entity1>> subscriber = observe(flowable); + drain(); + assertThat(subscriber.values().size(), is(1)); + + resetTransactionCount(); + mDao.insert(new Entity1(1, "foo")); + drain(); + + List<Entity1> allEntities = subscriber.values().get(1); + assertThat(allEntities.size(), is(1)); + int expectedTransactionCount = mUseTransactionDao ? 2 : 1; + assertTransactionCount(allEntities, expectedTransactionCount); + } + + @Test + public void maybe() { + mDao.insert(new Entity1(1, "foo")); + resetTransactionCount(); + + int expectedTransactionCount = mUseTransactionDao ? 1 : 0; + Maybe<List<Entity1>> listMaybe = mDao.maybe(); + TestObserver<List<Entity1>> observer = observe(listMaybe); + drain(); + List<Entity1> allEntities = observer.values().get(0); + assertTransactionCount(allEntities, expectedTransactionCount); + } + + @Test + public void single() { + mDao.insert(new Entity1(1, "foo")); + resetTransactionCount(); + + int expectedTransactionCount = mUseTransactionDao ? 1 : 0; + Single<List<Entity1>> listMaybe = mDao.single(); + TestObserver<List<Entity1>> observer = observe(listMaybe); + drain(); + List<Entity1> allEntities = observer.values().get(0); + assertTransactionCount(allEntities, expectedTransactionCount); + } + + @Test + public void relation() { + mDao.insert(new Entity1(1, "foo")); + mDao.insert(new Child(1, 1)); + mDao.insert(new Child(2, 1)); + resetTransactionCount(); + + List<Entity1WithChildren> result = mDao.withRelation(); + int expectedTransactionCount = mUseTransactionDao ? 1 : 0; + assertTransactionCountWithChildren(result, expectedTransactionCount); + } + + @Test + public void pagedList() { + LiveData<PagedList<Entity1>> pagedList = mDao.pagedList().create(null, 10); + observeForever(pagedList); + drain(); + assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 0 : 0)); + + mDao.insert(new Entity1(1, "foo")); + drain(); + //noinspection ConstantConditions + assertThat(pagedList.getValue().size(), is(1)); + assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 2 : 1); + + mDao.insert(new Entity1(2, "bar")); + drain(); + assertThat(pagedList.getValue().size(), is(2)); + assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 4 : 2); + } + + @Test + public void dataSource() { + mDao.insert(new Entity1(2, "bar")); + drain(); + resetTransactionCount(); + TiledDataSource<Entity1> dataSource = mDao.dataSource(); + dataSource.loadRange(0, 10); + assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0)); + } + + private void assertTransactionCount(List<Entity1> allEntities, int expectedTransactionCount) { + assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount)); + assertThat(allEntities.isEmpty(), is(false)); + for (Entity1 entity1 : allEntities) { + assertThat(entity1.transactionId, is(expectedTransactionCount)); + } + } + + private void assertTransactionCountWithChildren(List<Entity1WithChildren> allEntities, + int expectedTransactionCount) { + assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount)); + assertThat(allEntities.isEmpty(), is(false)); + for (Entity1WithChildren entity1 : allEntities) { + assertThat(entity1.transactionId, is(expectedTransactionCount)); + assertThat(entity1.children, notNullValue()); + assertThat(entity1.children.isEmpty(), is(false)); + for (Child child : entity1.children) { + assertThat(child.transactionId, is(expectedTransactionCount)); + } + } + } + + private void resetTransactionCount() { + sStartedTransactionCount.set(0); + } + + private void drain() { + try { + countingTaskExecutorRule.drainTasks(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new AssertionError("interrupted", e); + } catch (TimeoutException e) { + throw new AssertionError("drain timed out", e); + } + } + + private <T> TestSubscriber<T> observe(final Flowable<T> flowable) { + TestSubscriber<T> subscriber = new TestSubscriber<>(); + flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor())) + .subscribeWith(subscriber); + return subscriber; + } + + private <T> TestObserver<T> observe(final Maybe<T> maybe) { + TestObserver<T> observer = new TestObserver<>(); + maybe.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor())) + .subscribeWith(observer); + return observer; + } + + private <T> TestObserver<T> observe(final Single<T> single) { + TestObserver<T> observer = new TestObserver<>(); + single.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor())) + .subscribeWith(observer); + return observer; + } + + private <T> void observeForever(final LiveData<T> liveData) { + FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() { + @Override + public Void call() throws Exception { + liveData.observe(mLifecycleOwner, new Observer<T>() { + @Override + public void onChanged(@Nullable T t) { + + } + }); + return null; + } + }); + ArchTaskExecutor.getMainThreadExecutor().execute(futureTask); + try { + futureTask.get(); + } catch (InterruptedException e) { + throw new AssertionError("interrupted", e); + } catch (ExecutionException e) { + throw new AssertionError("execution error", e); + } + } + + @SuppressWarnings("WeakerAccess") + static class Entity1WithChildren extends Entity1 { + @Relation(entity = Child.class, parentColumn = "id", + entityColumn = "entity1Id") + public List<Child> children; + + Entity1WithChildren(int id, String value) { + super(id, value); + } + } + + @SuppressWarnings("WeakerAccess") + @Entity + static class Child { + @PrimaryKey(autoGenerate = true) + public int id; + public int entity1Id; + @Ignore + public final int transactionId = sStartedTransactionCount.get(); + + Child(int id, int entity1Id) { + this.id = id; + this.entity1Id = entity1Id; + } + } + + @SuppressWarnings("WeakerAccess") + @Entity + static class Entity1 { + @PrimaryKey(autoGenerate = true) + public int id; + public String value; + @Ignore + public final int transactionId = sStartedTransactionCount.get(); + + Entity1(int id, String value) { + this.id = id; + this.value = value; + } + } + + // we don't support dao inheritance for queries so for now, go with this + interface Entity1Dao { + String SELECT_ALL = "select * from Entity1"; + + List<Entity1> allEntities(); + + Flowable<List<Entity1>> flowable(); + + Maybe<List<Entity1>> maybe(); + + Single<List<Entity1>> single(); + + LiveData<List<Entity1>> liveData(); + + List<Entity1WithChildren> withRelation(); + + LivePagedListProvider<Integer, Entity1> pagedList(); + + TiledDataSource<Entity1> dataSource(); + + @Insert + void insert(Entity1 entity1); + + @Insert + void insert(Child entity1); + } + + @Dao + interface EntityDao extends Entity1Dao { + @Override + @Query(SELECT_ALL) + List<Entity1> allEntities(); + + @Override + @Query(SELECT_ALL) + Flowable<List<Entity1>> flowable(); + + @Override + @Query(SELECT_ALL) + LiveData<List<Entity1>> liveData(); + + @Override + @Query(SELECT_ALL) + Maybe<List<Entity1>> maybe(); + + @Override + @Query(SELECT_ALL) + Single<List<Entity1>> single(); + + @Override + @Query(SELECT_ALL) + @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION) + List<Entity1WithChildren> withRelation(); + + @Override + @Query(SELECT_ALL) + LivePagedListProvider<Integer, Entity1> pagedList(); + + @Override + @Query(SELECT_ALL) + TiledDataSource<Entity1> dataSource(); + } + + @Dao + interface TransactionDao extends Entity1Dao { + @Override + @Transaction + @Query(SELECT_ALL) + List<Entity1> allEntities(); + + @Override + @Transaction + @Query(SELECT_ALL) + Flowable<List<Entity1>> flowable(); + + @Override + @Transaction + @Query(SELECT_ALL) + LiveData<List<Entity1>> liveData(); + + @Override + @Transaction + @Query(SELECT_ALL) + Maybe<List<Entity1>> maybe(); + + @Override + @Transaction + @Query(SELECT_ALL) + Single<List<Entity1>> single(); + + @Override + @Transaction + @Query(SELECT_ALL) + List<Entity1WithChildren> withRelation(); + + @Override + @Transaction + @Query(SELECT_ALL) + LivePagedListProvider<Integer, Entity1> pagedList(); + + @Override + @Transaction + @Query(SELECT_ALL) + TiledDataSource<Entity1> dataSource(); + } + + @Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false) + abstract static class TransactionDb extends RoomDatabase { + abstract EntityDao dao(); + + abstract TransactionDao transactionDao(); + + @Override + public void beginTransaction() { + super.beginTransaction(); + sStartedTransactionCount.incrementAndGet(); + } + } +} diff --git a/android/arch/persistence/room/migration/Migration.java b/android/arch/persistence/room/migration/Migration.java index 907e624b..d69ea0dc 100644 --- a/android/arch/persistence/room/migration/Migration.java +++ b/android/arch/persistence/room/migration/Migration.java @@ -17,6 +17,7 @@ package android.arch.persistence.room.migration; import android.arch.persistence.db.SupportSQLiteDatabase; +import android.support.annotation.NonNull; /** * Base class for a database migration. @@ -58,5 +59,5 @@ public abstract class Migration { * * @param database The database instance */ - public abstract void migrate(SupportSQLiteDatabase database); + public abstract void migrate(@NonNull SupportSQLiteDatabase database); } diff --git a/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java b/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java index 1467a4f0..d72cf8cb 100644 --- a/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java +++ b/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java @@ -16,13 +16,18 @@ package android.arch.persistence.room.migration.bundle; +import android.support.annotation.RestrictTo; + import com.google.gson.annotations.SerializedName; import java.util.List; /** * Holds the information about a foreign key reference. + * + * @hide */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ForeignKeyBundle { @SerializedName("table") private String mTable; diff --git a/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/android/arch/persistence/room/paging/LimitOffsetDataSource.java index 800514cc..2f9a8882 100644 --- a/android/arch/persistence/room/paging/LimitOffsetDataSource.java +++ b/android/arch/persistence/room/paging/LimitOffsetDataSource.java @@ -49,10 +49,13 @@ public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> { private final RoomDatabase mDb; @SuppressWarnings("FieldCanBeLocal") private final InvalidationTracker.Observer mObserver; + private final boolean mInTransaction; - protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query, String... tables) { + protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query, + boolean inTransaction, String... tables) { mDb = db; mSourceQuery = query; + mInTransaction = inTransaction; mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )"; mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?"; mObserver = new InvalidationTracker.Observer(tables) { @@ -98,13 +101,30 @@ public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> { sqLiteQuery.copyArgumentsFrom(mSourceQuery); sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount); sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition); - Cursor cursor = mDb.query(sqLiteQuery); - - try { - return convertRows(cursor); - } finally { - cursor.close(); - sqLiteQuery.release(); + if (mInTransaction) { + mDb.beginTransaction(); + Cursor cursor = null; + try { + cursor = mDb.query(sqLiteQuery); + List<T> rows = convertRows(cursor); + mDb.setTransactionSuccessful(); + return rows; + } finally { + if (cursor != null) { + cursor.close(); + } + mDb.endTransaction(); + sqLiteQuery.release(); + } + } else { + Cursor cursor = mDb.query(sqLiteQuery); + //noinspection TryFinallyCanBeTryWithResources + try { + return convertRows(cursor); + } finally { + cursor.close(); + sqLiteQuery.release(); + } } } } diff --git a/android/arch/persistence/room/util/StringUtil.java b/android/arch/persistence/room/util/StringUtil.java index bee05ddd..d01e3c53 100644 --- a/android/arch/persistence/room/util/StringUtil.java +++ b/android/arch/persistence/room/util/StringUtil.java @@ -17,6 +17,7 @@ package android.arch.persistence.room.util; import android.support.annotation.Nullable; +import android.support.annotation.RestrictTo; import android.util.Log; import java.util.ArrayList; @@ -24,10 +25,14 @@ import java.util.List; import java.util.StringTokenizer; /** + * @hide + * * String utilities for Room */ -@SuppressWarnings("WeakerAccess") +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class StringUtil { + + @SuppressWarnings("unused") public static final String[] EMPTY_STRING_ARRAY = new String[0]; /** * Returns a new StringBuilder to be used while producing SQL queries. diff --git a/android/bluetooth/BluetoothAdapter.java b/android/bluetooth/BluetoothAdapter.java index 84765f6d..578a5b8b 100644 --- a/android/bluetooth/BluetoothAdapter.java +++ b/android/bluetooth/BluetoothAdapter.java @@ -1134,8 +1134,32 @@ public final class BluetoothAdapter { } /** - * Sets the {@link BluetoothClass} Bluetooth Class of Device (CoD) of - * the local Bluetooth adapter. + * Returns the {@link BluetoothClass} Bluetooth Class of Device (CoD) of the local Bluetooth + * adapter. + * + * @return {@link BluetoothClass} Bluetooth CoD of local Bluetooth device. + * + * @hide + */ + @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) + public BluetoothClass getBluetoothClass() { + if (getState() != STATE_ON) return null; + try { + mServiceLock.readLock().lock(); + if (mService != null) return mService.getBluetoothClass(); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } finally { + mServiceLock.readLock().unlock(); + } + return null; + } + + /** + * Sets the {@link BluetoothClass} Bluetooth Class of Device (CoD) of the local Bluetooth + * adapter. + * + * <p>Note: This value persists across system reboot. * * @param bluetoothClass {@link BluetoothClass} to set the local Bluetooth adapter to. * @return true if successful, false if unsuccessful. @@ -2104,8 +2128,8 @@ public final class BluetoothAdapter { } else if (profile == BluetoothProfile.AVRCP_CONTROLLER) { BluetoothAvrcpController avrcp = new BluetoothAvrcpController(context, listener); return true; - } else if (profile == BluetoothProfile.INPUT_DEVICE) { - BluetoothInputDevice iDev = new BluetoothInputDevice(context, listener); + } else if (profile == BluetoothProfile.HID_HOST) { + BluetoothHidHost iDev = new BluetoothHidHost(context, listener); return true; } else if (profile == BluetoothProfile.PAN) { BluetoothPan pan = new BluetoothPan(context, listener); @@ -2128,8 +2152,8 @@ public final class BluetoothAdapter { } else if (profile == BluetoothProfile.MAP_CLIENT) { BluetoothMapClient mapClient = new BluetoothMapClient(context, listener); return true; - } else if (profile == BluetoothProfile.INPUT_HOST) { - BluetoothInputHost iHost = new BluetoothInputHost(context, listener); + } else if (profile == BluetoothProfile.HID_DEVICE) { + BluetoothHidDevice hidDevice = new BluetoothHidDevice(context, listener); return true; } else { return false; @@ -2167,8 +2191,8 @@ public final class BluetoothAdapter { BluetoothAvrcpController avrcp = (BluetoothAvrcpController) proxy; avrcp.close(); break; - case BluetoothProfile.INPUT_DEVICE: - BluetoothInputDevice iDev = (BluetoothInputDevice) proxy; + case BluetoothProfile.HID_HOST: + BluetoothHidHost iDev = (BluetoothHidHost) proxy; iDev.close(); break; case BluetoothProfile.PAN: @@ -2207,9 +2231,9 @@ public final class BluetoothAdapter { BluetoothMapClient mapClient = (BluetoothMapClient) proxy; mapClient.close(); break; - case BluetoothProfile.INPUT_HOST: - BluetoothInputHost iHost = (BluetoothInputHost) proxy; - iHost.close(); + case BluetoothProfile.HID_DEVICE: + BluetoothHidDevice hidDevice = (BluetoothHidDevice) proxy; + hidDevice.close(); break; } } @@ -2277,6 +2301,8 @@ public final class BluetoothAdapter { * * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean enableNoAutoConnect() { if (isEnabled()) { if (DBG) Log.d(TAG, "enableNoAutoConnect(): BT already enabled!"); diff --git a/android/bluetooth/BluetoothDevice.java b/android/bluetooth/BluetoothDevice.java index d982bb7f..ad7a93cd 100644 --- a/android/bluetooth/BluetoothDevice.java +++ b/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/android/bluetooth/BluetoothHeadset.java b/android/bluetooth/BluetoothHeadset.java index 85550c77..1241f230 100644 --- a/android/bluetooth/BluetoothHeadset.java +++ b/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/android/bluetooth/BluetoothInputHost.java b/android/bluetooth/BluetoothHidDevice.java index e18d9d1b..179f36da 100644 --- a/android/bluetooth/BluetoothInputHost.java +++ b/android/bluetooth/BluetoothHidDevice.java @@ -33,9 +33,9 @@ import java.util.List; /** * @hide */ -public final class BluetoothInputHost implements BluetoothProfile { +public final class BluetoothHidDevice implements BluetoothProfile { - private static final String TAG = BluetoothInputHost.class.getSimpleName(); + private static final String TAG = BluetoothHidDevice.class.getSimpleName(); /** * Intent used to broadcast the change in connection state of the Input @@ -57,7 +57,7 @@ public final class BluetoothInputHost implements BluetoothProfile { */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_CONNECTION_STATE_CHANGED = - "android.bluetooth.inputhost.profile.action.CONNECTION_STATE_CHANGED"; + "android.bluetooth.hiddevice.profile.action.CONNECTION_STATE_CHANGED"; /** * Constants representing device subclass. @@ -113,7 +113,7 @@ public final class BluetoothInputHost implements BluetoothProfile { private ServiceListener mServiceListener; - private volatile IBluetoothInputHost mService; + private volatile IBluetoothHidDevice mService; private BluetoothAdapter mAdapter; @@ -205,23 +205,23 @@ public final class BluetoothInputHost implements BluetoothProfile { private final ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { Log.d(TAG, "onServiceConnected()"); - mService = IBluetoothInputHost.Stub.asInterface(service); + mService = IBluetoothHidDevice.Stub.asInterface(service); if (mServiceListener != null) { - mServiceListener.onServiceConnected(BluetoothProfile.INPUT_HOST, - BluetoothInputHost.this); + mServiceListener.onServiceConnected(BluetoothProfile.HID_DEVICE, + BluetoothHidDevice.this); } } public void onServiceDisconnected(ComponentName className) { Log.d(TAG, "onServiceDisconnected()"); mService = null; if (mServiceListener != null) { - mServiceListener.onServiceDisconnected(BluetoothProfile.INPUT_HOST); + mServiceListener.onServiceDisconnected(BluetoothProfile.HID_DEVICE); } } }; - BluetoothInputHost(Context context, ServiceListener listener) { - Log.v(TAG, "BluetoothInputHost"); + BluetoothHidDevice(Context context, ServiceListener listener) { + Log.v(TAG, "BluetoothHidDevice"); mContext = context; mServiceListener = listener; @@ -240,7 +240,7 @@ public final class BluetoothInputHost implements BluetoothProfile { } boolean doBind() { - Intent intent = new Intent(IBluetoothInputHost.class.getName()); + Intent intent = new Intent(IBluetoothHidDevice.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, @@ -285,7 +285,7 @@ public final class BluetoothInputHost implements BluetoothProfile { public List<BluetoothDevice> getConnectedDevices() { Log.v(TAG, "getConnectedDevices()"); - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { return service.getConnectedDevices(); @@ -306,7 +306,7 @@ public final class BluetoothInputHost implements BluetoothProfile { public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { Log.v(TAG, "getDevicesMatchingConnectionStates(): states=" + Arrays.toString(states)); - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { return service.getDevicesMatchingConnectionStates(states); @@ -327,7 +327,7 @@ public final class BluetoothInputHost implements BluetoothProfile { public int getConnectionState(BluetoothDevice device) { Log.v(TAG, "getConnectionState(): device=" + device); - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { return service.getConnectionState(device); @@ -367,7 +367,7 @@ public final class BluetoothInputHost implements BluetoothProfile { return false; } - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { BluetoothHidDeviceAppConfiguration config = @@ -401,7 +401,7 @@ public final class BluetoothInputHost implements BluetoothProfile { boolean result = false; - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { result = service.unregisterApp(config); @@ -426,7 +426,7 @@ public final class BluetoothInputHost implements BluetoothProfile { public boolean sendReport(BluetoothDevice device, int id, byte[] data) { boolean result = false; - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { result = service.sendReport(device, id, data); @@ -454,7 +454,7 @@ public final class BluetoothInputHost implements BluetoothProfile { boolean result = false; - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { result = service.replyReport(device, type, id, data); @@ -480,7 +480,7 @@ public final class BluetoothInputHost implements BluetoothProfile { boolean result = false; - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { result = service.reportError(device, error); @@ -504,7 +504,7 @@ public final class BluetoothInputHost implements BluetoothProfile { boolean result = false; - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { result = service.unplug(device); @@ -529,7 +529,7 @@ public final class BluetoothInputHost implements BluetoothProfile { boolean result = false; - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { result = service.connect(device); @@ -553,7 +553,7 @@ public final class BluetoothInputHost implements BluetoothProfile { boolean result = false; - final IBluetoothInputHost service = mService; + final IBluetoothHidDevice service = mService; if (service != null) { try { result = service.disconnect(device); diff --git a/android/bluetooth/BluetoothInputDevice.java b/android/bluetooth/BluetoothHidHost.java index 32615761..8ad0f9d0 100644 --- a/android/bluetooth/BluetoothInputDevice.java +++ b/android/bluetooth/BluetoothHidHost.java @@ -35,16 +35,16 @@ import java.util.List; * This class provides the public APIs to control the Bluetooth Input * Device Profile. * - * <p>BluetoothInputDevice is a proxy object for controlling the Bluetooth + * <p>BluetoothHidHost is a proxy object for controlling the Bluetooth * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get - * the BluetoothInputDevice proxy object. + * the BluetoothHidHost proxy object. * * <p>Each method is protected with its appropriate permission. * * @hide */ -public final class BluetoothInputDevice implements BluetoothProfile { - private static final String TAG = "BluetoothInputDevice"; +public final class BluetoothHidHost implements BluetoothProfile { + private static final String TAG = "BluetoothHidHost"; private static final boolean DBG = true; private static final boolean VDBG = false; @@ -177,52 +177,52 @@ public final class BluetoothInputDevice implements BluetoothProfile { * @hide */ public static final String EXTRA_PROTOCOL_MODE = - "android.bluetooth.BluetoothInputDevice.extra.PROTOCOL_MODE"; + "android.bluetooth.BluetoothHidHost.extra.PROTOCOL_MODE"; /** * @hide */ public static final String EXTRA_REPORT_TYPE = - "android.bluetooth.BluetoothInputDevice.extra.REPORT_TYPE"; + "android.bluetooth.BluetoothHidHost.extra.REPORT_TYPE"; /** * @hide */ public static final String EXTRA_REPORT_ID = - "android.bluetooth.BluetoothInputDevice.extra.REPORT_ID"; + "android.bluetooth.BluetoothHidHost.extra.REPORT_ID"; /** * @hide */ public static final String EXTRA_REPORT_BUFFER_SIZE = - "android.bluetooth.BluetoothInputDevice.extra.REPORT_BUFFER_SIZE"; + "android.bluetooth.BluetoothHidHost.extra.REPORT_BUFFER_SIZE"; /** * @hide */ - public static final String EXTRA_REPORT = "android.bluetooth.BluetoothInputDevice.extra.REPORT"; + public static final String EXTRA_REPORT = "android.bluetooth.BluetoothHidHost.extra.REPORT"; /** * @hide */ - public static final String EXTRA_STATUS = "android.bluetooth.BluetoothInputDevice.extra.STATUS"; + public static final String EXTRA_STATUS = "android.bluetooth.BluetoothHidHost.extra.STATUS"; /** * @hide */ public static final String EXTRA_VIRTUAL_UNPLUG_STATUS = - "android.bluetooth.BluetoothInputDevice.extra.VIRTUAL_UNPLUG_STATUS"; + "android.bluetooth.BluetoothHidHost.extra.VIRTUAL_UNPLUG_STATUS"; /** * @hide */ public static final String EXTRA_IDLE_TIME = - "android.bluetooth.BluetoothInputDevice.extra.IDLE_TIME"; + "android.bluetooth.BluetoothHidHost.extra.IDLE_TIME"; private Context mContext; private ServiceListener mServiceListener; private BluetoothAdapter mAdapter; - private volatile IBluetoothInputDevice mService; + private volatile IBluetoothHidHost mService; private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = new IBluetoothStateChangeCallback.Stub() { @@ -254,10 +254,10 @@ public final class BluetoothInputDevice implements BluetoothProfile { }; /** - * Create a BluetoothInputDevice proxy object for interacting with the local + * Create a BluetoothHidHost proxy object for interacting with the local * Bluetooth Service which handles the InputDevice profile */ - /*package*/ BluetoothInputDevice(Context context, ServiceListener l) { + /*package*/ BluetoothHidHost(Context context, ServiceListener l) { mContext = context; mServiceListener = l; mAdapter = BluetoothAdapter.getDefaultAdapter(); @@ -275,7 +275,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { } boolean doBind() { - Intent intent = new Intent(IBluetoothInputDevice.class.getName()); + Intent intent = new Intent(IBluetoothHidHost.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, @@ -331,7 +331,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean connect(BluetoothDevice device) { if (DBG) log("connect(" + device + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.connect(device); @@ -371,7 +371,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean disconnect(BluetoothDevice device) { if (DBG) log("disconnect(" + device + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.disconnect(device); @@ -390,7 +390,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { @Override public List<BluetoothDevice> getConnectedDevices() { if (VDBG) log("getConnectedDevices()"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled()) { try { return service.getConnectedDevices(); @@ -409,7 +409,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { @Override public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { if (VDBG) log("getDevicesMatchingStates()"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled()) { try { return service.getDevicesMatchingConnectionStates(states); @@ -428,7 +428,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { @Override public int getConnectionState(BluetoothDevice device) { if (VDBG) log("getState(" + device + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.getConnectionState(device); @@ -458,7 +458,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean setPriority(BluetoothDevice device, int priority) { if (DBG) log("setPriority(" + device + ", " + priority + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { if (priority != BluetoothProfile.PRIORITY_OFF && priority != BluetoothProfile.PRIORITY_ON) { @@ -490,7 +490,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public int getPriority(BluetoothDevice device) { if (VDBG) log("getPriority(" + device + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.getPriority(device); @@ -506,11 +506,11 @@ public final class BluetoothInputDevice implements BluetoothProfile { private final ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { if (DBG) Log.d(TAG, "Proxy object connected"); - mService = IBluetoothInputDevice.Stub.asInterface(Binder.allowBlocking(service)); + mService = IBluetoothHidHost.Stub.asInterface(Binder.allowBlocking(service)); if (mServiceListener != null) { - mServiceListener.onServiceConnected(BluetoothProfile.INPUT_DEVICE, - BluetoothInputDevice.this); + mServiceListener.onServiceConnected(BluetoothProfile.HID_HOST, + BluetoothHidHost.this); } } @@ -518,7 +518,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { if (DBG) Log.d(TAG, "Proxy object disconnected"); mService = null; if (mServiceListener != null) { - mServiceListener.onServiceDisconnected(BluetoothProfile.INPUT_DEVICE); + mServiceListener.onServiceDisconnected(BluetoothProfile.HID_HOST); } } }; @@ -542,7 +542,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean virtualUnplug(BluetoothDevice device) { if (DBG) log("virtualUnplug(" + device + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.virtualUnplug(device); @@ -568,7 +568,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean getProtocolMode(BluetoothDevice device) { if (VDBG) log("getProtocolMode(" + device + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.getProtocolMode(device); @@ -592,7 +592,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean setProtocolMode(BluetoothDevice device, int protocolMode) { if (DBG) log("setProtocolMode(" + device + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.setProtocolMode(device, protocolMode); @@ -623,7 +623,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { log("getReport(" + device + "), reportType=" + reportType + " reportId=" + reportId + "bufferSize=" + bufferSize); } - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.getReport(device, reportType, reportId, bufferSize); @@ -649,7 +649,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean setReport(BluetoothDevice device, byte reportType, String report) { if (VDBG) log("setReport(" + device + "), reportType=" + reportType + " report=" + report); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.setReport(device, reportType, report); @@ -674,7 +674,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean sendData(BluetoothDevice device, String report) { if (DBG) log("sendData(" + device + "), report=" + report); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.sendData(device, report); @@ -698,7 +698,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean getIdleTime(BluetoothDevice device) { if (DBG) log("getIdletime(" + device + ")"); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.getIdleTime(device); @@ -723,7 +723,7 @@ public final class BluetoothInputDevice implements BluetoothProfile { */ public boolean setIdleTime(BluetoothDevice device, byte idleTime) { if (DBG) log("setIdletime(" + device + "), idleTime=" + idleTime); - final IBluetoothInputDevice service = mService; + final IBluetoothHidHost service = mService; if (service != null && isEnabled() && isValidDevice(device)) { try { return service.setIdleTime(device, idleTime); diff --git a/android/bluetooth/BluetoothProfile.java b/android/bluetooth/BluetoothProfile.java index bc8fa846..46a230b5 100644 --- a/android/bluetooth/BluetoothProfile.java +++ b/android/bluetooth/BluetoothProfile.java @@ -73,11 +73,11 @@ public interface BluetoothProfile { public static final int HEALTH = 3; /** - * Input Device Profile + * HID Host * * @hide */ - public static final int INPUT_DEVICE = 4; + public static final int HID_HOST = 4; /** * PAN Profile @@ -152,11 +152,11 @@ public interface BluetoothProfile { public static final int MAP_CLIENT = 18; /** - * Input Host + * HID Device * * @hide */ - public static final int INPUT_HOST = 19; + public static final int HID_DEVICE = 19; /** * Max profile ID. This value should be updated whenever a new profile is added to match diff --git a/android/content/ContentProvider.java b/android/content/ContentProvider.java index 5b2bf456..cdeaea3e 100644 --- a/android/content/ContentProvider.java +++ b/android/content/ContentProvider.java @@ -2099,8 +2099,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { public static Uri maybeAddUserId(Uri uri, int userId) { if (uri == null) return null; if (userId != UserHandle.USER_CURRENT - && (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) - || ContentResolver.SCHEME_SLICE.equals(uri.getScheme()))) { + && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { if (!uriHasUserId(uri)) { //We don't add the user Id if there's already one Uri.Builder builder = uri.buildUpon(); diff --git a/android/content/ContentResolver.java b/android/content/ContentResolver.java index 02e70f55..9ccc552f 100644 --- a/android/content/ContentResolver.java +++ b/android/content/ContentResolver.java @@ -47,8 +47,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; -import android.slice.Slice; -import android.slice.SliceProvider; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; @@ -180,8 +178,6 @@ public abstract class ContentResolver { public static final Intent ACTION_SYNC_CONN_STATUS_CHANGED = new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED"); - /** @hide */ - public static final String SCHEME_SLICE = "slice"; public static final String SCHEME_CONTENT = "content"; public static final String SCHEME_ANDROID_RESOURCE = "android.resource"; public static final String SCHEME_FILE = "file"; @@ -1722,36 +1718,6 @@ public abstract class ContentResolver { } /** - * Turns a slice Uri into slice content. - * - * @param uri The URI to a slice provider - * @return The Slice provided by the app or null if none is given. - * @see Slice - * @hide - */ - public final @Nullable Slice bindSlice(@NonNull Uri uri) { - Preconditions.checkNotNull(uri, "uri"); - IContentProvider provider = acquireProvider(uri); - if (provider == null) { - throw new IllegalArgumentException("Unknown URI " + uri); - } - try { - Bundle extras = new Bundle(); - extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); - final Bundle res = provider.call(mPackageName, SliceProvider.METHOD_SLICE, null, - extras); - Bundle.setDefusable(res, true); - 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 { - releaseProvider(provider); - } - } - - /** * Returns the content provider for the given content URI. * * @param uri The URI to a content provider @@ -1759,7 +1725,7 @@ public abstract class ContentResolver { * @hide */ public final IContentProvider acquireProvider(Uri uri) { - if (!SCHEME_CONTENT.equals(uri.getScheme()) && !SCHEME_SLICE.equals(uri.getScheme())) { + if (!SCHEME_CONTENT.equals(uri.getScheme())) { return null; } final String auth = uri.getAuthority(); diff --git a/android/content/Context.java b/android/content/Context.java index 20fbf046..c165fb3e 100644 --- a/android/content/Context.java +++ b/android/content/Context.java @@ -3604,7 +3604,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. * diff --git a/android/content/Intent.java b/android/content/Intent.java index c9ad9519..e47de752 100644 --- a/android/content/Intent.java +++ b/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; @@ -9371,6 +9372,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. diff --git a/android/content/IntentFilter.java b/android/content/IntentFilter.java index c9bce530..a957aed8 100644 --- a/android/content/IntentFilter.java +++ b/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/android/content/pm/FeatureInfo.java b/android/content/pm/FeatureInfo.java index 9ee6fa24..ff9fd8ec 100644 --- a/android/content/pm/FeatureInfo.java +++ b/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/android/content/pm/LauncherApps.java b/android/content/pm/LauncherApps.java index aa9562ff..b94a410b 100644 --- a/android/content/pm/LauncherApps.java +++ b/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; @@ -282,12 +282,27 @@ 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 + * @hide include all pinned shortcuts by any launchers, not just by the caller, + * in the result. + * If the caller doesn't havve the {@link android.Manifest.permission#ACCESS_SHORTCUTS} + * permission, this flag will be ignored. + */ + @TestApi + public static final int FLAG_MATCH_ALL_PINNED = 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_ALL_PINNED; /** @hide kept for unit tests */ @Deprecated @@ -319,6 +334,7 @@ public class LauncherApps { FLAG_MATCH_PINNED, FLAG_MATCH_MANIFEST, FLAG_GET_KEY_FIELDS_ONLY, + FLAG_MATCH_MANIFEST, }) @Retention(RetentionPolicy.SOURCE) public @interface QueryFlags {} @@ -678,6 +694,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 +729,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/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java index be7f921e..143c51da 100644 --- a/android/content/pm/PackageManagerInternal.java +++ b/android/content/pm/PackageManagerInternal.java @@ -467,6 +467,7 @@ public abstract class PackageManagerInternal { /** Updates the flags for the given permission. */ public abstract void updatePermissionFlagsTEMP(@NonNull String permName, @NonNull String packageName, int flagMask, int flagValues, int userId); - /** temporary until mPermissionTrees is moved to PermissionManager */ - public abstract Object enforcePermissionTreeTEMP(@NonNull String permName, int callingUid); + /** Returns a PermissionGroup. */ + public abstract @Nullable PackageParser.PermissionGroup getPermissionGroupTEMP( + @NonNull String groupName); } diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java index 6c7c8a07..ad36139a 100644 --- a/android/content/pm/PackageParser.java +++ b/android/content/pm/PackageParser.java @@ -3711,17 +3711,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"; } } } @@ -6849,6 +6847,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/android/content/pm/PermissionInfo.java b/android/content/pm/PermissionInfo.java index b45c26ce..5dd7aeda 100644 --- a/android/content/pm/PermissionInfo.java +++ b/android/content/pm/PermissionInfo.java @@ -353,6 +353,11 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { return size; } + /** @hide */ + public boolean isAppOp() { + return (protectionLevel & PermissionInfo.PROTECTION_FLAG_APPOP) != 0; + } + public static final Creator<PermissionInfo> CREATOR = new Creator<PermissionInfo>() { @Override diff --git a/android/content/pm/ResolveInfo.java b/android/content/pm/ResolveInfo.java index 79931670..3f63d80f 100644 --- a/android/content/pm/ResolveInfo.java +++ b/android/content/pm/ResolveInfo.java @@ -222,6 +222,40 @@ public class ResolveInfo implements Parcelable { } /** + * @return The resource that would be used when loading + * the label for this resolve info. + * + * @hide + */ + public int resolveLabelResId() { + if (labelRes != 0) { + return labelRes; + } + final ComponentInfo componentInfo = getComponentInfo(); + if (componentInfo.labelRes != 0) { + return componentInfo.labelRes; + } + return componentInfo.applicationInfo.labelRes; + } + + /** + * @return The resource that would be used when loading + * the icon for this resolve info. + * + * @hide + */ + public int resolveIconResId() { + if (icon != 0) { + return icon; + } + final ComponentInfo componentInfo = getComponentInfo(); + if (componentInfo.icon != 0) { + return componentInfo.icon; + } + return componentInfo.applicationInfo.icon; + } + + /** * Retrieve the current graphical icon associated with this resolution. This * will call back on the given PackageManager to load the icon from * the application. diff --git a/android/content/pm/ShortcutInfo.java b/android/content/pm/ShortcutInfo.java index 6b9c7537..9ff07757 100644 --- a/android/content/pm/ShortcutInfo.java +++ b/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); @@ -1808,6 +1992,11 @@ 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("Dis"); } @@ -1848,7 +2037,9 @@ public final class ShortcutInfo implements Parcelable { sb.append("packageName="); sb.append(mPackageName); - sb.append(", activity="); + addIndentOrComma(sb, indent); + + sb.append("activity="); sb.append(mActivity); addIndentOrComma(sb, indent); @@ -1883,6 +2074,11 @@ public final class ShortcutInfo implements Parcelable { addIndentOrComma(sb, indent); + sb.append("disabledReason="); + sb.append(getDisabledReasonDebugString(mDisabledReason)); + + addIndentOrComma(sb, indent); + sb.append("categories="); sb.append(mCategories); @@ -1953,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; @@ -1978,5 +2174,6 @@ public final class ShortcutInfo implements Parcelable { mIconResId = iconResId; mIconResName = iconResName; mBitmapPath = bitmapPath; + mDisabledReason = disabledReason; } } diff --git a/android/content/pm/ShortcutServiceInternal.java b/android/content/pm/ShortcutServiceInternal.java index 7b7d8ae4..7fc25d82 100644 --- a/android/content/pm/ShortcutServiceInternal.java +++ b/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,7 +71,7 @@ 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 boolean requestPinAppWidget(@NonNull String callingPackage, @NonNull AppWidgetProviderInfo appWidget, @Nullable Bundle extras, diff --git a/android/content/res/FontResourcesParser.java b/android/content/res/FontResourcesParser.java index 042eb87f..28e9fce3 100644 --- a/android/content/res/FontResourcesParser.java +++ b/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,15 @@ public class FontResourcesParser { private final @NonNull String mFileName; private int mWeight; private int mItalic; + private int mTtcIndex; private int mResourceId; - public FontFileResourceEntry(@NonNull String fileName, int weight, int italic) { + public FontFileResourceEntry(@NonNull String fileName, int weight, int italic, + int ttcIndex) { mFileName = fileName; mWeight = weight; mItalic = italic; + mTtcIndex = ttcIndex; } public @NonNull String getFileName() { @@ -97,6 +101,10 @@ public class FontResourcesParser { public int getItalic() { return mItalic; } + + public int getTtcIndex() { + return mTtcIndex; + } } // A class represents file based font-family element in xml file. @@ -203,6 +211,7 @@ public class FontResourcesParser { Typeface.RESOLVE_BY_FONT_TABLE); int italic = array.getInt(R.styleable.FontFamilyFont_fontStyle, Typeface.RESOLVE_BY_FONT_TABLE); + 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 +220,7 @@ public class FontResourcesParser { if (filename == null) { return null; } - return new FontFileResourceEntry(filename, weight, italic); + return new FontFileResourceEntry(filename, weight, italic, ttcIndex); } private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { diff --git a/android/content/res/ResourcesImpl.java b/android/content/res/ResourcesImpl.java index a8b8c4b5..386239cf 100644 --- a/android/content/res/ResourcesImpl.java +++ b/android/content/res/ResourcesImpl.java @@ -796,7 +796,7 @@ public class ResourcesImpl { dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); is.close(); } - } catch (Exception e) { + } catch (Exception | StackOverflowError e) { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); final NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); diff --git a/android/database/CursorWindow.java b/android/database/CursorWindow.java index a75372ff..f84ec65f 100644 --- a/android/database/CursorWindow.java +++ b/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/android/database/SQLiteDatabaseIoPerfTest.java b/android/database/SQLiteDatabaseIoPerfTest.java new file mode 100644 index 00000000..7c5316d2 --- /dev/null +++ b/android/database/SQLiteDatabaseIoPerfTest.java @@ -0,0 +1,176 @@ +/* + * 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.database; + +import android.app.Activity; +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.os.Bundle; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.util.Preconditions; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * Performance tests for measuring amount of data written during typical DB operations + * + * <p>To run: bit CorePerfTests:android.database.SQLiteDatabaseIoPerfTest + */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class SQLiteDatabaseIoPerfTest { + private static final String TAG = "SQLiteDatabaseIoPerfTest"; + private static final String DB_NAME = "db_io_perftest"; + private static final int DEFAULT_DATASET_SIZE = 500; + + private Long mWriteBytes; + + private SQLiteDatabase mDatabase; + private Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getTargetContext(); + mContext.deleteDatabase(DB_NAME); + mDatabase = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null); + mDatabase.execSQL("CREATE TABLE T1 " + + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)"); + } + + @After + public void tearDown() { + mDatabase.close(); + mContext.deleteDatabase(DB_NAME); + } + + @Test + public void testDatabaseModifications() { + startMeasuringWrites(); + ContentValues cv = new ContentValues(); + String[] whereArg = new String[1]; + for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) { + cv.put("_ID", i); + cv.put("COL_A", i); + cv.put("COL_B", "NewValue"); + cv.put("COL_C", 1.0); + assertEquals(i, mDatabase.insert("T1", null, cv)); + } + cv = new ContentValues(); + for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) { + cv.put("COL_B", "UpdatedValue"); + cv.put("COL_C", 1.1); + whereArg[0] = String.valueOf(i); + assertEquals(1, mDatabase.update("T1", cv, "_ID=?", whereArg)); + } + for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) { + whereArg[0] = String.valueOf(i); + assertEquals(1, mDatabase.delete("T1", "_ID=?", whereArg)); + } + // Make sure all changes are written to disk + mDatabase.close(); + long bytes = endMeasuringWrites(); + sendResults("testDatabaseModifications" , bytes); + } + + @Test + public void testInsertsWithTransactions() { + startMeasuringWrites(); + final int txSize = 10; + ContentValues cv = new ContentValues(); + for (int i = 0; i < DEFAULT_DATASET_SIZE * 5; i++) { + if (i % txSize == 0) { + mDatabase.beginTransaction(); + } + if (i % txSize == txSize-1) { + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + } + cv.put("_ID", i); + cv.put("COL_A", i); + cv.put("COL_B", "NewValue"); + cv.put("COL_C", 1.0); + assertEquals(i, mDatabase.insert("T1", null, cv)); + } + // Make sure all changes are written to disk + mDatabase.close(); + long bytes = endMeasuringWrites(); + sendResults("testInsertsWithTransactions" , bytes); + } + + private void startMeasuringWrites() { + Preconditions.checkState(mWriteBytes == null, "Measurement already started"); + mWriteBytes = getIoStats().get("write_bytes"); + } + + private long endMeasuringWrites() { + Preconditions.checkState(mWriteBytes != null, "Measurement wasn't started"); + Long newWriteBytes = getIoStats().get("write_bytes"); + return newWriteBytes - mWriteBytes; + } + + private void sendResults(String testName, long writeBytes) { + Log.i(TAG, testName + " write_bytes: " + writeBytes); + Bundle status = new Bundle(); + status.putLong("write_bytes", writeBytes); + InstrumentationRegistry.getInstrumentation().sendStatus(Activity.RESULT_OK, status); + } + + private static Map<String, Long> getIoStats() { + String ioStat = "/proc/self/io"; + Map<String, Long> results = new ArrayMap<>(); + try { + List<String> lines = Files.readAllLines(new File(ioStat).toPath()); + for (String line : lines) { + line = line.trim(); + String[] split = line.split(":"); + if (split.length == 2) { + try { + String key = split[0].trim(); + Long value = Long.valueOf(split[1].trim()); + results.put(key, value); + } catch (NumberFormatException e) { + Log.e(TAG, "Cannot parse number from " + line); + } + } else if (line.isEmpty()) { + Log.e(TAG, "Cannot parse line " + line); + } + } + } catch (IOException e) { + Log.e(TAG, "Can't read: " + ioStat, e); + } + return results; + } + +} diff --git a/android/database/SQLiteDatabasePerfTest.java b/android/database/SQLiteDatabasePerfTest.java new file mode 100644 index 00000000..7a32c0cc --- /dev/null +++ b/android/database/SQLiteDatabasePerfTest.java @@ -0,0 +1,223 @@ +/* + * 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.database; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Performance tests for typical CRUD operations and loading rows into the Cursor + * + * <p>To run: bit CorePerfTests:android.database.SQLiteDatabasePerfTest + */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class SQLiteDatabasePerfTest { + // TODO b/64262688 Add Concurrency tests to compare WAL vs DELETE read/write + private static final String DB_NAME = "dbperftest"; + private static final int DEFAULT_DATASET_SIZE = 1000; + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + private SQLiteDatabase mDatabase; + private Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getTargetContext(); + mContext.deleteDatabase(DB_NAME); + mDatabase = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null); + mDatabase.execSQL("CREATE TABLE T1 " + + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)"); + mDatabase.execSQL("CREATE TABLE T2 (" + + "_ID INTEGER PRIMARY KEY, COL_A VARCHAR(100), T1_ID INTEGER," + + "FOREIGN KEY(T1_ID) REFERENCES T1 (_ID))"); + } + + @After + public void tearDown() { + mDatabase.close(); + mContext.deleteDatabase(DB_NAME); + } + + @Test + public void testSelect() { + insertT1TestDataSet(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + Random rnd = new Random(0); + while (state.keepRunning()) { + int index = rnd.nextInt(DEFAULT_DATASET_SIZE); + try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 " + + "WHERE _ID=?", new String[]{String.valueOf(index)})) { + assertTrue(cursor.moveToNext()); + assertEquals(index, cursor.getInt(0)); + assertEquals(index, cursor.getInt(1)); + assertEquals("T1Value" + index, cursor.getString(2)); + assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d); + } + } + } + + @Test + public void testSelectMultipleRows() { + insertT1TestDataSet(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + Random rnd = new Random(0); + final int querySize = 50; + while (state.keepRunning()) { + int index = rnd.nextInt(DEFAULT_DATASET_SIZE - querySize - 1); + try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 " + + "WHERE _ID BETWEEN ? and ? ORDER BY _ID", + new String[]{String.valueOf(index), String.valueOf(index + querySize - 1)})) { + int i = 0; + while(cursor.moveToNext()) { + assertEquals(index, cursor.getInt(0)); + assertEquals(index, cursor.getInt(1)); + assertEquals("T1Value" + index, cursor.getString(2)); + assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d); + index++; + i++; + } + assertEquals(querySize, i); + } + } + } + + @Test + public void testInnerJoin() { + mDatabase.setForeignKeyConstraintsEnabled(true); + mDatabase.beginTransaction(); + insertT1TestDataSet(); + insertT2TestDataSet(); + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + Random rnd = new Random(0); + while (state.keepRunning()) { + int index = rnd.nextInt(1000); + try (Cursor cursor = mDatabase.rawQuery( + "SELECT T1._ID, T1.COL_A, T1.COL_B, T1.COL_C, T2.COL_A FROM T1 " + + "INNER JOIN T2 on T2.T1_ID=T1._ID WHERE T1._ID = ?", + new String[]{String.valueOf(index)})) { + assertTrue(cursor.moveToNext()); + assertEquals(index, cursor.getInt(0)); + assertEquals(index, cursor.getInt(1)); + assertEquals("T1Value" + index, cursor.getString(2)); + assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d); + assertEquals("T2Value" + index, cursor.getString(4)); + } + } + } + + @Test + public void testInsert() { + insertT1TestDataSet(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + ContentValues cv = new ContentValues(); + cv.put("_ID", DEFAULT_DATASET_SIZE); + cv.put("COL_B", "NewValue"); + cv.put("COL_C", 1.1); + String[] deleteArgs = new String[]{String.valueOf(DEFAULT_DATASET_SIZE)}; + while (state.keepRunning()) { + assertEquals(DEFAULT_DATASET_SIZE, mDatabase.insert("T1", null, cv)); + state.pauseTiming(); + assertEquals(1, mDatabase.delete("T1", "_ID=?", deleteArgs)); + state.resumeTiming(); + } + } + + @Test + public void testDelete() { + insertT1TestDataSet(); + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + String[] deleteArgs = new String[]{String.valueOf(DEFAULT_DATASET_SIZE)}; + Object[] insertsArgs = new Object[]{DEFAULT_DATASET_SIZE, DEFAULT_DATASET_SIZE, + "ValueToDelete", 1.1}; + + while (state.keepRunning()) { + state.pauseTiming(); + mDatabase.execSQL("INSERT INTO T1 VALUES (?, ?, ?, ?)", insertsArgs); + state.resumeTiming(); + assertEquals(1, mDatabase.delete("T1", "_ID=?", deleteArgs)); + } + } + + @Test + public void testUpdate() { + insertT1TestDataSet(); + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + Random rnd = new Random(0); + int i = 0; + ContentValues cv = new ContentValues(); + String[] argArray = new String[1]; + while (state.keepRunning()) { + int id = rnd.nextInt(DEFAULT_DATASET_SIZE); + cv.put("COL_A", i); + cv.put("COL_B", "UpdatedValue"); + cv.put("COL_C", i); + argArray[0] = String.valueOf(id); + assertEquals(1, mDatabase.update("T1", cv, "_ID=?", argArray)); + i++; + } + } + + private void insertT1TestDataSet() { + mDatabase.beginTransaction(); + for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) { + mDatabase.execSQL("INSERT INTO T1 VALUES (?, ?, ?, ?)", + new Object[]{i, i, "T1Value" + i, i * 1.1}); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } + + private void insertT2TestDataSet() { + mDatabase.beginTransaction(); + for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) { + mDatabase.execSQL("INSERT INTO T2 VALUES (?, ?, ?)", + new Object[]{i, "T2Value" + i, i}); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + } +} + diff --git a/android/graphics/Bitmap.java b/android/graphics/Bitmap.java index 57c75490..0072012f 100644 --- a/android/graphics/Bitmap.java +++ b/android/graphics/Bitmap.java @@ -21,6 +21,7 @@ import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Size; +import android.annotation.WorkerThread; import android.content.res.ResourcesImpl; import android.os.Parcel; import android.os.Parcelable; @@ -1233,6 +1234,7 @@ public final class Bitmap implements Parcelable { * @param stream The outputstream to write the compressed data. * @return true if successfully compressed to the specified stream. */ + @WorkerThread public boolean compress(CompressFormat format, int quality, OutputStream stream) { checkRecycled("Can't compress a recycled bitmap"); // do explicit check before calling the native method diff --git a/android/graphics/BitmapFactory.java b/android/graphics/BitmapFactory.java index ffb39e33..f5bf754a 100644 --- a/android/graphics/BitmapFactory.java +++ b/android/graphics/BitmapFactory.java @@ -354,6 +354,7 @@ public class BitmapFactory { * decode, in the case of which a more accurate, but slightly slower, * IDCT method will be used instead. */ + @Deprecated public boolean inPreferQualityOverSpeed; /** @@ -412,6 +413,7 @@ public class BitmapFactory { * can check, inbetween the bounds decode and the image decode, to see * if the operation is canceled. */ + @Deprecated public boolean mCancel; /** @@ -426,6 +428,7 @@ public class BitmapFactory { * or if inJustDecodeBounds is true, will set outWidth/outHeight * to -1 */ + @Deprecated public void requestCancelDecode() { mCancel = true; } diff --git a/android/graphics/Typeface.java b/android/graphics/Typeface.java index cfc389f0..9961ed64 100644 --- a/android/graphics/Typeface.java +++ b/android/graphics/Typeface.java @@ -250,9 +250,9 @@ public class Typeface { FontFamily fontFamily = new FontFamily(); for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) { - // TODO: Add ttc and variation font support. (b/37853920) + // TODO: Add variation font support. (b/37853920) if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(), - 0 /* resourceCookie */, false /* isAsset */, 0 /* ttcIndex */, + 0 /* resourceCookie */, false /* isAsset */, fontFile.getTtcIndex(), fontFile.getWeight(), fontFile.getItalic(), null /* axes */)) { return null; } diff --git a/android/graphics/pdf/PdfEditor.java b/android/graphics/pdf/PdfEditor.java index 0c509b77..3821bc7a 100644 --- a/android/graphics/pdf/PdfEditor.java +++ b/android/graphics/pdf/PdfEditor.java @@ -23,6 +23,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; +import android.system.Os; import android.system.OsConstants; import dalvik.system.CloseGuard; import libcore.io.IoUtils; @@ -72,8 +73,8 @@ public final class PdfEditor { final long size; try { - Libcore.os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET); - size = Libcore.os.fstat(input.getFileDescriptor()).st_size; + Os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET); + size = Os.fstat(input.getFileDescriptor()).st_size; } catch (ErrnoException ee) { throw new IllegalArgumentException("file descriptor not seekable"); } diff --git a/android/graphics/pdf/PdfRenderer.java b/android/graphics/pdf/PdfRenderer.java index c82ab0dd..4a917052 100644 --- a/android/graphics/pdf/PdfRenderer.java +++ b/android/graphics/pdf/PdfRenderer.java @@ -26,12 +26,12 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; +import android.system.Os; import android.system.OsConstants; import com.android.internal.util.Preconditions; import dalvik.system.CloseGuard; import libcore.io.IoUtils; -import libcore.io.Libcore; import java.io.IOException; import java.lang.annotation.Retention; @@ -156,8 +156,8 @@ public final class PdfRenderer implements AutoCloseable { final long size; try { - Libcore.os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET); - size = Libcore.os.fstat(input.getFileDescriptor()).st_size; + Os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET); + size = Os.fstat(input.getFileDescriptor()).st_size; } catch (ErrnoException ee) { throw new IllegalArgumentException("file descriptor not seekable"); } diff --git a/android/inputmethodservice/AbstractInputMethodService.java b/android/inputmethodservice/AbstractInputMethodService.java index 29177b6b..185215a5 100644 --- a/android/inputmethodservice/AbstractInputMethodService.java +++ b/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/android/inputmethodservice/IInputMethodWrapper.java b/android/inputmethodservice/IInputMethodWrapper.java index 765aff96..2c7e51a1 100644 --- a/android/inputmethodservice/IInputMethodWrapper.java +++ b/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/android/inputmethodservice/InputMethodService.java b/android/inputmethodservice/InputMethodService.java index 7a20943e..223ed73b 100644 --- a/android/inputmethodservice/InputMethodService.java +++ b/android/inputmethodservice/InputMethodService.java @@ -382,8 +382,10 @@ 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; @@ -392,10 +394,12 @@ public class InputMethodService extends AbstractInputMethodService { } /** - * 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); } diff --git a/android/location/GnssMeasurement.java b/android/location/GnssMeasurement.java index d24a4774..412cc291 100644 --- a/android/location/GnssMeasurement.java +++ b/android/location/GnssMeasurement.java @@ -612,6 +612,16 @@ public final class GnssMeasurement implements Parcelable { * * <pre> * accumulated delta range = -k * carrier phase (where k is a constant)</pre> + * + * <p>Similar to the concept of an RTCM "Phaserange", when the accumulated delta range is + * initially chosen, and whenever it is reset, it will retain the integer nature + * of the relative carrier phase offset between satellites observed by this receiver, such that + * the double difference of this value between receivers and satellites may be used, together + * with integer ambiguity resolution, to determine highly precise relative location between + * receivers. + * + * <p>This includes ensuring that all half-cycle ambiguities are resolved before this value is + * reported as {@link #ADR_STATE_VALID}. */ public double getAccumulatedDeltaRangeMeters() { return mAccumulatedDeltaRangeMeters; @@ -861,7 +871,7 @@ public final class GnssMeasurement implements Parcelable { } /** - * Gets the Signal-to-Noise ratio (SNR) in dB. + * Gets the (post-correlation & integration) Signal-to-Noise ratio (SNR) in dB. * * <p>The value is only available if {@link #hasSnrInDb()} is {@code true}. */ diff --git a/android/location/Location.java b/android/location/Location.java index e8eaa59a..e7f903e8 100644 --- a/android/location/Location.java +++ b/android/location/Location.java @@ -813,15 +813,16 @@ public class Location implements Parcelable { /** * Get the estimated vertical accuracy of this location, in meters. * - * <p>We define vertical accuracy as the radius of 68% confidence. In other - * words, if you draw a circle centered at this location's altitude, and with a radius - * equal to the vertical accuracy, then there is a 68% probability that the true altitude is - * inside the circle. + * <p>We define vertical accuracy at 68% confidence. Specifically, as 1-side of the + * 2-sided range above and below the estimated altitude reported by {@link #getAltitude()}, + * within which there is a 68% probability of finding the true altitude. * - * <p>In statistical terms, it is assumed that location errors - * are random with a normal distribution, so the 68% confidence circle - * represents one standard deviation. Note that in practice, location - * errors do not always follow such a simple distribution. + * <p>In the case where the underlying distribution is assumed Gaussian normal, this would be + * considered 1 standard deviation. + * + * <p>For example, if {@link #getAltitude()} returns 150, and + * {@link #getVerticalAccuracyMeters()} ()} returns 20 then there is a 68% probability + * of the true altitude being between 130 and 170 meters. * * <p>If this location does not have a vertical accuracy, then 0.0 is returned. */ @@ -866,14 +867,16 @@ public class Location implements Parcelable { /** * Get the estimated speed accuracy of this location, in meters per second. * - * <p>We define speed accuracy as a 1-standard-deviation value, i.e. as 1-side of the - * 2-sided range above and below the estimated - * speed reported by {@link #getSpeed()}, within which there is a 68% probability of - * finding the true speed. + * <p>We define speed accuracy at 68% confidence. Specifically, as 1-side of the + * 2-sided range above and below the estimated speed reported by {@link #getSpeed()}, + * within which there is a 68% probability of finding the true speed. + * + * <p>In the case where the underlying + * distribution is assumed Gaussian normal, this would be considered 1 standard deviation. * - * <p>For example, if {@link #getSpeed()} returns 5.0, and - * {@link #getSpeedAccuracyMetersPerSecond()} returns 1.0, then there is a 68% probably of the - * true speed being between 4.0 and 6.0 meters per second. + * <p>For example, if {@link #getSpeed()} returns 5, and + * {@link #getSpeedAccuracyMetersPerSecond()} returns 1, then there is a 68% probability of + * the true speed being between 4 and 6 meters per second. * * <p>Note that the speed and speed accuracy is often better than would be obtained simply from * differencing sequential positions, such as when the Doppler measurements from GNSS satellites @@ -922,13 +925,16 @@ public class Location implements Parcelable { /** * Get the estimated bearing accuracy of this location, in degrees. * - * <p>We define bearing accuracy as a 1-standard-deviation value, i.e. as 1-side of the + * <p>We define bearing accuracy at 68% confidence. Specifically, as 1-side of the * 2-sided range on each side of the estimated bearing reported by {@link #getBearing()}, * within which there is a 68% probability of finding the true bearing. * - * <p>For example, if {@link #getBearing()} returns 60., and - * {@link #getBearingAccuracyDegrees()} ()} returns 10., then there is a 68% probably of the - * true bearing being between 50. and 70. degrees. + * <p>In the case where the underlying distribution is assumed Gaussian normal, this would be + * considered 1 standard deviation. + * + * <p>For example, if {@link #getBearing()} returns 60, and + * {@link #getBearingAccuracyDegrees()} ()} returns 10, then there is a 68% probability of the + * true bearing being between 50 and 70 degrees. * * <p>If this location does not have a bearing accuracy, then 0.0 is returned. */ diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java index 26ead3d1..20405d3b 100644 --- a/android/media/AudioAttributes.java +++ b/android/media/AudioAttributes.java @@ -202,6 +202,22 @@ public final class AudioAttributes implements Parcelable { * @see #SUPPRESSIBLE_USAGES */ public final static int SUPPRESSIBLE_NEVER = 3; + /** + * @hide + * Denotes a usage for alarms, + * will be muted when the Zen mode doesn't allow alarms + * @see #SUPPRESSIBLE_USAGES + */ + public final static int SUPPRESSIBLE_ALARM = 4; + /** + * @hide + * Denotes a usage for all other sounds not caught in SUPPRESSIBLE_NOTIFICATION, + * SUPPRESSIBLE_CALL,SUPPRESSIBLE_NEVER or SUPPRESSIBLE_ALARM. + * This includes media, system, game, navigation, the assistant, and more. + * These will be muted when the Zen mode doesn't allow media/system/other. + * @see #SUPPRESSIBLE_USAGES + */ + public final static int SUPPRESSIBLE_MEDIA_SYSTEM_OTHER = 5; /** * @hide @@ -221,6 +237,13 @@ public final class AudioAttributes implements Parcelable { SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_EVENT, SUPPRESSIBLE_NOTIFICATION); SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_ACCESSIBILITY, SUPPRESSIBLE_NEVER); SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION, SUPPRESSIBLE_NEVER); + SUPPRESSIBLE_USAGES.put(USAGE_ALARM, SUPPRESSIBLE_ALARM); + SUPPRESSIBLE_USAGES.put(USAGE_MEDIA, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_GAME, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); + SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT, SUPPRESSIBLE_MEDIA_SYSTEM_OTHER); } /** diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java index 4ea4e381..760cc49b 100644 --- a/android/media/MediaMetadataRetriever.java +++ b/android/media/MediaMetadataRetriever.java @@ -395,7 +395,7 @@ public class MediaMetadataRetriever * @see #getFrameAtTime(long, int) */ /* Do not change these option values without updating their counterparts - * in include/media/stagefright/MediaSource.h! + * in include/media/MediaSource.h! */ /** * This option is used with {@link #getFrameAtTime(long, int)} to retrieve diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java index 7787d4b5..62757e2e 100644 --- a/android/media/MediaPlayer.java +++ b/android/media/MediaPlayer.java @@ -39,6 +39,7 @@ import android.os.PowerManager; import android.os.SystemProperties; import android.provider.Settings; import android.system.ErrnoException; +import android.system.Os; import android.system.OsConstants; import android.util.Log; import android.util.Pair; @@ -60,7 +61,6 @@ import android.media.SyncParams; import com.android.internal.util.Preconditions; import libcore.io.IoBridge; -import libcore.io.Libcore; import libcore.io.Streams; import java.io.ByteArrayOutputStream; @@ -2843,7 +2843,7 @@ public class MediaPlayer extends PlayerBase final FileDescriptor dupedFd; try { - dupedFd = Libcore.os.dup(fd); + dupedFd = Os.dup(fd); } catch (ErrnoException ex) { Log.e(TAG, ex.getMessage(), ex); throw new RuntimeException(ex); @@ -2881,7 +2881,7 @@ public class MediaPlayer extends PlayerBase private int addTrack() { final ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { - Libcore.os.lseek(dupedFd, offset2, OsConstants.SEEK_SET); + Os.lseek(dupedFd, offset2, OsConstants.SEEK_SET); byte[] buffer = new byte[4096]; for (long total = 0; total < length2;) { int bytesToRead = (int) Math.min(buffer.length, length2 - total); @@ -2905,7 +2905,7 @@ public class MediaPlayer extends PlayerBase return MEDIA_INFO_TIMED_TEXT_ERROR; } finally { try { - Libcore.os.close(dupedFd); + Os.close(dupedFd); } catch (ErrnoException e) { Log.e(TAG, e.getMessage(), e); } diff --git a/android/media/MediaRecorder.java b/android/media/MediaRecorder.java index 59a124fa..76784904 100644 --- a/android/media/MediaRecorder.java +++ b/android/media/MediaRecorder.java @@ -917,7 +917,7 @@ public class MediaRecorder */ public void setNextOutputFile(File file) throws IOException { - RandomAccessFile f = new RandomAccessFile(file, "rws"); + RandomAccessFile f = new RandomAccessFile(file, "rw"); try { _setNextOutputFile(f.getFD()); } finally { @@ -942,7 +942,7 @@ public class MediaRecorder public void prepare() throws IllegalStateException, IOException { if (mPath != null) { - RandomAccessFile file = new RandomAccessFile(mPath, "rws"); + RandomAccessFile file = new RandomAccessFile(mPath, "rw"); try { _setOutputFile(file.getFD()); } finally { @@ -951,7 +951,7 @@ public class MediaRecorder } else if (mFd != null) { _setOutputFile(mFd); } else if (mFile != null) { - RandomAccessFile file = new RandomAccessFile(mFile, "rws"); + RandomAccessFile file = new RandomAccessFile(mFile, "rw"); try { _setOutputFile(file.getFD()); } finally { diff --git a/android/media/projection/MediaProjectionManager.java b/android/media/projection/MediaProjectionManager.java index 9f2c08e5..aa0d0cc0 100644 --- a/android/media/projection/MediaProjectionManager.java +++ b/android/media/projection/MediaProjectionManager.java @@ -20,8 +20,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; import android.app.Activity; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.media.projection.IMediaProjection; import android.os.Handler; import android.os.IBinder; @@ -71,8 +73,11 @@ public final class MediaProjectionManager { */ public Intent createScreenCaptureIntent() { Intent i = new Intent(); - i.setClassName("com.android.systemui", - "com.android.systemui.media.MediaProjectionPermissionActivity"); + final ComponentName mediaProjectionPermissionDialogComponent = + ComponentName.unflattenFromString(mContext.getResources().getString( + com.android.internal.R.string + .config_mediaProjectionPermissionDialogComponent)); + i.setComponent(mediaProjectionPermissionDialogComponent); return i; } diff --git a/android/media/tv/TvInputManager.java b/android/media/tv/TvInputManager.java index d7a9edef..fd1f2cf6 100644 --- a/android/media/tv/TvInputManager.java +++ b/android/media/tv/TvInputManager.java @@ -2590,12 +2590,9 @@ public final class TvInputManager { } } + /** @removed */ public boolean dispatchKeyEventToHdmi(KeyEvent event) { - try { - return mInterface.dispatchKeyEventToHdmi(event); - } catch (RemoteException e) { - throw new RuntimeException(e); - } + return false; } public void overrideAudioSink(int audioType, String audioAddress, int samplingRate, diff --git a/android/mtp/MtpDatabase.java b/android/mtp/MtpDatabase.java index 17b23264..aaf18e7f 100644 --- a/android/mtp/MtpDatabase.java +++ b/android/mtp/MtpDatabase.java @@ -471,10 +471,14 @@ public class MtpDatabase implements AutoCloseable { if (parent == 0xFFFFFFFF) { // all objects in root of store parent = 0; + where = STORAGE_PARENT_WHERE; + whereArgs = new String[]{Integer.toString(storageID), + Integer.toString(parent)}; + } else { + // If a parent is specified, the storage is redundant + where = PARENT_WHERE; + whereArgs = new String[]{Integer.toString(parent)}; } - where = STORAGE_PARENT_WHERE; - whereArgs = new String[] { Integer.toString(storageID), - Integer.toString(parent) }; } } else { // query specific format @@ -487,11 +491,16 @@ public class MtpDatabase implements AutoCloseable { if (parent == 0xFFFFFFFF) { // all objects in root of store parent = 0; + where = STORAGE_FORMAT_PARENT_WHERE; + whereArgs = new String[]{Integer.toString(storageID), + Integer.toString(format), + Integer.toString(parent)}; + } else { + // If a parent is specified, the storage is redundant + where = FORMAT_PARENT_WHERE; + whereArgs = new String[]{Integer.toString(format), + Integer.toString(parent)}; } - where = STORAGE_FORMAT_PARENT_WHERE; - whereArgs = new String[] { Integer.toString(storageID), - Integer.toString(format), - Integer.toString(parent) }; } } } @@ -845,7 +854,7 @@ public class MtpDatabase implements AutoCloseable { return MtpConstants.RESPONSE_OK; } - private int moveObject(int handle, int newParent, String newPath) { + private int moveObject(int handle, int newParent, int newStorage, String newPath) { String[] whereArgs = new String[] { Integer.toString(handle) }; // do not allow renaming any of the special subdirectories @@ -857,6 +866,7 @@ public class MtpDatabase implements AutoCloseable { ContentValues values = new ContentValues(); values.put(Files.FileColumns.DATA, newPath); values.put(Files.FileColumns.PARENT, newParent); + values.put(Files.FileColumns.STORAGE_ID, newStorage); int updated = 0; try { // note - we are relying on a special case in MediaProvider.update() to update diff --git a/android/net/IpSecAlgorithm.java b/android/net/IpSecAlgorithm.java index 79310e29..16b14523 100644 --- a/android/net/IpSecAlgorithm.java +++ b/android/net/IpSecAlgorithm.java @@ -31,7 +31,6 @@ import java.util.Arrays; * RFC 4301. */ public final class IpSecAlgorithm implements Parcelable { - /** * AES-CBC Encryption/Ciphering Algorithm. * @@ -68,6 +67,7 @@ public final class IpSecAlgorithm implements Parcelable { * <p>Valid truncation lengths are multiples of 8 bits from 192 to (default) 384. */ public static final String AUTH_HMAC_SHA384 = "hmac(sha384)"; + /** * SHA512 HMAC Authentication/Integrity Algorithm * @@ -75,8 +75,24 @@ public final class IpSecAlgorithm implements Parcelable { */ public static final String AUTH_HMAC_SHA512 = "hmac(sha512)"; + /** + * AES-GCM Authentication/Integrity + Encryption/Ciphering Algorithm. + * + * <p>Valid lengths for this key are {128, 192, 256}. + * + * <p>Valid ICV (truncation) lengths are {64, 96, 128}. + */ + public static final String AUTH_CRYPT_AES_GCM = "rfc4106(gcm(aes))"; + /** @hide */ - @StringDef({CRYPT_AES_CBC, AUTH_HMAC_MD5, AUTH_HMAC_SHA1, AUTH_HMAC_SHA256, AUTH_HMAC_SHA512}) + @StringDef({ + CRYPT_AES_CBC, + AUTH_HMAC_MD5, + AUTH_HMAC_SHA1, + AUTH_HMAC_SHA256, + AUTH_HMAC_SHA512, + AUTH_CRYPT_AES_GCM + }) @Retention(RetentionPolicy.SOURCE) public @interface AlgorithmName {} @@ -102,7 +118,7 @@ public final class IpSecAlgorithm implements Parcelable { * @param algoName precise name of the algorithm to be used. * @param key non-null Key padded to a multiple of 8 bits. * @param truncLenBits the number of bits of output hash to use; only meaningful for - * Authentication. + * Authentication or Authenticated Encryption (equivalent to ICV length). */ public IpSecAlgorithm(@AlgorithmName String algoName, byte[] key, int truncLenBits) { if (!isTruncationLengthValid(algoName, truncLenBits)) { @@ -175,6 +191,8 @@ public final class IpSecAlgorithm implements Parcelable { return (truncLenBits >= 192 && truncLenBits <= 384); case AUTH_HMAC_SHA512: return (truncLenBits >= 256 && truncLenBits <= 512); + case AUTH_CRYPT_AES_GCM: + return (truncLenBits == 64 || truncLenBits == 96 || truncLenBits == 128); default: return false; } diff --git a/android/net/IpSecConfig.java b/android/net/IpSecConfig.java index 632b7fc0..61b13a92 100644 --- a/android/net/IpSecConfig.java +++ b/android/net/IpSecConfig.java @@ -50,6 +50,9 @@ public final class IpSecConfig implements Parcelable { // Authentication Algorithm private IpSecAlgorithm mAuthentication; + // Authenticated Encryption Algorithm + private IpSecAlgorithm mAuthenticatedEncryption; + @Override public String toString() { return new StringBuilder() @@ -59,6 +62,8 @@ public final class IpSecConfig implements Parcelable { .append(mEncryption) .append(", mAuthentication=") .append(mAuthentication) + .append(", mAuthenticatedEncryption=") + .append(mAuthenticatedEncryption) .append("}") .toString(); } @@ -118,6 +123,11 @@ public final class IpSecConfig implements Parcelable { mFlow[direction].mAuthentication = authentication; } + /** Set the authenticated encryption algorithm for a given direction */ + public void setAuthenticatedEncryption(int direction, IpSecAlgorithm authenticatedEncryption) { + mFlow[direction].mAuthenticatedEncryption = authenticatedEncryption; + } + public void setNetwork(Network network) { mNetwork = network; } @@ -163,6 +173,10 @@ public final class IpSecConfig implements Parcelable { return mFlow[direction].mAuthentication; } + public IpSecAlgorithm getAuthenticatedEncryption(int direction) { + return mFlow[direction].mAuthenticatedEncryption; + } + public Network getNetwork() { return mNetwork; } @@ -199,9 +213,11 @@ public final class IpSecConfig implements Parcelable { out.writeInt(mFlow[IpSecTransform.DIRECTION_IN].mSpiResourceId); out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mEncryption, flags); out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mAuthentication, flags); + out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mAuthenticatedEncryption, flags); out.writeInt(mFlow[IpSecTransform.DIRECTION_OUT].mSpiResourceId); out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mEncryption, flags); out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mAuthentication, flags); + out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mAuthenticatedEncryption, flags); out.writeInt(mEncapType); out.writeInt(mEncapSocketResourceId); out.writeInt(mEncapRemotePort); @@ -221,11 +237,15 @@ public final class IpSecConfig implements Parcelable { (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); mFlow[IpSecTransform.DIRECTION_IN].mAuthentication = (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); + mFlow[IpSecTransform.DIRECTION_IN].mAuthenticatedEncryption = + (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); mFlow[IpSecTransform.DIRECTION_OUT].mSpiResourceId = in.readInt(); mFlow[IpSecTransform.DIRECTION_OUT].mEncryption = (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); mFlow[IpSecTransform.DIRECTION_OUT].mAuthentication = (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); + mFlow[IpSecTransform.DIRECTION_OUT].mAuthenticatedEncryption = + (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); mEncapType = in.readInt(); mEncapSocketResourceId = in.readInt(); mEncapRemotePort = in.readInt(); diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java index e15a2c67..48b5bd5c 100644 --- a/android/net/IpSecTransform.java +++ b/android/net/IpSecTransform.java @@ -281,6 +281,8 @@ public final class IpSecTransform implements AutoCloseable { * <p>If encryption is set for a given direction without also providing an SPI for that * direction, creation of an IpSecTransform will fail upon calling a build() method. * + * <p>Authenticated encryption is mutually exclusive with encryption and authentication. + * * @param direction either {@link #DIRECTION_IN or #DIRECTION_OUT} * @param algo {@link IpSecAlgorithm} specifying the encryption to be applied. */ @@ -296,6 +298,8 @@ public final class IpSecTransform implements AutoCloseable { * <p>If authentication is set for a given direction without also providing an SPI for that * direction, creation of an IpSecTransform will fail upon calling a build() method. * + * <p>Authenticated encryption is mutually exclusive with encryption and authentication. + * * @param direction either {@link #DIRECTION_IN or #DIRECTION_OUT} * @param algo {@link IpSecAlgorithm} specifying the authentication to be applied. */ @@ -306,6 +310,29 @@ public final class IpSecTransform implements AutoCloseable { } /** + * Add an authenticated encryption algorithm to the transform for the given direction. + * + * <p>If an authenticated encryption algorithm is set for a given direction without also + * providing an SPI for that direction, creation of an IpSecTransform will fail upon calling + * a build() method. + * + * <p>The Authenticated Encryption (AE) class of algorithms are also known as Authenticated + * Encryption with Associated Data (AEAD) algorithms, or Combined mode algorithms (as + * referred to in RFC 4301) + * + * <p>Authenticated encryption is mutually exclusive with encryption and authentication. + * + * @param direction either {@link #DIRECTION_IN or #DIRECTION_OUT} + * @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to + * be applied. + */ + public IpSecTransform.Builder setAuthenticatedEncryption( + @TransformDirection int direction, IpSecAlgorithm algo) { + mConfig.setAuthenticatedEncryption(direction, algo); + return this; + } + + /** * Set the SPI, which uniquely identifies a particular IPsec session from others. Because * IPsec operates at the IP layer, this 32-bit identifier uniquely identifies packets to a * given destination address. diff --git a/android/net/LinkProperties.java b/android/net/LinkProperties.java index 2c9fb23e..4e474c8e 100644 --- a/android/net/LinkProperties.java +++ b/android/net/LinkProperties.java @@ -683,9 +683,9 @@ public final class LinkProperties implements Parcelable { */ public boolean hasIPv4Address() { for (LinkAddress address : mLinkAddresses) { - if (address.getAddress() instanceof Inet4Address) { - return true; - } + if (address.getAddress() instanceof Inet4Address) { + return true; + } } return false; } @@ -725,9 +725,9 @@ public final class LinkProperties implements Parcelable { */ public boolean hasIPv4DefaultRoute() { for (RouteInfo r : mRoutes) { - if (r.isIPv4Default()) { - return true; - } + if (r.isIPv4Default()) { + return true; + } } return false; } @@ -740,9 +740,9 @@ public final class LinkProperties implements Parcelable { */ public boolean hasIPv6DefaultRoute() { for (RouteInfo r : mRoutes) { - if (r.isIPv6Default()) { - return true; - } + if (r.isIPv6Default()) { + return true; + } } return false; } @@ -755,9 +755,9 @@ public final class LinkProperties implements Parcelable { */ public boolean hasIPv4DnsServer() { for (InetAddress ia : mDnses) { - if (ia instanceof Inet4Address) { - return true; - } + if (ia instanceof Inet4Address) { + return true; + } } return false; } @@ -770,9 +770,9 @@ public final class LinkProperties implements Parcelable { */ public boolean hasIPv6DnsServer() { for (InetAddress ia : mDnses) { - if (ia instanceof Inet6Address) { - return true; - } + if (ia instanceof Inet6Address) { + return true; + } } return false; } diff --git a/android/net/apf/ApfFilter.java b/android/net/apf/ApfFilter.java index 190b3a61..5c2b66f6 100644 --- a/android/net/apf/ApfFilter.java +++ b/android/net/apf/ApfFilter.java @@ -33,7 +33,7 @@ import android.net.NetworkUtils; import android.net.apf.ApfGenerator; import android.net.apf.ApfGenerator.IllegalInstructionException; import android.net.apf.ApfGenerator.Register; -import android.net.ip.IpManager; +import android.net.ip.IpClient; import android.net.metrics.ApfProgramEvent; import android.net.metrics.ApfStats; import android.net.metrics.IpConnectivityLog; @@ -238,7 +238,7 @@ public class ApfFilter { private static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20; private final ApfCapabilities mApfCapabilities; - private final IpManager.Callback mIpManagerCallback; + private final IpClient.Callback mIpClientCallback; private final NetworkInterface mNetworkInterface; private final IpConnectivityLog mMetricsLog; @@ -262,10 +262,10 @@ public class ApfFilter { @VisibleForTesting ApfFilter(ApfCapabilities apfCapabilities, NetworkInterface networkInterface, - IpManager.Callback ipManagerCallback, boolean multicastFilter, + IpClient.Callback ipClientCallback, boolean multicastFilter, boolean ieee802_3Filter, int[] ethTypeBlackList, IpConnectivityLog log) { mApfCapabilities = apfCapabilities; - mIpManagerCallback = ipManagerCallback; + mIpClientCallback = ipClientCallback; mNetworkInterface = networkInterface; mMulticastFilter = multicastFilter; mDrop802_3Frames = ieee802_3Filter; @@ -275,7 +275,7 @@ public class ApfFilter { mMetricsLog = log; - // TODO: ApfFilter should not generate programs until IpManager sends provisioning success. + // TODO: ApfFilter should not generate programs until IpClient sends provisioning success. maybeStartFilter(); } @@ -1051,7 +1051,7 @@ public class ApfFilter { if (VDBG) { hexDump("Installing filter: ", program, program.length); } - mIpManagerCallback.installPacketFilter(program); + mIpClientCallback.installPacketFilter(program); logApfProgramEventLocked(now); mLastInstallEvent = new ApfProgramEvent(); mLastInstallEvent.lifetime = programMinLifetime; @@ -1161,7 +1161,7 @@ public class ApfFilter { * filtering using APF programs. */ public static ApfFilter maybeCreate(ApfCapabilities apfCapabilities, - NetworkInterface networkInterface, IpManager.Callback ipManagerCallback, + NetworkInterface networkInterface, IpClient.Callback ipClientCallback, boolean multicastFilter, boolean ieee802_3Filter, int[] ethTypeBlackList) { if (apfCapabilities == null || networkInterface == null) return null; if (apfCapabilities.apfVersionSupported == 0) return null; @@ -1178,7 +1178,7 @@ public class ApfFilter { Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported); return null; } - return new ApfFilter(apfCapabilities, networkInterface, ipManagerCallback, + return new ApfFilter(apfCapabilities, networkInterface, ipClientCallback, multicastFilter, ieee802_3Filter, ethTypeBlackList, new IpConnectivityLog()); } diff --git a/android/net/ip/ConnectivityPacketTracker.java b/android/net/ip/ConnectivityPacketTracker.java index 0230f36b..1925c39e 100644 --- a/android/net/ip/ConnectivityPacketTracker.java +++ b/android/net/ip/ConnectivityPacketTracker.java @@ -25,6 +25,7 @@ import android.os.Handler; import android.system.ErrnoException; import android.system.Os; import android.system.PacketSocketAddress; +import android.text.TextUtils; import android.util.Log; import android.util.LocalLog; @@ -59,11 +60,14 @@ public class ConnectivityPacketTracker { private static final boolean DBG = false; private static final String MARK_START = "--- START ---"; private static final String MARK_STOP = "--- STOP ---"; + private static final String MARK_NAMED_START = "--- START (%s) ---"; + private static final String MARK_NAMED_STOP = "--- STOP (%s) ---"; private final String mTag; private final LocalLog mLog; private final BlockingSocketReader mPacketListener; private boolean mRunning; + private String mDisplayName; public ConnectivityPacketTracker(Handler h, NetworkInterface netif, LocalLog log) { final String ifname; @@ -85,14 +89,16 @@ public class ConnectivityPacketTracker { mPacketListener = new PacketListener(h, ifindex, hwaddr, mtu); } - public void start() { + public void start(String displayName) { mRunning = true; + mDisplayName = displayName; mPacketListener.start(); } public void stop() { mPacketListener.stop(); mRunning = false; + mDisplayName = null; } private final class PacketListener extends BlockingSocketReader { @@ -133,16 +139,19 @@ public class ConnectivityPacketTracker { @Override protected void onStart() { - mLog.log(MARK_START); + final String msg = TextUtils.isEmpty(mDisplayName) + ? MARK_START + : String.format(MARK_NAMED_START, mDisplayName); + mLog.log(msg); } @Override protected void onStop() { - if (mRunning) { - mLog.log(MARK_STOP); - } else { - mLog.log(MARK_STOP + " (packet listener stopped unexpectedly)"); - } + String msg = TextUtils.isEmpty(mDisplayName) + ? MARK_STOP + : String.format(MARK_NAMED_STOP, mDisplayName); + if (!mRunning) msg += " (packet listener stopped unexpectedly)"; + mLog.log(msg); } @Override diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java new file mode 100644 index 00000000..2359fab4 --- /dev/null +++ b/android/net/ip/IpClient.java @@ -0,0 +1,1712 @@ +/* + * 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.ip; + +import com.android.internal.util.MessageUtils; +import com.android.internal.util.WakeupMessage; + +import android.content.Context; +import android.net.DhcpResults; +import android.net.INetd; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties.ProvisioningChange; +import android.net.LinkProperties; +import android.net.Network; +import android.net.ProxyInfo; +import android.net.RouteInfo; +import android.net.StaticIpConfiguration; +import android.net.apf.ApfCapabilities; +import android.net.apf.ApfFilter; +import android.net.dhcp.DhcpClient; +import android.net.metrics.IpConnectivityLog; +import android.net.metrics.IpManagerEvent; +import android.net.util.MultinetworkPolicyTracker; +import android.net.util.NetdService; +import android.net.util.NetworkConstants; +import android.net.util.SharedLog; +import android.os.INetworkManagementService; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.LocalLog; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.R; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.IState; +import com.android.internal.util.Preconditions; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; +import com.android.server.net.NetlinkTracker; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Predicate; +import java.util.stream.Collectors; + + +/** + * IpClient + * + * This class provides the interface to IP-layer provisioning and maintenance + * functionality that can be used by transport layers like Wi-Fi, Ethernet, + * et cetera. + * + * [ Lifetime ] + * IpClient is designed to be instantiated as soon as the interface name is + * known and can be as long-lived as the class containing it (i.e. declaring + * it "private final" is okay). + * + * @hide + */ +public class IpClient extends StateMachine { + private static final boolean DBG = false; + + // For message logging. + private static final Class[] sMessageClasses = { IpClient.class, DhcpClient.class }; + private static final SparseArray<String> sWhatToString = + MessageUtils.findMessageNames(sMessageClasses); + + /** + * Callbacks for handling IpClient events. + */ + public static class Callback { + // In order to receive onPreDhcpAction(), call #withPreDhcpAction() + // when constructing a ProvisioningConfiguration. + // + // Implementations of onPreDhcpAction() must call + // IpClient#completedPreDhcpAction() to indicate that DHCP is clear + // to proceed. + public void onPreDhcpAction() {} + public void onPostDhcpAction() {} + + // This is purely advisory and not an indication of provisioning + // success or failure. This is only here for callers that want to + // expose DHCPv4 results to other APIs (e.g., WifiInfo#setInetAddress). + // DHCPv4 or static IPv4 configuration failure or success can be + // determined by whether or not the passed-in DhcpResults object is + // null or not. + public void onNewDhcpResults(DhcpResults dhcpResults) {} + + public void onProvisioningSuccess(LinkProperties newLp) {} + public void onProvisioningFailure(LinkProperties newLp) {} + + // Invoked on LinkProperties changes. + public void onLinkPropertiesChange(LinkProperties newLp) {} + + // Called when the internal IpReachabilityMonitor (if enabled) has + // detected the loss of a critical number of required neighbors. + public void onReachabilityLost(String logMsg) {} + + // Called when the IpClient state machine terminates. + public void onQuit() {} + + // Install an APF program to filter incoming packets. + public void installPacketFilter(byte[] filter) {} + + // If multicast filtering cannot be accomplished with APF, this function will be called to + // actuate multicast filtering using another means. + public void setFallbackMulticastFilter(boolean enabled) {} + + // Enabled/disable Neighbor Discover offload functionality. This is + // called, for example, whenever 464xlat is being started or stopped. + public void setNeighborDiscoveryOffload(boolean enable) {} + } + + // Use a wrapper class to log in order to ensure complete and detailed + // logging. This method is lighter weight than annotations/reflection + // and has the following benefits: + // + // - No invoked method can be forgotten. + // Any new method added to IpClient.Callback must be overridden + // here or it will never be called. + // + // - No invoking call site can be forgotten. + // Centralized logging in this way means call sites don't need to + // remember to log, and therefore no call site can be forgotten. + // + // - No variation in log format among call sites. + // Encourages logging of any available arguments, and all call sites + // are necessarily logged identically. + // + // TODO: Find an lighter weight approach. + private class LoggingCallbackWrapper extends Callback { + private static final String PREFIX = "INVOKE "; + private Callback mCallback; + + public LoggingCallbackWrapper(Callback callback) { + mCallback = callback; + } + + private void log(String msg) { + mLog.log(PREFIX + msg); + } + + @Override + public void onPreDhcpAction() { + mCallback.onPreDhcpAction(); + log("onPreDhcpAction()"); + } + @Override + public void onPostDhcpAction() { + mCallback.onPostDhcpAction(); + log("onPostDhcpAction()"); + } + @Override + public void onNewDhcpResults(DhcpResults dhcpResults) { + mCallback.onNewDhcpResults(dhcpResults); + log("onNewDhcpResults({" + dhcpResults + "})"); + } + @Override + public void onProvisioningSuccess(LinkProperties newLp) { + mCallback.onProvisioningSuccess(newLp); + log("onProvisioningSuccess({" + newLp + "})"); + } + @Override + public void onProvisioningFailure(LinkProperties newLp) { + mCallback.onProvisioningFailure(newLp); + log("onProvisioningFailure({" + newLp + "})"); + } + @Override + public void onLinkPropertiesChange(LinkProperties newLp) { + mCallback.onLinkPropertiesChange(newLp); + log("onLinkPropertiesChange({" + newLp + "})"); + } + @Override + public void onReachabilityLost(String logMsg) { + mCallback.onReachabilityLost(logMsg); + log("onReachabilityLost(" + logMsg + ")"); + } + @Override + public void onQuit() { + mCallback.onQuit(); + log("onQuit()"); + } + @Override + public void installPacketFilter(byte[] filter) { + mCallback.installPacketFilter(filter); + log("installPacketFilter(byte[" + filter.length + "])"); + } + @Override + public void setFallbackMulticastFilter(boolean enabled) { + mCallback.setFallbackMulticastFilter(enabled); + log("setFallbackMulticastFilter(" + enabled + ")"); + } + @Override + public void setNeighborDiscoveryOffload(boolean enable) { + mCallback.setNeighborDiscoveryOffload(enable); + log("setNeighborDiscoveryOffload(" + enable + ")"); + } + } + + /** + * This class encapsulates parameters to be passed to + * IpClient#startProvisioning(). A defensive copy is made by IpClient + * and the values specified herein are in force until IpClient#stop() + * is called. + * + * Example use: + * + * final ProvisioningConfiguration config = + * mIpClient.buildProvisioningConfiguration() + * .withPreDhcpAction() + * .withProvisioningTimeoutMs(36 * 1000) + * .build(); + * mIpClient.startProvisioning(config); + * ... + * mIpClient.stop(); + * + * The specified provisioning configuration will only be active until + * IpClient#stop() is called. Future calls to IpClient#startProvisioning() + * must specify the configuration again. + */ + public static class ProvisioningConfiguration { + // TODO: Delete this default timeout once those callers that care are + // fixed to pass in their preferred timeout. + // + // We pick 36 seconds so we can send DHCP requests at + // + // t=0, t=2, t=6, t=14, t=30 + // + // allowing for 10% jitter. + private static final int DEFAULT_TIMEOUT_MS = 36 * 1000; + + public static class Builder { + private ProvisioningConfiguration mConfig = new ProvisioningConfiguration(); + + public Builder withoutIPv4() { + mConfig.mEnableIPv4 = false; + return this; + } + + public Builder withoutIPv6() { + mConfig.mEnableIPv6 = false; + return this; + } + + public Builder withoutIpReachabilityMonitor() { + mConfig.mUsingIpReachabilityMonitor = false; + return this; + } + + public Builder withPreDhcpAction() { + mConfig.mRequestedPreDhcpActionMs = DEFAULT_TIMEOUT_MS; + return this; + } + + public Builder withPreDhcpAction(int dhcpActionTimeoutMs) { + mConfig.mRequestedPreDhcpActionMs = dhcpActionTimeoutMs; + return this; + } + + public Builder withInitialConfiguration(InitialConfiguration initialConfig) { + mConfig.mInitialConfig = initialConfig; + return this; + } + + public Builder withStaticConfiguration(StaticIpConfiguration staticConfig) { + mConfig.mStaticIpConfig = staticConfig; + return this; + } + + public Builder withApfCapabilities(ApfCapabilities apfCapabilities) { + mConfig.mApfCapabilities = apfCapabilities; + return this; + } + + public Builder withProvisioningTimeoutMs(int timeoutMs) { + mConfig.mProvisioningTimeoutMs = timeoutMs; + return this; + } + + public Builder withIPv6AddrGenModeEUI64() { + mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_EUI64; + return this; + } + + public Builder withIPv6AddrGenModeStablePrivacy() { + mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY; + return this; + } + + public Builder withNetwork(Network network) { + mConfig.mNetwork = network; + return this; + } + + public Builder withDisplayName(String displayName) { + mConfig.mDisplayName = displayName; + return this; + } + + public ProvisioningConfiguration build() { + return new ProvisioningConfiguration(mConfig); + } + } + + /* package */ boolean mEnableIPv4 = true; + /* package */ boolean mEnableIPv6 = true; + /* package */ boolean mUsingIpReachabilityMonitor = true; + /* package */ int mRequestedPreDhcpActionMs; + /* package */ InitialConfiguration mInitialConfig; + /* package */ StaticIpConfiguration mStaticIpConfig; + /* package */ ApfCapabilities mApfCapabilities; + /* package */ int mProvisioningTimeoutMs = DEFAULT_TIMEOUT_MS; + /* package */ int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY; + /* package */ Network mNetwork = null; + /* package */ String mDisplayName = null; + + public ProvisioningConfiguration() {} // used by Builder + + public ProvisioningConfiguration(ProvisioningConfiguration other) { + mEnableIPv4 = other.mEnableIPv4; + mEnableIPv6 = other.mEnableIPv6; + mUsingIpReachabilityMonitor = other.mUsingIpReachabilityMonitor; + mRequestedPreDhcpActionMs = other.mRequestedPreDhcpActionMs; + mInitialConfig = InitialConfiguration.copy(other.mInitialConfig); + mStaticIpConfig = other.mStaticIpConfig; + mApfCapabilities = other.mApfCapabilities; + mProvisioningTimeoutMs = other.mProvisioningTimeoutMs; + mIPv6AddrGenMode = other.mIPv6AddrGenMode; + mNetwork = other.mNetwork; + mDisplayName = other.mDisplayName; + } + + @Override + public String toString() { + return new StringJoiner(", ", getClass().getSimpleName() + "{", "}") + .add("mEnableIPv4: " + mEnableIPv4) + .add("mEnableIPv6: " + mEnableIPv6) + .add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor) + .add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs) + .add("mInitialConfig: " + mInitialConfig) + .add("mStaticIpConfig: " + mStaticIpConfig) + .add("mApfCapabilities: " + mApfCapabilities) + .add("mProvisioningTimeoutMs: " + mProvisioningTimeoutMs) + .add("mIPv6AddrGenMode: " + mIPv6AddrGenMode) + .add("mNetwork: " + mNetwork) + .add("mDisplayName: " + mDisplayName) + .toString(); + } + + public boolean isValid() { + return (mInitialConfig == null) || mInitialConfig.isValid(); + } + } + + public static class InitialConfiguration { + public final Set<LinkAddress> ipAddresses = new HashSet<>(); + public final Set<IpPrefix> directlyConnectedRoutes = new HashSet<>(); + public final Set<InetAddress> dnsServers = new HashSet<>(); + public Inet4Address gateway; // WiFi legacy behavior with static ipv4 config + + public static InitialConfiguration copy(InitialConfiguration config) { + if (config == null) { + return null; + } + InitialConfiguration configCopy = new InitialConfiguration(); + configCopy.ipAddresses.addAll(config.ipAddresses); + configCopy.directlyConnectedRoutes.addAll(config.directlyConnectedRoutes); + configCopy.dnsServers.addAll(config.dnsServers); + return configCopy; + } + + @Override + public String toString() { + return String.format( + "InitialConfiguration(IPs: {%s}, prefixes: {%s}, DNS: {%s}, v4 gateway: %s)", + join(", ", ipAddresses), join(", ", directlyConnectedRoutes), + join(", ", dnsServers), gateway); + } + + public boolean isValid() { + if (ipAddresses.isEmpty()) { + return false; + } + + // For every IP address, there must be at least one prefix containing that address. + for (LinkAddress addr : ipAddresses) { + if (!any(directlyConnectedRoutes, (p) -> p.contains(addr.getAddress()))) { + return false; + } + } + // For every dns server, there must be at least one prefix containing that address. + for (InetAddress addr : dnsServers) { + if (!any(directlyConnectedRoutes, (p) -> p.contains(addr))) { + return false; + } + } + // All IPv6 LinkAddresses have an RFC7421-suitable prefix length + // (read: compliant with RFC4291#section2.5.4). + if (any(ipAddresses, not(InitialConfiguration::isPrefixLengthCompliant))) { + return false; + } + // If directlyConnectedRoutes contains an IPv6 default route + // then ipAddresses MUST contain at least one non-ULA GUA. + if (any(directlyConnectedRoutes, InitialConfiguration::isIPv6DefaultRoute) + && all(ipAddresses, not(InitialConfiguration::isIPv6GUA))) { + return false; + } + // The prefix length of routes in directlyConnectedRoutes be within reasonable + // bounds for IPv6: /48-/64 just as we’d accept in RIOs. + if (any(directlyConnectedRoutes, not(InitialConfiguration::isPrefixLengthCompliant))) { + return false; + } + // There no more than one IPv4 address + if (ipAddresses.stream().filter(Inet4Address.class::isInstance).count() > 1) { + return false; + } + + return true; + } + + /** + * @return true if the given list of addressess and routes satisfies provisioning for this + * InitialConfiguration. LinkAddresses and RouteInfo objects are not compared with equality + * because addresses and routes seen by Netlink will contain additional fields like flags, + * interfaces, and so on. If this InitialConfiguration has no IP address specified, the + * provisioning check always fails. + * + * If the given list of routes is null, only addresses are taken into considerations. + */ + public boolean isProvisionedBy(List<LinkAddress> addresses, List<RouteInfo> routes) { + if (ipAddresses.isEmpty()) { + return false; + } + + for (LinkAddress addr : ipAddresses) { + if (!any(addresses, (addrSeen) -> addr.isSameAddressAs(addrSeen))) { + return false; + } + } + + if (routes != null) { + for (IpPrefix prefix : directlyConnectedRoutes) { + if (!any(routes, (routeSeen) -> isDirectlyConnectedRoute(routeSeen, prefix))) { + return false; + } + } + } + + return true; + } + + private static boolean isDirectlyConnectedRoute(RouteInfo route, IpPrefix prefix) { + return !route.hasGateway() && prefix.equals(route.getDestination()); + } + + private static boolean isPrefixLengthCompliant(LinkAddress addr) { + return addr.isIPv4() || isCompliantIPv6PrefixLength(addr.getPrefixLength()); + } + + private static boolean isPrefixLengthCompliant(IpPrefix prefix) { + return prefix.isIPv4() || isCompliantIPv6PrefixLength(prefix.getPrefixLength()); + } + + private static boolean isCompliantIPv6PrefixLength(int prefixLength) { + return (NetworkConstants.RFC6177_MIN_PREFIX_LENGTH <= prefixLength) + && (prefixLength <= NetworkConstants.RFC7421_PREFIX_LENGTH); + } + + private static boolean isIPv6DefaultRoute(IpPrefix prefix) { + return prefix.getAddress().equals(Inet6Address.ANY); + } + + private static boolean isIPv6GUA(LinkAddress addr) { + return addr.isIPv6() && addr.isGlobalPreferred(); + } + } + + public static final String DUMP_ARG = "ipclient"; + public static final String DUMP_ARG_CONFIRM = "confirm"; + + private static final int CMD_TERMINATE_AFTER_STOP = 1; + private static final int CMD_STOP = 2; + private static final int CMD_START = 3; + private static final int CMD_CONFIRM = 4; + private static final int EVENT_PRE_DHCP_ACTION_COMPLETE = 5; + // Sent by NetlinkTracker to communicate netlink events. + private static final int EVENT_NETLINK_LINKPROPERTIES_CHANGED = 6; + private static final int CMD_UPDATE_TCP_BUFFER_SIZES = 7; + private static final int CMD_UPDATE_HTTP_PROXY = 8; + private static final int CMD_SET_MULTICAST_FILTER = 9; + private static final int EVENT_PROVISIONING_TIMEOUT = 10; + private static final int EVENT_DHCPACTION_TIMEOUT = 11; + + private static final int MAX_LOG_RECORDS = 500; + private static final int MAX_PACKET_RECORDS = 100; + + private static final boolean NO_CALLBACKS = false; + private static final boolean SEND_CALLBACKS = true; + + // This must match the interface prefix in clatd.c. + // TODO: Revert this hack once IpClient and Nat464Xlat work in concert. + private static final String CLAT_PREFIX = "v4-"; + + private final State mStoppedState = new StoppedState(); + private final State mStoppingState = new StoppingState(); + private final State mStartedState = new StartedState(); + private final State mRunningState = new RunningState(); + + private final String mTag; + private final Context mContext; + private final String mInterfaceName; + private final String mClatInterfaceName; + @VisibleForTesting + protected final Callback mCallback; + private final INetworkManagementService mNwService; + private final NetlinkTracker mNetlinkTracker; + private final WakeupMessage mProvisioningTimeoutAlarm; + private final WakeupMessage mDhcpActionTimeoutAlarm; + private final MultinetworkPolicyTracker mMultinetworkPolicyTracker; + private final SharedLog mLog; + private final LocalLog mConnectivityPacketLog; + private final MessageHandlingLogger mMsgStateLogger; + private final IpConnectivityLog mMetricsLog = new IpConnectivityLog(); + private final InterfaceController mInterfaceCtrl; + + private NetworkInterface mNetworkInterface; + + /** + * Non-final member variables accessed only from within our StateMachine. + */ + private LinkProperties mLinkProperties; + private ProvisioningConfiguration mConfiguration; + private IpReachabilityMonitor mIpReachabilityMonitor; + private DhcpClient mDhcpClient; + private DhcpResults mDhcpResults; + private String mTcpBufferSizes; + private ProxyInfo mHttpProxy; + private ApfFilter mApfFilter; + private boolean mMulticastFiltering; + private long mStartTimeMillis; + + public IpClient(Context context, String ifName, Callback callback) { + this(context, ifName, callback, INetworkManagementService.Stub.asInterface( + ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)), + NetdService.getInstance()); + } + + /** + * An expanded constructor, useful for dependency injection. + * TODO: migrate all test users to mock IpClient directly and remove this ctor. + */ + public IpClient(Context context, String ifName, Callback callback, + INetworkManagementService nwService) { + this(context, ifName, callback, nwService, NetdService.getInstance()); + } + + @VisibleForTesting + IpClient(Context context, String ifName, Callback callback, + INetworkManagementService nwService, INetd netd) { + super(IpClient.class.getSimpleName() + "." + ifName); + mTag = getName(); + + mContext = context; + mInterfaceName = ifName; + mClatInterfaceName = CLAT_PREFIX + ifName; + mCallback = new LoggingCallbackWrapper(callback); + mNwService = nwService; + + mLog = new SharedLog(MAX_LOG_RECORDS, mTag); + mConnectivityPacketLog = new LocalLog(MAX_PACKET_RECORDS); + mMsgStateLogger = new MessageHandlingLogger(); + + mInterfaceCtrl = new InterfaceController(mInterfaceName, mNwService, netd, mLog); + + mNetlinkTracker = new NetlinkTracker( + mInterfaceName, + new NetlinkTracker.Callback() { + @Override + public void update() { + sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED); + } + }) { + @Override + public void interfaceAdded(String iface) { + super.interfaceAdded(iface); + if (mClatInterfaceName.equals(iface)) { + mCallback.setNeighborDiscoveryOffload(false); + } else if (!mInterfaceName.equals(iface)) { + return; + } + + final String msg = "interfaceAdded(" + iface +")"; + logMsg(msg); + } + + @Override + public void interfaceRemoved(String iface) { + super.interfaceRemoved(iface); + // TODO: Also observe mInterfaceName going down and take some + // kind of appropriate action. + if (mClatInterfaceName.equals(iface)) { + // TODO: consider sending a message to the IpClient main + // StateMachine thread, in case "NDO enabled" state becomes + // tied to more things that 464xlat operation. + mCallback.setNeighborDiscoveryOffload(true); + } else if (!mInterfaceName.equals(iface)) { + return; + } + + final String msg = "interfaceRemoved(" + iface +")"; + logMsg(msg); + } + + private void logMsg(String msg) { + Log.d(mTag, msg); + getHandler().post(() -> { mLog.log("OBSERVED " + msg); }); + } + }; + + mLinkProperties = new LinkProperties(); + mLinkProperties.setInterfaceName(mInterfaceName); + + mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(mContext, getHandler(), + () -> { mLog.log("OBSERVED AvoidBadWifi changed"); }); + + mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(), + mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT); + mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(), + mTag + ".EVENT_DHCPACTION_TIMEOUT", EVENT_DHCPACTION_TIMEOUT); + + // Anything the StateMachine may access must have been instantiated + // before this point. + configureAndStartStateMachine(); + + // Anything that may send messages to the StateMachine must only be + // configured to do so after the StateMachine has started (above). + startStateMachineUpdaters(); + } + + private void configureAndStartStateMachine() { + addState(mStoppedState); + addState(mStartedState); + addState(mRunningState, mStartedState); + addState(mStoppingState); + + setInitialState(mStoppedState); + + super.start(); + } + + private void startStateMachineUpdaters() { + try { + mNwService.registerObserver(mNetlinkTracker); + } catch (RemoteException e) { + logError("Couldn't register NetlinkTracker: %s", e); + } + + mMultinetworkPolicyTracker.start(); + } + + private void stopStateMachineUpdaters() { + try { + mNwService.unregisterObserver(mNetlinkTracker); + } catch (RemoteException e) { + logError("Couldn't unregister NetlinkTracker: %s", e); + } + + mMultinetworkPolicyTracker.shutdown(); + } + + @Override + protected void onQuitting() { + mCallback.onQuit(); + } + + // Shut down this IpClient instance altogether. + public void shutdown() { + stop(); + sendMessage(CMD_TERMINATE_AFTER_STOP); + } + + public static ProvisioningConfiguration.Builder buildProvisioningConfiguration() { + return new ProvisioningConfiguration.Builder(); + } + + public void startProvisioning(ProvisioningConfiguration req) { + if (!req.isValid()) { + doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING); + return; + } + + getNetworkInterface(); + + mCallback.setNeighborDiscoveryOffload(true); + sendMessage(CMD_START, new ProvisioningConfiguration(req)); + } + + // TODO: Delete this. + public void startProvisioning(StaticIpConfiguration staticIpConfig) { + startProvisioning(buildProvisioningConfiguration() + .withStaticConfiguration(staticIpConfig) + .build()); + } + + public void startProvisioning() { + startProvisioning(new ProvisioningConfiguration()); + } + + public void stop() { + sendMessage(CMD_STOP); + } + + public void confirmConfiguration() { + sendMessage(CMD_CONFIRM); + } + + public void completedPreDhcpAction() { + sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE); + } + + /** + * Set the TCP buffer sizes to use. + * + * This may be called, repeatedly, at any time before or after a call to + * #startProvisioning(). The setting is cleared upon calling #stop(). + */ + public void setTcpBufferSizes(String tcpBufferSizes) { + sendMessage(CMD_UPDATE_TCP_BUFFER_SIZES, tcpBufferSizes); + } + + /** + * Set the HTTP Proxy configuration to use. + * + * This may be called, repeatedly, at any time before or after a call to + * #startProvisioning(). The setting is cleared upon calling #stop(). + */ + public void setHttpProxy(ProxyInfo proxyInfo) { + sendMessage(CMD_UPDATE_HTTP_PROXY, proxyInfo); + } + + /** + * Enable or disable the multicast filter. Attempts to use APF to accomplish the filtering, + * if not, Callback.setFallbackMulticastFilter() is called. + */ + public void setMulticastFilter(boolean enabled) { + sendMessage(CMD_SET_MULTICAST_FILTER, enabled); + } + + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + if (args != null && args.length > 0 && DUMP_ARG_CONFIRM.equals(args[0])) { + // Execute confirmConfiguration() and take no further action. + confirmConfiguration(); + return; + } + + // Thread-unsafe access to mApfFilter but just used for debugging. + final ApfFilter apfFilter = mApfFilter; + final ProvisioningConfiguration provisioningConfig = mConfiguration; + final ApfCapabilities apfCapabilities = (provisioningConfig != null) + ? provisioningConfig.mApfCapabilities : null; + + IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + pw.println(mTag + " APF dump:"); + pw.increaseIndent(); + if (apfFilter != null) { + apfFilter.dump(pw); + } else { + pw.print("No active ApfFilter; "); + if (provisioningConfig == null) { + pw.println("IpClient not yet started."); + } else if (apfCapabilities == null || apfCapabilities.apfVersionSupported == 0) { + pw.println("Hardware does not support APF."); + } else { + pw.println("ApfFilter not yet started, APF capabilities: " + apfCapabilities); + } + } + pw.decreaseIndent(); + + pw.println(); + pw.println(mTag + " current ProvisioningConfiguration:"); + pw.increaseIndent(); + pw.println(Objects.toString(provisioningConfig, "N/A")); + pw.decreaseIndent(); + + pw.println(); + pw.println(mTag + " StateMachine dump:"); + pw.increaseIndent(); + mLog.dump(fd, pw, args); + pw.decreaseIndent(); + + pw.println(); + pw.println(mTag + " connectivity packet log:"); + pw.println(); + pw.println("Debug with python and scapy via:"); + pw.println("shell$ python"); + pw.println(">>> from scapy import all as scapy"); + pw.println(">>> scapy.Ether(\"<paste_hex_string>\".decode(\"hex\")).show2()"); + pw.println(); + + pw.increaseIndent(); + mConnectivityPacketLog.readOnlyLocalLog().dump(fd, pw, args); + pw.decreaseIndent(); + } + + + /** + * Internals. + */ + + @Override + protected String getWhatToString(int what) { + return sWhatToString.get(what, "UNKNOWN: " + Integer.toString(what)); + } + + @Override + protected String getLogRecString(Message msg) { + final String logLine = String.format( + "%s/%d %d %d %s [%s]", + mInterfaceName, mNetworkInterface == null ? -1 : mNetworkInterface.getIndex(), + msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger); + + final String richerLogLine = getWhatToString(msg.what) + " " + logLine; + mLog.log(richerLogLine); + if (DBG) { + Log.d(mTag, richerLogLine); + } + + mMsgStateLogger.reset(); + return logLine; + } + + @Override + protected boolean recordLogRec(Message msg) { + // Don't log EVENT_NETLINK_LINKPROPERTIES_CHANGED. They can be noisy, + // and we already log any LinkProperties change that results in an + // invocation of IpClient.Callback#onLinkPropertiesChange(). + final boolean shouldLog = (msg.what != EVENT_NETLINK_LINKPROPERTIES_CHANGED); + if (!shouldLog) { + mMsgStateLogger.reset(); + } + return shouldLog; + } + + private void logError(String fmt, Object... args) { + final String msg = "ERROR " + String.format(fmt, args); + Log.e(mTag, msg); + mLog.log(msg); + } + + private void getNetworkInterface() { + try { + mNetworkInterface = NetworkInterface.getByName(mInterfaceName); + } catch (SocketException | NullPointerException e) { + // TODO: throw new IllegalStateException. + logError("Failed to get interface object: %s", e); + } + } + + // This needs to be called with care to ensure that our LinkProperties + // are in sync with the actual LinkProperties of the interface. For example, + // we should only call this if we know for sure that there are no IP addresses + // assigned to the interface, etc. + private void resetLinkProperties() { + mNetlinkTracker.clearLinkProperties(); + mConfiguration = null; + mDhcpResults = null; + mTcpBufferSizes = ""; + mHttpProxy = null; + + mLinkProperties = new LinkProperties(); + mLinkProperties.setInterfaceName(mInterfaceName); + } + + private void recordMetric(final int type) { + if (mStartTimeMillis <= 0) { Log.wtf(mTag, "Start time undefined!"); } + final long duration = SystemClock.elapsedRealtime() - mStartTimeMillis; + mMetricsLog.log(mInterfaceName, new IpManagerEvent(type, duration)); + } + + // For now: use WifiStateMachine's historical notion of provisioned. + @VisibleForTesting + static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) { + // For historical reasons, we should connect even if all we have is + // an IPv4 address and nothing else. + if (lp.hasIPv4Address() || lp.isProvisioned()) { + return true; + } + if (config == null) { + return false; + } + + // When an InitialConfiguration is specified, ignore any difference with previous + // properties and instead check if properties observed match the desired properties. + return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes()); + } + + // TODO: Investigate folding all this into the existing static function + // LinkProperties.compareProvisioning() or some other single function that + // takes two LinkProperties objects and returns a ProvisioningChange + // object that is a correct and complete assessment of what changed, taking + // account of the asymmetries described in the comments in this function. + // Then switch to using it everywhere (IpReachabilityMonitor, etc.). + private ProvisioningChange compareProvisioning(LinkProperties oldLp, LinkProperties newLp) { + ProvisioningChange delta; + InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null; + final boolean wasProvisioned = isProvisioned(oldLp, config); + final boolean isProvisioned = isProvisioned(newLp, config); + + if (!wasProvisioned && isProvisioned) { + delta = ProvisioningChange.GAINED_PROVISIONING; + } else if (wasProvisioned && isProvisioned) { + delta = ProvisioningChange.STILL_PROVISIONED; + } else if (!wasProvisioned && !isProvisioned) { + delta = ProvisioningChange.STILL_NOT_PROVISIONED; + } else { + // (wasProvisioned && !isProvisioned) + // + // Note that this is true even if we lose a configuration element + // (e.g., a default gateway) that would not be required to advance + // into provisioned state. This is intended: if we have a default + // router and we lose it, that's a sure sign of a problem, but if + // we connect to a network with no IPv4 DNS servers, we consider + // that to be a network without DNS servers and connect anyway. + // + // See the comment below. + delta = ProvisioningChange.LOST_PROVISIONING; + } + + final boolean lostIPv6 = oldLp.isIPv6Provisioned() && !newLp.isIPv6Provisioned(); + final boolean lostIPv4Address = oldLp.hasIPv4Address() && !newLp.hasIPv4Address(); + final boolean lostIPv6Router = oldLp.hasIPv6DefaultRoute() && !newLp.hasIPv6DefaultRoute(); + + // If bad wifi avoidance is disabled, then ignore IPv6 loss of + // provisioning. Otherwise, when a hotspot that loses Internet + // access sends out a 0-lifetime RA to its clients, the clients + // will disconnect and then reconnect, avoiding the bad hotspot, + // instead of getting stuck on the bad hotspot. http://b/31827713 . + // + // This is incorrect because if the hotspot then regains Internet + // access with a different prefix, TCP connections on the + // deprecated addresses will remain stuck. + // + // Note that we can still be disconnected by IpReachabilityMonitor + // if the IPv6 default gateway (but not the IPv6 DNS servers; see + // accompanying code in IpReachabilityMonitor) is unreachable. + final boolean ignoreIPv6ProvisioningLoss = !mMultinetworkPolicyTracker.getAvoidBadWifi(); + + // Additionally: + // + // Partial configurations (e.g., only an IPv4 address with no DNS + // servers and no default route) are accepted as long as DHCPv4 + // succeeds. On such a network, isProvisioned() will always return + // false, because the configuration is not complete, but we want to + // connect anyway. It might be a disconnected network such as a + // Chromecast or a wireless printer, for example. + // + // Because on such a network isProvisioned() will always return false, + // delta will never be LOST_PROVISIONING. So check for loss of + // provisioning here too. + if (lostIPv4Address || (lostIPv6 && !ignoreIPv6ProvisioningLoss)) { + delta = ProvisioningChange.LOST_PROVISIONING; + } + + // Additionally: + // + // If the previous link properties had a global IPv6 address and an + // IPv6 default route then also consider the loss of that default route + // to be a loss of provisioning. See b/27962810. + if (oldLp.hasGlobalIPv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) { + delta = ProvisioningChange.LOST_PROVISIONING; + } + + return delta; + } + + private void dispatchCallback(ProvisioningChange delta, LinkProperties newLp) { + switch (delta) { + case GAINED_PROVISIONING: + if (DBG) { Log.d(mTag, "onProvisioningSuccess()"); } + recordMetric(IpManagerEvent.PROVISIONING_OK); + mCallback.onProvisioningSuccess(newLp); + break; + + case LOST_PROVISIONING: + if (DBG) { Log.d(mTag, "onProvisioningFailure()"); } + recordMetric(IpManagerEvent.PROVISIONING_FAIL); + mCallback.onProvisioningFailure(newLp); + break; + + default: + if (DBG) { Log.d(mTag, "onLinkPropertiesChange()"); } + mCallback.onLinkPropertiesChange(newLp); + break; + } + } + + // Updates all IpClient-related state concerned with LinkProperties. + // Returns a ProvisioningChange for possibly notifying other interested + // parties that are not fronted by IpClient. + private ProvisioningChange setLinkProperties(LinkProperties newLp) { + if (mApfFilter != null) { + mApfFilter.setLinkProperties(newLp); + } + if (mIpReachabilityMonitor != null) { + mIpReachabilityMonitor.updateLinkProperties(newLp); + } + + ProvisioningChange delta = compareProvisioning(mLinkProperties, newLp); + mLinkProperties = new LinkProperties(newLp); + + if (delta == ProvisioningChange.GAINED_PROVISIONING) { + // TODO: Add a proper ProvisionedState and cancel the alarm in + // its enter() method. + mProvisioningTimeoutAlarm.cancel(); + } + + return delta; + } + + private LinkProperties assembleLinkProperties() { + // [1] Create a new LinkProperties object to populate. + LinkProperties newLp = new LinkProperties(); + newLp.setInterfaceName(mInterfaceName); + + // [2] Pull in data from netlink: + // - IPv4 addresses + // - IPv6 addresses + // - IPv6 routes + // - IPv6 DNS servers + // + // N.B.: this is fundamentally race-prone and should be fixed by + // changing NetlinkTracker from a hybrid edge/level model to an + // edge-only model, or by giving IpClient its own netlink socket(s) + // so as to track all required information directly. + LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties(); + newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses()); + for (RouteInfo route : netlinkLinkProperties.getRoutes()) { + newLp.addRoute(route); + } + addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers()); + + // [3] Add in data from DHCPv4, if available. + // + // mDhcpResults is never shared with any other owner so we don't have + // to worry about concurrent modification. + if (mDhcpResults != null) { + for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) { + newLp.addRoute(route); + } + addAllReachableDnsServers(newLp, mDhcpResults.dnsServers); + newLp.setDomains(mDhcpResults.domains); + + if (mDhcpResults.mtu != 0) { + newLp.setMtu(mDhcpResults.mtu); + } + } + + // [4] Add in TCP buffer sizes and HTTP Proxy config, if available. + if (!TextUtils.isEmpty(mTcpBufferSizes)) { + newLp.setTcpBufferSizes(mTcpBufferSizes); + } + if (mHttpProxy != null) { + newLp.setHttpProxy(mHttpProxy); + } + + // [5] Add data from InitialConfiguration + if (mConfiguration != null && mConfiguration.mInitialConfig != null) { + InitialConfiguration config = mConfiguration.mInitialConfig; + // Add InitialConfiguration routes and dns server addresses once all addresses + // specified in the InitialConfiguration have been observed with Netlink. + if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) { + for (IpPrefix prefix : config.directlyConnectedRoutes) { + newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName)); + } + } + addAllReachableDnsServers(newLp, config.dnsServers); + } + final LinkProperties oldLp = mLinkProperties; + if (DBG) { + Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s", + netlinkLinkProperties, newLp, oldLp)); + } + + // TODO: also learn via netlink routes specified by an InitialConfiguration and specified + // from a static IP v4 config instead of manually patching them in in steps [3] and [5]. + return newLp; + } + + private static void addAllReachableDnsServers( + LinkProperties lp, Iterable<InetAddress> dnses) { + // TODO: Investigate deleting this reachability check. We should be + // able to pass everything down to netd and let netd do evaluation + // and RFC6724-style sorting. + for (InetAddress dns : dnses) { + if (!dns.isAnyLocalAddress() && lp.isReachable(dns)) { + lp.addDnsServer(dns); + } + } + } + + // Returns false if we have lost provisioning, true otherwise. + private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) { + final LinkProperties newLp = assembleLinkProperties(); + if (Objects.equals(newLp, mLinkProperties)) { + return true; + } + final ProvisioningChange delta = setLinkProperties(newLp); + if (sendCallbacks) { + dispatchCallback(delta, newLp); + } + return (delta != ProvisioningChange.LOST_PROVISIONING); + } + + private void handleIPv4Success(DhcpResults dhcpResults) { + mDhcpResults = new DhcpResults(dhcpResults); + final LinkProperties newLp = assembleLinkProperties(); + final ProvisioningChange delta = setLinkProperties(newLp); + + if (DBG) { + Log.d(mTag, "onNewDhcpResults(" + Objects.toString(dhcpResults) + ")"); + } + mCallback.onNewDhcpResults(dhcpResults); + dispatchCallback(delta, newLp); + } + + private void handleIPv4Failure() { + // TODO: Investigate deleting this clearIPv4Address() call. + // + // DhcpClient will send us CMD_CLEAR_LINKADDRESS in all circumstances + // that could trigger a call to this function. If we missed handling + // that message in StartedState for some reason we would still clear + // any addresses upon entry to StoppedState. + mInterfaceCtrl.clearIPv4Address(); + mDhcpResults = null; + if (DBG) { Log.d(mTag, "onNewDhcpResults(null)"); } + mCallback.onNewDhcpResults(null); + + handleProvisioningFailure(); + } + + private void handleProvisioningFailure() { + final LinkProperties newLp = assembleLinkProperties(); + ProvisioningChange delta = setLinkProperties(newLp); + // If we've gotten here and we're still not provisioned treat that as + // a total loss of provisioning. + // + // Either (a) static IP configuration failed or (b) DHCPv4 failed AND + // there was no usable IPv6 obtained before a non-zero provisioning + // timeout expired. + // + // Regardless: GAME OVER. + if (delta == ProvisioningChange.STILL_NOT_PROVISIONED) { + delta = ProvisioningChange.LOST_PROVISIONING; + } + + dispatchCallback(delta, newLp); + if (delta == ProvisioningChange.LOST_PROVISIONING) { + transitionTo(mStoppingState); + } + } + + private void doImmediateProvisioningFailure(int failureType) { + logError("onProvisioningFailure(): %s", failureType); + recordMetric(failureType); + mCallback.onProvisioningFailure(new LinkProperties(mLinkProperties)); + } + + private boolean startIPv4() { + // If we have a StaticIpConfiguration attempt to apply it and + // handle the result accordingly. + if (mConfiguration.mStaticIpConfig != null) { + if (mInterfaceCtrl.setIPv4Address(mConfiguration.mStaticIpConfig.ipAddress)) { + handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig)); + } else { + return false; + } + } else { + // Start DHCPv4. + mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceName); + mDhcpClient.registerForPreDhcpNotification(); + mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP); + } + + return true; + } + + private boolean startIPv6() { + return mInterfaceCtrl.setIPv6PrivacyExtensions(true) && + mInterfaceCtrl.setIPv6AddrGenModeIfSupported(mConfiguration.mIPv6AddrGenMode) && + mInterfaceCtrl.enableIPv6(); + } + + private boolean applyInitialConfig(InitialConfiguration config) { + // TODO: also support specifying a static IPv4 configuration in InitialConfiguration. + for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIPv6)) { + if (!mInterfaceCtrl.addAddress(addr)) return false; + } + + return true; + } + + private boolean startIpReachabilityMonitor() { + try { + mIpReachabilityMonitor = new IpReachabilityMonitor( + mContext, + mInterfaceName, + mLog, + new IpReachabilityMonitor.Callback() { + @Override + public void notifyLost(InetAddress ip, String logMsg) { + mCallback.onReachabilityLost(logMsg); + } + }, + mMultinetworkPolicyTracker); + } catch (IllegalArgumentException iae) { + // Failed to start IpReachabilityMonitor. Log it and call + // onProvisioningFailure() immediately. + // + // See http://b/31038971. + logError("IpReachabilityMonitor failure: %s", iae); + mIpReachabilityMonitor = null; + } + + return (mIpReachabilityMonitor != null); + } + + private void stopAllIP() { + // We don't need to worry about routes, just addresses, because: + // - disableIpv6() will clear autoconf IPv6 routes as well, and + // - we don't get IPv4 routes from netlink + // so we neither react to nor need to wait for changes in either. + + mInterfaceCtrl.disableIPv6(); + mInterfaceCtrl.clearAllAddresses(); + } + + class StoppedState extends State { + @Override + public void enter() { + stopAllIP(); + + resetLinkProperties(); + if (mStartTimeMillis > 0) { + recordMetric(IpManagerEvent.COMPLETE_LIFECYCLE); + mStartTimeMillis = 0; + } + } + + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case CMD_TERMINATE_AFTER_STOP: + stopStateMachineUpdaters(); + quit(); + break; + + case CMD_STOP: + break; + + case CMD_START: + mConfiguration = (ProvisioningConfiguration) msg.obj; + transitionTo(mStartedState); + break; + + case EVENT_NETLINK_LINKPROPERTIES_CHANGED: + handleLinkPropertiesUpdate(NO_CALLBACKS); + break; + + case CMD_UPDATE_TCP_BUFFER_SIZES: + mTcpBufferSizes = (String) msg.obj; + handleLinkPropertiesUpdate(NO_CALLBACKS); + break; + + case CMD_UPDATE_HTTP_PROXY: + mHttpProxy = (ProxyInfo) msg.obj; + handleLinkPropertiesUpdate(NO_CALLBACKS); + break; + + case CMD_SET_MULTICAST_FILTER: + mMulticastFiltering = (boolean) msg.obj; + break; + + case DhcpClient.CMD_ON_QUIT: + // Everything is already stopped. + logError("Unexpected CMD_ON_QUIT (already stopped)."); + break; + + default: + return NOT_HANDLED; + } + + mMsgStateLogger.handled(this, getCurrentState()); + return HANDLED; + } + } + + class StoppingState extends State { + @Override + public void enter() { + if (mDhcpClient == null) { + // There's no DHCPv4 for which to wait; proceed to stopped. + transitionTo(mStoppedState); + } + } + + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case CMD_STOP: + break; + + case DhcpClient.CMD_CLEAR_LINKADDRESS: + mInterfaceCtrl.clearIPv4Address(); + break; + + case DhcpClient.CMD_ON_QUIT: + mDhcpClient = null; + transitionTo(mStoppedState); + break; + + default: + deferMessage(msg); + } + + mMsgStateLogger.handled(this, getCurrentState()); + return HANDLED; + } + } + + class StartedState extends State { + @Override + public void enter() { + mStartTimeMillis = SystemClock.elapsedRealtime(); + + if (mConfiguration.mProvisioningTimeoutMs > 0) { + final long alarmTime = SystemClock.elapsedRealtime() + + mConfiguration.mProvisioningTimeoutMs; + mProvisioningTimeoutAlarm.schedule(alarmTime); + } + + if (readyToProceed()) { + transitionTo(mRunningState); + } else { + // Clear all IPv4 and IPv6 before proceeding to RunningState. + // Clean up any leftover state from an abnormal exit from + // tethering or during an IpClient restart. + stopAllIP(); + } + } + + @Override + public void exit() { + mProvisioningTimeoutAlarm.cancel(); + } + + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case CMD_STOP: + transitionTo(mStoppingState); + break; + + case EVENT_NETLINK_LINKPROPERTIES_CHANGED: + handleLinkPropertiesUpdate(NO_CALLBACKS); + if (readyToProceed()) { + transitionTo(mRunningState); + } + break; + + case EVENT_PROVISIONING_TIMEOUT: + handleProvisioningFailure(); + break; + + default: + // It's safe to process messages out of order because the + // only message that can both + // a) be received at this time and + // b) affect provisioning state + // is EVENT_NETLINK_LINKPROPERTIES_CHANGED (handled above). + deferMessage(msg); + } + + mMsgStateLogger.handled(this, getCurrentState()); + return HANDLED; + } + + boolean readyToProceed() { + return (!mLinkProperties.hasIPv4Address() && + !mLinkProperties.hasGlobalIPv6Address()); + } + } + + class RunningState extends State { + private ConnectivityPacketTracker mPacketTracker; + private boolean mDhcpActionInFlight; + + @Override + public void enter() { + // Get the Configuration for ApfFilter from Context + final boolean filter802_3Frames = + mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames); + + final int[] ethTypeBlackList = mContext.getResources().getIntArray( + R.array.config_apfEthTypeBlackList); + + mApfFilter = ApfFilter.maybeCreate(mConfiguration.mApfCapabilities, mNetworkInterface, + mCallback, mMulticastFiltering, filter802_3Frames, ethTypeBlackList); + // TODO: investigate the effects of any multicast filtering racing/interfering with the + // rest of this IP configuration startup. + if (mApfFilter == null) { + mCallback.setFallbackMulticastFilter(mMulticastFiltering); + } + + mPacketTracker = createPacketTracker(); + if (mPacketTracker != null) mPacketTracker.start(mConfiguration.mDisplayName); + + if (mConfiguration.mEnableIPv6 && !startIPv6()) { + doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6); + transitionTo(mStoppingState); + return; + } + + if (mConfiguration.mEnableIPv4 && !startIPv4()) { + doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV4); + transitionTo(mStoppingState); + return; + } + + final InitialConfiguration config = mConfiguration.mInitialConfig; + if ((config != null) && !applyInitialConfig(config)) { + // TODO introduce a new IpManagerEvent constant to distinguish this error case. + doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING); + transitionTo(mStoppingState); + return; + } + + if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) { + doImmediateProvisioningFailure( + IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR); + transitionTo(mStoppingState); + return; + } + } + + @Override + public void exit() { + stopDhcpAction(); + + if (mIpReachabilityMonitor != null) { + mIpReachabilityMonitor.stop(); + mIpReachabilityMonitor = null; + } + + if (mDhcpClient != null) { + mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP); + mDhcpClient.doQuit(); + } + + if (mPacketTracker != null) { + mPacketTracker.stop(); + mPacketTracker = null; + } + + if (mApfFilter != null) { + mApfFilter.shutdown(); + mApfFilter = null; + } + + resetLinkProperties(); + } + + private ConnectivityPacketTracker createPacketTracker() { + try { + return new ConnectivityPacketTracker( + getHandler(), mNetworkInterface, mConnectivityPacketLog); + } catch (IllegalArgumentException e) { + return null; + } + } + + private void ensureDhcpAction() { + if (!mDhcpActionInFlight) { + mCallback.onPreDhcpAction(); + mDhcpActionInFlight = true; + final long alarmTime = SystemClock.elapsedRealtime() + + mConfiguration.mRequestedPreDhcpActionMs; + mDhcpActionTimeoutAlarm.schedule(alarmTime); + } + } + + private void stopDhcpAction() { + mDhcpActionTimeoutAlarm.cancel(); + if (mDhcpActionInFlight) { + mCallback.onPostDhcpAction(); + mDhcpActionInFlight = false; + } + } + + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case CMD_STOP: + transitionTo(mStoppingState); + break; + + case CMD_START: + logError("ALERT: START received in StartedState. Please fix caller."); + break; + + case CMD_CONFIRM: + // TODO: Possibly introduce a second type of confirmation + // that both probes (a) on-link neighbors and (b) does + // a DHCPv4 RENEW. We used to do this on Wi-Fi framework + // roams. + if (mIpReachabilityMonitor != null) { + mIpReachabilityMonitor.probeAll(); + } + break; + + case EVENT_PRE_DHCP_ACTION_COMPLETE: + // It's possible to reach here if, for example, someone + // calls completedPreDhcpAction() after provisioning with + // a static IP configuration. + if (mDhcpClient != null) { + mDhcpClient.sendMessage(DhcpClient.CMD_PRE_DHCP_ACTION_COMPLETE); + } + break; + + case EVENT_NETLINK_LINKPROPERTIES_CHANGED: + if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) { + transitionTo(mStoppingState); + } + break; + + case CMD_UPDATE_TCP_BUFFER_SIZES: + mTcpBufferSizes = (String) msg.obj; + // This cannot possibly change provisioning state. + handleLinkPropertiesUpdate(SEND_CALLBACKS); + break; + + case CMD_UPDATE_HTTP_PROXY: + mHttpProxy = (ProxyInfo) msg.obj; + // This cannot possibly change provisioning state. + handleLinkPropertiesUpdate(SEND_CALLBACKS); + break; + + case CMD_SET_MULTICAST_FILTER: { + mMulticastFiltering = (boolean) msg.obj; + if (mApfFilter != null) { + mApfFilter.setMulticastFilter(mMulticastFiltering); + } else { + mCallback.setFallbackMulticastFilter(mMulticastFiltering); + } + break; + } + + case EVENT_DHCPACTION_TIMEOUT: + stopDhcpAction(); + break; + + case DhcpClient.CMD_PRE_DHCP_ACTION: + if (mConfiguration.mRequestedPreDhcpActionMs > 0) { + ensureDhcpAction(); + } else { + sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE); + } + break; + + case DhcpClient.CMD_CLEAR_LINKADDRESS: + mInterfaceCtrl.clearIPv4Address(); + break; + + case DhcpClient.CMD_CONFIGURE_LINKADDRESS: { + final LinkAddress ipAddress = (LinkAddress) msg.obj; + if (mInterfaceCtrl.setIPv4Address(ipAddress)) { + mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED); + } else { + logError("Failed to set IPv4 address."); + dispatchCallback(ProvisioningChange.LOST_PROVISIONING, + new LinkProperties(mLinkProperties)); + transitionTo(mStoppingState); + } + break; + } + + // This message is only received when: + // + // a) initial address acquisition succeeds, + // b) renew succeeds or is NAK'd, + // c) rebind succeeds or is NAK'd, or + // c) the lease expires, + // + // but never when initial address acquisition fails. The latter + // condition is now governed by the provisioning timeout. + case DhcpClient.CMD_POST_DHCP_ACTION: + stopDhcpAction(); + + switch (msg.arg1) { + case DhcpClient.DHCP_SUCCESS: + handleIPv4Success((DhcpResults) msg.obj); + break; + case DhcpClient.DHCP_FAILURE: + handleIPv4Failure(); + break; + default: + logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1); + } + break; + + case DhcpClient.CMD_ON_QUIT: + // DHCPv4 quit early for some reason. + logError("Unexpected CMD_ON_QUIT."); + mDhcpClient = null; + break; + + default: + return NOT_HANDLED; + } + + mMsgStateLogger.handled(this, getCurrentState()); + return HANDLED; + } + } + + private static class MessageHandlingLogger { + public String processedInState; + public String receivedInState; + + public void reset() { + processedInState = null; + receivedInState = null; + } + + public void handled(State processedIn, IState receivedIn) { + processedInState = processedIn.getClass().getSimpleName(); + receivedInState = receivedIn.getName(); + } + + public String toString() { + return String.format("rcvd_in=%s, proc_in=%s", + receivedInState, processedInState); + } + } + + // TODO: extract out into CollectionUtils. + static <T> boolean any(Iterable<T> coll, Predicate<T> fn) { + for (T t : coll) { + if (fn.test(t)) { + return true; + } + } + return false; + } + + static <T> boolean all(Iterable<T> coll, Predicate<T> fn) { + return !any(coll, not(fn)); + } + + static <T> Predicate<T> not(Predicate<T> fn) { + return (t) -> !fn.test(t); + } + + static <T> String join(String delimiter, Collection<T> coll) { + return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter)); + } + + static <T> T find(Iterable<T> coll, Predicate<T> fn) { + for (T t: coll) { + if (fn.test(t)) { + return t; + } + } + return null; + } + + static <T> List<T> findAll(Collection<T> coll, Predicate<T> fn) { + return coll.stream().filter(fn).collect(Collectors.toList()); + } +} diff --git a/android/net/ip/IpManager.java b/android/net/ip/IpManager.java index bc07b810..b12cb32c 100644 --- a/android/net/ip/IpManager.java +++ b/android/net/ip/IpManager.java @@ -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. @@ -16,1708 +16,161 @@ package android.net.ip; -import com.android.internal.util.MessageUtils; -import com.android.internal.util.WakeupMessage; - import android.content.Context; -import android.net.DhcpResults; import android.net.INetd; -import android.net.IpPrefix; -import android.net.LinkAddress; -import android.net.LinkProperties.ProvisioningChange; import android.net.LinkProperties; -import android.net.ProxyInfo; -import android.net.RouteInfo; +import android.net.Network; import android.net.StaticIpConfiguration; import android.net.apf.ApfCapabilities; -import android.net.apf.ApfFilter; -import android.net.dhcp.DhcpClient; -import android.net.metrics.IpConnectivityLog; -import android.net.metrics.IpManagerEvent; -import android.net.util.MultinetworkPolicyTracker; import android.net.util.NetdService; -import android.net.util.NetworkConstants; -import android.net.util.SharedLog; import android.os.INetworkManagementService; -import android.os.Message; -import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.LocalLog; -import android.util.Log; -import android.util.SparseArray; +import android.net.apf.ApfCapabilities; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.R; -import com.android.internal.util.IndentingPrintWriter; -import com.android.internal.util.IState; -import com.android.internal.util.Preconditions; -import com.android.internal.util.State; -import com.android.internal.util.StateMachine; -import com.android.server.net.NetlinkTracker; -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Objects; -import java.util.List; -import java.util.Set; -import java.util.StringJoiner; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * IpManager - * - * This class provides the interface to IP-layer provisioning and maintenance - * functionality that can be used by transport layers like Wi-Fi, Ethernet, - * et cetera. - * - * [ Lifetime ] - * IpManager is designed to be instantiated as soon as the interface name is - * known and can be as long-lived as the class containing it (i.e. declaring - * it "private final" is okay). +/* + * TODO: Delete this altogether in favor of its renamed successor: IpClient. * * @hide */ -public class IpManager extends StateMachine { - private static final boolean DBG = false; - - // For message logging. - private static final Class[] sMessageClasses = { IpManager.class, DhcpClient.class }; - private static final SparseArray<String> sWhatToString = - MessageUtils.findMessageNames(sMessageClasses); - - /** - * Callbacks for handling IpManager events. - */ - public static class Callback { - // In order to receive onPreDhcpAction(), call #withPreDhcpAction() - // when constructing a ProvisioningConfiguration. - // - // Implementations of onPreDhcpAction() must call - // IpManager#completedPreDhcpAction() to indicate that DHCP is clear - // to proceed. - public void onPreDhcpAction() {} - public void onPostDhcpAction() {} - - // This is purely advisory and not an indication of provisioning - // success or failure. This is only here for callers that want to - // expose DHCPv4 results to other APIs (e.g., WifiInfo#setInetAddress). - // DHCPv4 or static IPv4 configuration failure or success can be - // determined by whether or not the passed-in DhcpResults object is - // null or not. - public void onNewDhcpResults(DhcpResults dhcpResults) {} - - public void onProvisioningSuccess(LinkProperties newLp) {} - public void onProvisioningFailure(LinkProperties newLp) {} - - // Invoked on LinkProperties changes. - public void onLinkPropertiesChange(LinkProperties newLp) {} - - // Called when the internal IpReachabilityMonitor (if enabled) has - // detected the loss of a critical number of required neighbors. - public void onReachabilityLost(String logMsg) {} - - // Called when the IpManager state machine terminates. - public void onQuit() {} - - // Install an APF program to filter incoming packets. - public void installPacketFilter(byte[] filter) {} - - // If multicast filtering cannot be accomplished with APF, this function will be called to - // actuate multicast filtering using another means. - public void setFallbackMulticastFilter(boolean enabled) {} - - // Enabled/disable Neighbor Discover offload functionality. This is - // called, for example, whenever 464xlat is being started or stopped. - public void setNeighborDiscoveryOffload(boolean enable) {} - } - - public static class WaitForProvisioningCallback extends Callback { - private LinkProperties mCallbackLinkProperties; - - public LinkProperties waitForProvisioning() { - synchronized (this) { - try { - wait(); - } catch (InterruptedException e) {} - return mCallbackLinkProperties; - } +public class IpManager extends IpClient { + public static class ProvisioningConfiguration extends IpClient.ProvisioningConfiguration { + public ProvisioningConfiguration(IpClient.ProvisioningConfiguration ipcConfig) { + super(ipcConfig); } - @Override - public void onProvisioningSuccess(LinkProperties newLp) { - synchronized (this) { - mCallbackLinkProperties = newLp; - notify(); - } - } - - @Override - public void onProvisioningFailure(LinkProperties newLp) { - synchronized (this) { - mCallbackLinkProperties = null; - notify(); - } - } - } - - // Use a wrapper class to log in order to ensure complete and detailed - // logging. This method is lighter weight than annotations/reflection - // and has the following benefits: - // - // - No invoked method can be forgotten. - // Any new method added to IpManager.Callback must be overridden - // here or it will never be called. - // - // - No invoking call site can be forgotten. - // Centralized logging in this way means call sites don't need to - // remember to log, and therefore no call site can be forgotten. - // - // - No variation in log format among call sites. - // Encourages logging of any available arguments, and all call sites - // are necessarily logged identically. - // - // TODO: Find an lighter weight approach. - private class LoggingCallbackWrapper extends Callback { - private static final String PREFIX = "INVOKE "; - private Callback mCallback; - - public LoggingCallbackWrapper(Callback callback) { - mCallback = callback; - } - - private void log(String msg) { - mLog.log(PREFIX + msg); - } - - @Override - public void onPreDhcpAction() { - mCallback.onPreDhcpAction(); - log("onPreDhcpAction()"); - } - @Override - public void onPostDhcpAction() { - mCallback.onPostDhcpAction(); - log("onPostDhcpAction()"); - } - @Override - public void onNewDhcpResults(DhcpResults dhcpResults) { - mCallback.onNewDhcpResults(dhcpResults); - log("onNewDhcpResults({" + dhcpResults + "})"); - } - @Override - public void onProvisioningSuccess(LinkProperties newLp) { - mCallback.onProvisioningSuccess(newLp); - log("onProvisioningSuccess({" + newLp + "})"); - } - @Override - public void onProvisioningFailure(LinkProperties newLp) { - mCallback.onProvisioningFailure(newLp); - log("onProvisioningFailure({" + newLp + "})"); - } - @Override - public void onLinkPropertiesChange(LinkProperties newLp) { - mCallback.onLinkPropertiesChange(newLp); - log("onLinkPropertiesChange({" + newLp + "})"); - } - @Override - public void onReachabilityLost(String logMsg) { - mCallback.onReachabilityLost(logMsg); - log("onReachabilityLost(" + logMsg + ")"); - } - @Override - public void onQuit() { - mCallback.onQuit(); - log("onQuit()"); - } - @Override - public void installPacketFilter(byte[] filter) { - mCallback.installPacketFilter(filter); - log("installPacketFilter(byte[" + filter.length + "])"); - } - @Override - public void setFallbackMulticastFilter(boolean enabled) { - mCallback.setFallbackMulticastFilter(enabled); - log("setFallbackMulticastFilter(" + enabled + ")"); - } - @Override - public void setNeighborDiscoveryOffload(boolean enable) { - mCallback.setNeighborDiscoveryOffload(enable); - log("setNeighborDiscoveryOffload(" + enable + ")"); - } - } - - /** - * This class encapsulates parameters to be passed to - * IpManager#startProvisioning(). A defensive copy is made by IpManager - * and the values specified herein are in force until IpManager#stop() - * is called. - * - * Example use: - * - * final ProvisioningConfiguration config = - * mIpManager.buildProvisioningConfiguration() - * .withPreDhcpAction() - * .withProvisioningTimeoutMs(36 * 1000) - * .build(); - * mIpManager.startProvisioning(config); - * ... - * mIpManager.stop(); - * - * The specified provisioning configuration will only be active until - * IpManager#stop() is called. Future calls to IpManager#startProvisioning() - * must specify the configuration again. - */ - public static class ProvisioningConfiguration { - // TODO: Delete this default timeout once those callers that care are - // fixed to pass in their preferred timeout. - // - // We pick 36 seconds so we can send DHCP requests at - // - // t=0, t=2, t=6, t=14, t=30 - // - // allowing for 10% jitter. - private static final int DEFAULT_TIMEOUT_MS = 36 * 1000; - - public static class Builder { - private ProvisioningConfiguration mConfig = new ProvisioningConfiguration(); - + public static class Builder extends IpClient.ProvisioningConfiguration.Builder { + @Override public Builder withoutIPv4() { - mConfig.mEnableIPv4 = false; + super.withoutIPv4(); return this; } - + @Override public Builder withoutIPv6() { - mConfig.mEnableIPv6 = false; + super.withoutIPv6(); return this; } - + @Override public Builder withoutIpReachabilityMonitor() { - mConfig.mUsingIpReachabilityMonitor = false; + super.withoutIpReachabilityMonitor(); return this; } - + @Override public Builder withPreDhcpAction() { - mConfig.mRequestedPreDhcpActionMs = DEFAULT_TIMEOUT_MS; + super.withPreDhcpAction(); return this; } - + @Override public Builder withPreDhcpAction(int dhcpActionTimeoutMs) { - mConfig.mRequestedPreDhcpActionMs = dhcpActionTimeoutMs; + super.withPreDhcpAction(dhcpActionTimeoutMs); return this; } - + // No Override; locally defined type. public Builder withInitialConfiguration(InitialConfiguration initialConfig) { - mConfig.mInitialConfig = initialConfig; + super.withInitialConfiguration((IpClient.InitialConfiguration) initialConfig); return this; } - + @Override public Builder withStaticConfiguration(StaticIpConfiguration staticConfig) { - mConfig.mStaticIpConfig = staticConfig; + super.withStaticConfiguration(staticConfig); return this; } - + @Override public Builder withApfCapabilities(ApfCapabilities apfCapabilities) { - mConfig.mApfCapabilities = apfCapabilities; + super.withApfCapabilities(apfCapabilities); return this; } - + @Override public Builder withProvisioningTimeoutMs(int timeoutMs) { - mConfig.mProvisioningTimeoutMs = timeoutMs; + super.withProvisioningTimeoutMs(timeoutMs); return this; } - + @Override public Builder withIPv6AddrGenModeEUI64() { - mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_EUI64; + super.withIPv6AddrGenModeEUI64(); return this; } - + @Override public Builder withIPv6AddrGenModeStablePrivacy() { - mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY; + super.withIPv6AddrGenModeStablePrivacy(); return this; } - + @Override + public Builder withNetwork(Network network) { + super.withNetwork(network); + return this; + } + @Override + public Builder withDisplayName(String displayName) { + super.withDisplayName(displayName); + return this; + } + @Override public ProvisioningConfiguration build() { - return new ProvisioningConfiguration(mConfig); + return new ProvisioningConfiguration(super.build()); } } + } - /* package */ boolean mEnableIPv4 = true; - /* package */ boolean mEnableIPv6 = true; - /* package */ boolean mUsingIpReachabilityMonitor = true; - /* package */ int mRequestedPreDhcpActionMs; - /* package */ InitialConfiguration mInitialConfig; - /* package */ StaticIpConfiguration mStaticIpConfig; - /* package */ ApfCapabilities mApfCapabilities; - /* package */ int mProvisioningTimeoutMs = DEFAULT_TIMEOUT_MS; - /* package */ int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY; - - public ProvisioningConfiguration() {} // used by Builder - - public ProvisioningConfiguration(ProvisioningConfiguration other) { - mEnableIPv4 = other.mEnableIPv4; - mEnableIPv6 = other.mEnableIPv6; - mUsingIpReachabilityMonitor = other.mUsingIpReachabilityMonitor; - mRequestedPreDhcpActionMs = other.mRequestedPreDhcpActionMs; - mInitialConfig = InitialConfiguration.copy(other.mInitialConfig); - mStaticIpConfig = other.mStaticIpConfig; - mApfCapabilities = other.mApfCapabilities; - mProvisioningTimeoutMs = other.mProvisioningTimeoutMs; - } + public static ProvisioningConfiguration.Builder buildProvisioningConfiguration() { + return new ProvisioningConfiguration.Builder(); + } - @Override - public String toString() { - return new StringJoiner(", ", getClass().getSimpleName() + "{", "}") - .add("mEnableIPv4: " + mEnableIPv4) - .add("mEnableIPv6: " + mEnableIPv6) - .add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor) - .add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs) - .add("mInitialConfig: " + mInitialConfig) - .add("mStaticIpConfig: " + mStaticIpConfig) - .add("mApfCapabilities: " + mApfCapabilities) - .add("mProvisioningTimeoutMs: " + mProvisioningTimeoutMs) - .add("mIPv6AddrGenMode: " + mIPv6AddrGenMode) - .toString(); - } + public static class InitialConfiguration extends IpClient.InitialConfiguration { + } - public boolean isValid() { - return (mInitialConfig == null) || mInitialConfig.isValid(); - } + public static class Callback extends IpClient.Callback { } - public static class InitialConfiguration { - public final Set<LinkAddress> ipAddresses = new HashSet<>(); - public final Set<IpPrefix> directlyConnectedRoutes = new HashSet<>(); - public final Set<InetAddress> dnsServers = new HashSet<>(); - public Inet4Address gateway; // WiFi legacy behavior with static ipv4 config + public static class WaitForProvisioningCallback extends Callback { + private LinkProperties mCallbackLinkProperties; - public static InitialConfiguration copy(InitialConfiguration config) { - if (config == null) { - return null; + public LinkProperties waitForProvisioning() { + synchronized (this) { + try { + wait(); + } catch (InterruptedException e) {} + return mCallbackLinkProperties; } - InitialConfiguration configCopy = new InitialConfiguration(); - configCopy.ipAddresses.addAll(config.ipAddresses); - configCopy.directlyConnectedRoutes.addAll(config.directlyConnectedRoutes); - configCopy.dnsServers.addAll(config.dnsServers); - return configCopy; } @Override - public String toString() { - return String.format( - "InitialConfiguration(IPs: {%s}, prefixes: {%s}, DNS: {%s}, v4 gateway: %s)", - join(", ", ipAddresses), join(", ", directlyConnectedRoutes), - join(", ", dnsServers), gateway); - } - - public boolean isValid() { - if (ipAddresses.isEmpty()) { - return false; - } - - // For every IP address, there must be at least one prefix containing that address. - for (LinkAddress addr : ipAddresses) { - if (!any(directlyConnectedRoutes, (p) -> p.contains(addr.getAddress()))) { - return false; - } - } - // For every dns server, there must be at least one prefix containing that address. - for (InetAddress addr : dnsServers) { - if (!any(directlyConnectedRoutes, (p) -> p.contains(addr))) { - return false; - } - } - // All IPv6 LinkAddresses have an RFC7421-suitable prefix length - // (read: compliant with RFC4291#section2.5.4). - if (any(ipAddresses, not(InitialConfiguration::isPrefixLengthCompliant))) { - return false; - } - // If directlyConnectedRoutes contains an IPv6 default route - // then ipAddresses MUST contain at least one non-ULA GUA. - if (any(directlyConnectedRoutes, InitialConfiguration::isIPv6DefaultRoute) - && all(ipAddresses, not(InitialConfiguration::isIPv6GUA))) { - return false; - } - // The prefix length of routes in directlyConnectedRoutes be within reasonable - // bounds for IPv6: /48-/64 just as we’d accept in RIOs. - if (any(directlyConnectedRoutes, not(InitialConfiguration::isPrefixLengthCompliant))) { - return false; - } - // There no more than one IPv4 address - if (ipAddresses.stream().filter(Inet4Address.class::isInstance).count() > 1) { - return false; + public void onProvisioningSuccess(LinkProperties newLp) { + synchronized (this) { + mCallbackLinkProperties = newLp; + notify(); } - - return true; } - /** - * @return true if the given list of addressess and routes satisfies provisioning for this - * InitialConfiguration. LinkAddresses and RouteInfo objects are not compared with equality - * because addresses and routes seen by Netlink will contain additional fields like flags, - * interfaces, and so on. If this InitialConfiguration has no IP address specified, the - * provisioning check always fails. - * - * If the given list of routes is null, only addresses are taken into considerations. - */ - public boolean isProvisionedBy(List<LinkAddress> addresses, List<RouteInfo> routes) { - if (ipAddresses.isEmpty()) { - return false; - } - - for (LinkAddress addr : ipAddresses) { - if (!any(addresses, (addrSeen) -> addr.isSameAddressAs(addrSeen))) { - return false; - } - } - - if (routes != null) { - for (IpPrefix prefix : directlyConnectedRoutes) { - if (!any(routes, (routeSeen) -> isDirectlyConnectedRoute(routeSeen, prefix))) { - return false; - } - } + @Override + public void onProvisioningFailure(LinkProperties newLp) { + synchronized (this) { + mCallbackLinkProperties = null; + notify(); } - - return true; - } - - private static boolean isDirectlyConnectedRoute(RouteInfo route, IpPrefix prefix) { - return !route.hasGateway() && prefix.equals(route.getDestination()); - } - - private static boolean isPrefixLengthCompliant(LinkAddress addr) { - return addr.isIPv4() || isCompliantIPv6PrefixLength(addr.getPrefixLength()); - } - - private static boolean isPrefixLengthCompliant(IpPrefix prefix) { - return prefix.isIPv4() || isCompliantIPv6PrefixLength(prefix.getPrefixLength()); - } - - private static boolean isCompliantIPv6PrefixLength(int prefixLength) { - return (NetworkConstants.RFC6177_MIN_PREFIX_LENGTH <= prefixLength) - && (prefixLength <= NetworkConstants.RFC7421_PREFIX_LENGTH); - } - - private static boolean isIPv6DefaultRoute(IpPrefix prefix) { - return prefix.getAddress().equals(Inet6Address.ANY); - } - - private static boolean isIPv6GUA(LinkAddress addr) { - return addr.isIPv6() && addr.isGlobalPreferred(); } } - public static final String DUMP_ARG = "ipmanager"; - public static final String DUMP_ARG_CONFIRM = "confirm"; - - private static final int CMD_TERMINATE_AFTER_STOP = 1; - private static final int CMD_STOP = 2; - private static final int CMD_START = 3; - private static final int CMD_CONFIRM = 4; - private static final int EVENT_PRE_DHCP_ACTION_COMPLETE = 5; - // Sent by NetlinkTracker to communicate netlink events. - private static final int EVENT_NETLINK_LINKPROPERTIES_CHANGED = 6; - private static final int CMD_UPDATE_TCP_BUFFER_SIZES = 7; - private static final int CMD_UPDATE_HTTP_PROXY = 8; - private static final int CMD_SET_MULTICAST_FILTER = 9; - private static final int EVENT_PROVISIONING_TIMEOUT = 10; - private static final int EVENT_DHCPACTION_TIMEOUT = 11; - - private static final int MAX_LOG_RECORDS = 500; - private static final int MAX_PACKET_RECORDS = 100; - - private static final boolean NO_CALLBACKS = false; - private static final boolean SEND_CALLBACKS = true; - - // This must match the interface prefix in clatd.c. - // TODO: Revert this hack once IpManager and Nat464Xlat work in concert. - private static final String CLAT_PREFIX = "v4-"; - - private final State mStoppedState = new StoppedState(); - private final State mStoppingState = new StoppingState(); - private final State mStartedState = new StartedState(); - private final State mRunningState = new RunningState(); - - private final String mTag; - private final Context mContext; - private final String mInterfaceName; - private final String mClatInterfaceName; - @VisibleForTesting - protected final Callback mCallback; - private final INetworkManagementService mNwService; - private final NetlinkTracker mNetlinkTracker; - private final WakeupMessage mProvisioningTimeoutAlarm; - private final WakeupMessage mDhcpActionTimeoutAlarm; - private final MultinetworkPolicyTracker mMultinetworkPolicyTracker; - private final SharedLog mLog; - private final LocalLog mConnectivityPacketLog; - private final MessageHandlingLogger mMsgStateLogger; - private final IpConnectivityLog mMetricsLog = new IpConnectivityLog(); - private final InterfaceController mInterfaceCtrl; - - private NetworkInterface mNetworkInterface; - - /** - * Non-final member variables accessed only from within our StateMachine. - */ - private LinkProperties mLinkProperties; - private ProvisioningConfiguration mConfiguration; - private IpReachabilityMonitor mIpReachabilityMonitor; - private DhcpClient mDhcpClient; - private DhcpResults mDhcpResults; - private String mTcpBufferSizes; - private ProxyInfo mHttpProxy; - private ApfFilter mApfFilter; - private boolean mMulticastFiltering; - private long mStartTimeMillis; - public IpManager(Context context, String ifName, Callback callback) { this(context, ifName, callback, INetworkManagementService.Stub.asInterface( ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)), NetdService.getInstance()); } - /** - * An expanded constructor, useful for dependency injection. - * TODO: migrate all test users to mock IpManager directly and remove this ctor. - */ public IpManager(Context context, String ifName, Callback callback, INetworkManagementService nwService) { this(context, ifName, callback, nwService, NetdService.getInstance()); } @VisibleForTesting - IpManager(Context context, String ifName, Callback callback, + public IpManager(Context context, String ifName, Callback callback, INetworkManagementService nwService, INetd netd) { - super(IpManager.class.getSimpleName() + "." + ifName); - mTag = getName(); - - mContext = context; - mInterfaceName = ifName; - mClatInterfaceName = CLAT_PREFIX + ifName; - mCallback = new LoggingCallbackWrapper(callback); - mNwService = nwService; - - mLog = new SharedLog(MAX_LOG_RECORDS, mTag); - mConnectivityPacketLog = new LocalLog(MAX_PACKET_RECORDS); - mMsgStateLogger = new MessageHandlingLogger(); - - mInterfaceCtrl = new InterfaceController(mInterfaceName, mNwService, netd, mLog); - - mNetlinkTracker = new NetlinkTracker( - mInterfaceName, - new NetlinkTracker.Callback() { - @Override - public void update() { - sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED); - } - }) { - @Override - public void interfaceAdded(String iface) { - super.interfaceAdded(iface); - if (mClatInterfaceName.equals(iface)) { - mCallback.setNeighborDiscoveryOffload(false); - } else if (!mInterfaceName.equals(iface)) { - return; - } - - final String msg = "interfaceAdded(" + iface +")"; - logMsg(msg); - } - - @Override - public void interfaceRemoved(String iface) { - super.interfaceRemoved(iface); - // TODO: Also observe mInterfaceName going down and take some - // kind of appropriate action. - if (mClatInterfaceName.equals(iface)) { - // TODO: consider sending a message to the IpManager main - // StateMachine thread, in case "NDO enabled" state becomes - // tied to more things that 464xlat operation. - mCallback.setNeighborDiscoveryOffload(true); - } else if (!mInterfaceName.equals(iface)) { - return; - } - - final String msg = "interfaceRemoved(" + iface +")"; - logMsg(msg); - } - - private void logMsg(String msg) { - Log.d(mTag, msg); - getHandler().post(() -> { mLog.log("OBSERVED " + msg); }); - } - }; - - mLinkProperties = new LinkProperties(); - mLinkProperties.setInterfaceName(mInterfaceName); - - mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(mContext, getHandler(), - () -> { mLog.log("OBSERVED AvoidBadWifi changed"); }); - - mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(), - mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT); - mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(), - mTag + ".EVENT_DHCPACTION_TIMEOUT", EVENT_DHCPACTION_TIMEOUT); - - // Anything the StateMachine may access must have been instantiated - // before this point. - configureAndStartStateMachine(); - - // Anything that may send messages to the StateMachine must only be - // configured to do so after the StateMachine has started (above). - startStateMachineUpdaters(); - } - - private void configureAndStartStateMachine() { - addState(mStoppedState); - addState(mStartedState); - addState(mRunningState, mStartedState); - addState(mStoppingState); - - setInitialState(mStoppedState); - - super.start(); - } - - private void startStateMachineUpdaters() { - try { - mNwService.registerObserver(mNetlinkTracker); - } catch (RemoteException e) { - logError("Couldn't register NetlinkTracker: %s", e); - } - - mMultinetworkPolicyTracker.start(); - } - - private void stopStateMachineUpdaters() { - try { - mNwService.unregisterObserver(mNetlinkTracker); - } catch (RemoteException e) { - logError("Couldn't unregister NetlinkTracker: %s", e); - } - - mMultinetworkPolicyTracker.shutdown(); - } - - @Override - protected void onQuitting() { - mCallback.onQuit(); - } - - // Shut down this IpManager instance altogether. - public void shutdown() { - stop(); - sendMessage(CMD_TERMINATE_AFTER_STOP); - } - - public static ProvisioningConfiguration.Builder buildProvisioningConfiguration() { - return new ProvisioningConfiguration.Builder(); + super(context, ifName, callback, nwService, netd); } public void startProvisioning(ProvisioningConfiguration req) { - if (!req.isValid()) { - doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING); - return; - } - - getNetworkInterface(); - - mCallback.setNeighborDiscoveryOffload(true); - sendMessage(CMD_START, new ProvisioningConfiguration(req)); - } - - // TODO: Delete this. - public void startProvisioning(StaticIpConfiguration staticIpConfig) { - startProvisioning(buildProvisioningConfiguration() - .withStaticConfiguration(staticIpConfig) - .build()); - } - - public void startProvisioning() { - startProvisioning(new ProvisioningConfiguration()); - } - - public void stop() { - sendMessage(CMD_STOP); - } - - public void confirmConfiguration() { - sendMessage(CMD_CONFIRM); - } - - public void completedPreDhcpAction() { - sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE); - } - - /** - * Set the TCP buffer sizes to use. - * - * This may be called, repeatedly, at any time before or after a call to - * #startProvisioning(). The setting is cleared upon calling #stop(). - */ - public void setTcpBufferSizes(String tcpBufferSizes) { - sendMessage(CMD_UPDATE_TCP_BUFFER_SIZES, tcpBufferSizes); - } - - /** - * Set the HTTP Proxy configuration to use. - * - * This may be called, repeatedly, at any time before or after a call to - * #startProvisioning(). The setting is cleared upon calling #stop(). - */ - public void setHttpProxy(ProxyInfo proxyInfo) { - sendMessage(CMD_UPDATE_HTTP_PROXY, proxyInfo); - } - - /** - * Enable or disable the multicast filter. Attempts to use APF to accomplish the filtering, - * if not, Callback.setFallbackMulticastFilter() is called. - */ - public void setMulticastFilter(boolean enabled) { - sendMessage(CMD_SET_MULTICAST_FILTER, enabled); - } - - public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { - if (args != null && args.length > 0 && DUMP_ARG_CONFIRM.equals(args[0])) { - // Execute confirmConfiguration() and take no further action. - confirmConfiguration(); - return; - } - - // Thread-unsafe access to mApfFilter but just used for debugging. - final ApfFilter apfFilter = mApfFilter; - final ProvisioningConfiguration provisioningConfig = mConfiguration; - final ApfCapabilities apfCapabilities = (provisioningConfig != null) - ? provisioningConfig.mApfCapabilities : null; - - IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); - pw.println(mTag + " APF dump:"); - pw.increaseIndent(); - if (apfFilter != null) { - apfFilter.dump(pw); - } else { - pw.print("No active ApfFilter; "); - if (provisioningConfig == null) { - pw.println("IpManager not yet started."); - } else if (apfCapabilities == null || apfCapabilities.apfVersionSupported == 0) { - pw.println("Hardware does not support APF."); - } else { - pw.println("ApfFilter not yet started, APF capabilities: " + apfCapabilities); - } - } - pw.decreaseIndent(); - - pw.println(); - pw.println(mTag + " current ProvisioningConfiguration:"); - pw.increaseIndent(); - pw.println(Objects.toString(provisioningConfig, "N/A")); - pw.decreaseIndent(); - - pw.println(); - pw.println(mTag + " StateMachine dump:"); - pw.increaseIndent(); - mLog.dump(fd, pw, args); - pw.decreaseIndent(); - - pw.println(); - pw.println(mTag + " connectivity packet log:"); - pw.println(); - pw.println("Debug with python and scapy via:"); - pw.println("shell$ python"); - pw.println(">>> from scapy import all as scapy"); - pw.println(">>> scapy.Ether(\"<paste_hex_string>\".decode(\"hex\")).show2()"); - pw.println(); - - pw.increaseIndent(); - mConnectivityPacketLog.readOnlyLocalLog().dump(fd, pw, args); - pw.decreaseIndent(); - } - - - /** - * Internals. - */ - - @Override - protected String getWhatToString(int what) { - return sWhatToString.get(what, "UNKNOWN: " + Integer.toString(what)); - } - - @Override - protected String getLogRecString(Message msg) { - final String logLine = String.format( - "%s/%d %d %d %s [%s]", - mInterfaceName, mNetworkInterface == null ? -1 : mNetworkInterface.getIndex(), - msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger); - - final String richerLogLine = getWhatToString(msg.what) + " " + logLine; - mLog.log(richerLogLine); - if (DBG) { - Log.d(mTag, richerLogLine); - } - - mMsgStateLogger.reset(); - return logLine; - } - - @Override - protected boolean recordLogRec(Message msg) { - // Don't log EVENT_NETLINK_LINKPROPERTIES_CHANGED. They can be noisy, - // and we already log any LinkProperties change that results in an - // invocation of IpManager.Callback#onLinkPropertiesChange(). - final boolean shouldLog = (msg.what != EVENT_NETLINK_LINKPROPERTIES_CHANGED); - if (!shouldLog) { - mMsgStateLogger.reset(); - } - return shouldLog; - } - - private void logError(String fmt, Object... args) { - final String msg = "ERROR " + String.format(fmt, args); - Log.e(mTag, msg); - mLog.log(msg); - } - - private void getNetworkInterface() { - try { - mNetworkInterface = NetworkInterface.getByName(mInterfaceName); - } catch (SocketException | NullPointerException e) { - // TODO: throw new IllegalStateException. - logError("Failed to get interface object: %s", e); - } - } - - // This needs to be called with care to ensure that our LinkProperties - // are in sync with the actual LinkProperties of the interface. For example, - // we should only call this if we know for sure that there are no IP addresses - // assigned to the interface, etc. - private void resetLinkProperties() { - mNetlinkTracker.clearLinkProperties(); - mConfiguration = null; - mDhcpResults = null; - mTcpBufferSizes = ""; - mHttpProxy = null; - - mLinkProperties = new LinkProperties(); - mLinkProperties.setInterfaceName(mInterfaceName); - } - - private void recordMetric(final int type) { - if (mStartTimeMillis <= 0) { Log.wtf(mTag, "Start time undefined!"); } - final long duration = SystemClock.elapsedRealtime() - mStartTimeMillis; - mMetricsLog.log(mInterfaceName, new IpManagerEvent(type, duration)); - } - - // For now: use WifiStateMachine's historical notion of provisioned. - @VisibleForTesting - static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) { - // For historical reasons, we should connect even if all we have is - // an IPv4 address and nothing else. - if (lp.hasIPv4Address() || lp.isProvisioned()) { - return true; - } - if (config == null) { - return false; - } - - // When an InitialConfiguration is specified, ignore any difference with previous - // properties and instead check if properties observed match the desired properties. - return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes()); - } - - // TODO: Investigate folding all this into the existing static function - // LinkProperties.compareProvisioning() or some other single function that - // takes two LinkProperties objects and returns a ProvisioningChange - // object that is a correct and complete assessment of what changed, taking - // account of the asymmetries described in the comments in this function. - // Then switch to using it everywhere (IpReachabilityMonitor, etc.). - private ProvisioningChange compareProvisioning(LinkProperties oldLp, LinkProperties newLp) { - ProvisioningChange delta; - InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null; - final boolean wasProvisioned = isProvisioned(oldLp, config); - final boolean isProvisioned = isProvisioned(newLp, config); - - if (!wasProvisioned && isProvisioned) { - delta = ProvisioningChange.GAINED_PROVISIONING; - } else if (wasProvisioned && isProvisioned) { - delta = ProvisioningChange.STILL_PROVISIONED; - } else if (!wasProvisioned && !isProvisioned) { - delta = ProvisioningChange.STILL_NOT_PROVISIONED; - } else { - // (wasProvisioned && !isProvisioned) - // - // Note that this is true even if we lose a configuration element - // (e.g., a default gateway) that would not be required to advance - // into provisioned state. This is intended: if we have a default - // router and we lose it, that's a sure sign of a problem, but if - // we connect to a network with no IPv4 DNS servers, we consider - // that to be a network without DNS servers and connect anyway. - // - // See the comment below. - delta = ProvisioningChange.LOST_PROVISIONING; - } - - final boolean lostIPv6 = oldLp.isIPv6Provisioned() && !newLp.isIPv6Provisioned(); - final boolean lostIPv4Address = oldLp.hasIPv4Address() && !newLp.hasIPv4Address(); - final boolean lostIPv6Router = oldLp.hasIPv6DefaultRoute() && !newLp.hasIPv6DefaultRoute(); - - // If bad wifi avoidance is disabled, then ignore IPv6 loss of - // provisioning. Otherwise, when a hotspot that loses Internet - // access sends out a 0-lifetime RA to its clients, the clients - // will disconnect and then reconnect, avoiding the bad hotspot, - // instead of getting stuck on the bad hotspot. http://b/31827713 . - // - // This is incorrect because if the hotspot then regains Internet - // access with a different prefix, TCP connections on the - // deprecated addresses will remain stuck. - // - // Note that we can still be disconnected by IpReachabilityMonitor - // if the IPv6 default gateway (but not the IPv6 DNS servers; see - // accompanying code in IpReachabilityMonitor) is unreachable. - final boolean ignoreIPv6ProvisioningLoss = !mMultinetworkPolicyTracker.getAvoidBadWifi(); - - // Additionally: - // - // Partial configurations (e.g., only an IPv4 address with no DNS - // servers and no default route) are accepted as long as DHCPv4 - // succeeds. On such a network, isProvisioned() will always return - // false, because the configuration is not complete, but we want to - // connect anyway. It might be a disconnected network such as a - // Chromecast or a wireless printer, for example. - // - // Because on such a network isProvisioned() will always return false, - // delta will never be LOST_PROVISIONING. So check for loss of - // provisioning here too. - if (lostIPv4Address || (lostIPv6 && !ignoreIPv6ProvisioningLoss)) { - delta = ProvisioningChange.LOST_PROVISIONING; - } - - // Additionally: - // - // If the previous link properties had a global IPv6 address and an - // IPv6 default route then also consider the loss of that default route - // to be a loss of provisioning. See b/27962810. - if (oldLp.hasGlobalIPv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) { - delta = ProvisioningChange.LOST_PROVISIONING; - } - - return delta; - } - - private void dispatchCallback(ProvisioningChange delta, LinkProperties newLp) { - switch (delta) { - case GAINED_PROVISIONING: - if (DBG) { Log.d(mTag, "onProvisioningSuccess()"); } - recordMetric(IpManagerEvent.PROVISIONING_OK); - mCallback.onProvisioningSuccess(newLp); - break; - - case LOST_PROVISIONING: - if (DBG) { Log.d(mTag, "onProvisioningFailure()"); } - recordMetric(IpManagerEvent.PROVISIONING_FAIL); - mCallback.onProvisioningFailure(newLp); - break; - - default: - if (DBG) { Log.d(mTag, "onLinkPropertiesChange()"); } - mCallback.onLinkPropertiesChange(newLp); - break; - } - } - - // Updates all IpManager-related state concerned with LinkProperties. - // Returns a ProvisioningChange for possibly notifying other interested - // parties that are not fronted by IpManager. - private ProvisioningChange setLinkProperties(LinkProperties newLp) { - if (mApfFilter != null) { - mApfFilter.setLinkProperties(newLp); - } - if (mIpReachabilityMonitor != null) { - mIpReachabilityMonitor.updateLinkProperties(newLp); - } - - ProvisioningChange delta = compareProvisioning(mLinkProperties, newLp); - mLinkProperties = new LinkProperties(newLp); - - if (delta == ProvisioningChange.GAINED_PROVISIONING) { - // TODO: Add a proper ProvisionedState and cancel the alarm in - // its enter() method. - mProvisioningTimeoutAlarm.cancel(); - } - - return delta; - } - - private LinkProperties assembleLinkProperties() { - // [1] Create a new LinkProperties object to populate. - LinkProperties newLp = new LinkProperties(); - newLp.setInterfaceName(mInterfaceName); - - // [2] Pull in data from netlink: - // - IPv4 addresses - // - IPv6 addresses - // - IPv6 routes - // - IPv6 DNS servers - // - // N.B.: this is fundamentally race-prone and should be fixed by - // changing NetlinkTracker from a hybrid edge/level model to an - // edge-only model, or by giving IpManager its own netlink socket(s) - // so as to track all required information directly. - LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties(); - newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses()); - for (RouteInfo route : netlinkLinkProperties.getRoutes()) { - newLp.addRoute(route); - } - addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers()); - - // [3] Add in data from DHCPv4, if available. - // - // mDhcpResults is never shared with any other owner so we don't have - // to worry about concurrent modification. - if (mDhcpResults != null) { - for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) { - newLp.addRoute(route); - } - addAllReachableDnsServers(newLp, mDhcpResults.dnsServers); - newLp.setDomains(mDhcpResults.domains); - - if (mDhcpResults.mtu != 0) { - newLp.setMtu(mDhcpResults.mtu); - } - } - - // [4] Add in TCP buffer sizes and HTTP Proxy config, if available. - if (!TextUtils.isEmpty(mTcpBufferSizes)) { - newLp.setTcpBufferSizes(mTcpBufferSizes); - } - if (mHttpProxy != null) { - newLp.setHttpProxy(mHttpProxy); - } - - // [5] Add data from InitialConfiguration - if (mConfiguration != null && mConfiguration.mInitialConfig != null) { - InitialConfiguration config = mConfiguration.mInitialConfig; - // Add InitialConfiguration routes and dns server addresses once all addresses - // specified in the InitialConfiguration have been observed with Netlink. - if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) { - for (IpPrefix prefix : config.directlyConnectedRoutes) { - newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName)); - } - } - addAllReachableDnsServers(newLp, config.dnsServers); - } - final LinkProperties oldLp = mLinkProperties; - if (DBG) { - Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s", - netlinkLinkProperties, newLp, oldLp)); - } - - // TODO: also learn via netlink routes specified by an InitialConfiguration and specified - // from a static IP v4 config instead of manually patching them in in steps [3] and [5]. - return newLp; - } - - private static void addAllReachableDnsServers( - LinkProperties lp, Iterable<InetAddress> dnses) { - // TODO: Investigate deleting this reachability check. We should be - // able to pass everything down to netd and let netd do evaluation - // and RFC6724-style sorting. - for (InetAddress dns : dnses) { - if (!dns.isAnyLocalAddress() && lp.isReachable(dns)) { - lp.addDnsServer(dns); - } - } - } - - // Returns false if we have lost provisioning, true otherwise. - private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) { - final LinkProperties newLp = assembleLinkProperties(); - if (Objects.equals(newLp, mLinkProperties)) { - return true; - } - final ProvisioningChange delta = setLinkProperties(newLp); - if (sendCallbacks) { - dispatchCallback(delta, newLp); - } - return (delta != ProvisioningChange.LOST_PROVISIONING); - } - - private void handleIPv4Success(DhcpResults dhcpResults) { - mDhcpResults = new DhcpResults(dhcpResults); - final LinkProperties newLp = assembleLinkProperties(); - final ProvisioningChange delta = setLinkProperties(newLp); - - if (DBG) { - Log.d(mTag, "onNewDhcpResults(" + Objects.toString(dhcpResults) + ")"); - } - mCallback.onNewDhcpResults(dhcpResults); - dispatchCallback(delta, newLp); - } - - private void handleIPv4Failure() { - // TODO: Investigate deleting this clearIPv4Address() call. - // - // DhcpClient will send us CMD_CLEAR_LINKADDRESS in all circumstances - // that could trigger a call to this function. If we missed handling - // that message in StartedState for some reason we would still clear - // any addresses upon entry to StoppedState. - mInterfaceCtrl.clearIPv4Address(); - mDhcpResults = null; - if (DBG) { Log.d(mTag, "onNewDhcpResults(null)"); } - mCallback.onNewDhcpResults(null); - - handleProvisioningFailure(); - } - - private void handleProvisioningFailure() { - final LinkProperties newLp = assembleLinkProperties(); - ProvisioningChange delta = setLinkProperties(newLp); - // If we've gotten here and we're still not provisioned treat that as - // a total loss of provisioning. - // - // Either (a) static IP configuration failed or (b) DHCPv4 failed AND - // there was no usable IPv6 obtained before a non-zero provisioning - // timeout expired. - // - // Regardless: GAME OVER. - if (delta == ProvisioningChange.STILL_NOT_PROVISIONED) { - delta = ProvisioningChange.LOST_PROVISIONING; - } - - dispatchCallback(delta, newLp); - if (delta == ProvisioningChange.LOST_PROVISIONING) { - transitionTo(mStoppingState); - } - } - - private void doImmediateProvisioningFailure(int failureType) { - logError("onProvisioningFailure(): %s", failureType); - recordMetric(failureType); - mCallback.onProvisioningFailure(new LinkProperties(mLinkProperties)); - } - - private boolean startIPv4() { - // If we have a StaticIpConfiguration attempt to apply it and - // handle the result accordingly. - if (mConfiguration.mStaticIpConfig != null) { - if (mInterfaceCtrl.setIPv4Address(mConfiguration.mStaticIpConfig.ipAddress)) { - handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig)); - } else { - return false; - } - } else { - // Start DHCPv4. - mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpManager.this, mInterfaceName); - mDhcpClient.registerForPreDhcpNotification(); - mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP); - } - - return true; - } - - private boolean startIPv6() { - return mInterfaceCtrl.setIPv6PrivacyExtensions(true) && - mInterfaceCtrl.setIPv6AddrGenModeIfSupported(mConfiguration.mIPv6AddrGenMode) && - mInterfaceCtrl.enableIPv6(); - } - - private boolean applyInitialConfig(InitialConfiguration config) { - // TODO: also support specifying a static IPv4 configuration in InitialConfiguration. - for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIPv6)) { - if (!mInterfaceCtrl.addAddress(addr)) return false; - } - - return true; - } - - private boolean startIpReachabilityMonitor() { - try { - mIpReachabilityMonitor = new IpReachabilityMonitor( - mContext, - mInterfaceName, - mLog, - new IpReachabilityMonitor.Callback() { - @Override - public void notifyLost(InetAddress ip, String logMsg) { - mCallback.onReachabilityLost(logMsg); - } - }, - mMultinetworkPolicyTracker); - } catch (IllegalArgumentException iae) { - // Failed to start IpReachabilityMonitor. Log it and call - // onProvisioningFailure() immediately. - // - // See http://b/31038971. - logError("IpReachabilityMonitor failure: %s", iae); - mIpReachabilityMonitor = null; - } - - return (mIpReachabilityMonitor != null); - } - - private void stopAllIP() { - // We don't need to worry about routes, just addresses, because: - // - disableIpv6() will clear autoconf IPv6 routes as well, and - // - we don't get IPv4 routes from netlink - // so we neither react to nor need to wait for changes in either. - - mInterfaceCtrl.disableIPv6(); - mInterfaceCtrl.clearAllAddresses(); - } - - class StoppedState extends State { - @Override - public void enter() { - stopAllIP(); - - resetLinkProperties(); - if (mStartTimeMillis > 0) { - recordMetric(IpManagerEvent.COMPLETE_LIFECYCLE); - mStartTimeMillis = 0; - } - } - - @Override - public boolean processMessage(Message msg) { - switch (msg.what) { - case CMD_TERMINATE_AFTER_STOP: - stopStateMachineUpdaters(); - quit(); - break; - - case CMD_STOP: - break; - - case CMD_START: - mConfiguration = (ProvisioningConfiguration) msg.obj; - transitionTo(mStartedState); - break; - - case EVENT_NETLINK_LINKPROPERTIES_CHANGED: - handleLinkPropertiesUpdate(NO_CALLBACKS); - break; - - case CMD_UPDATE_TCP_BUFFER_SIZES: - mTcpBufferSizes = (String) msg.obj; - handleLinkPropertiesUpdate(NO_CALLBACKS); - break; - - case CMD_UPDATE_HTTP_PROXY: - mHttpProxy = (ProxyInfo) msg.obj; - handleLinkPropertiesUpdate(NO_CALLBACKS); - break; - - case CMD_SET_MULTICAST_FILTER: - mMulticastFiltering = (boolean) msg.obj; - break; - - case DhcpClient.CMD_ON_QUIT: - // Everything is already stopped. - logError("Unexpected CMD_ON_QUIT (already stopped)."); - break; - - default: - return NOT_HANDLED; - } - - mMsgStateLogger.handled(this, getCurrentState()); - return HANDLED; - } - } - - class StoppingState extends State { - @Override - public void enter() { - if (mDhcpClient == null) { - // There's no DHCPv4 for which to wait; proceed to stopped. - transitionTo(mStoppedState); - } - } - - @Override - public boolean processMessage(Message msg) { - switch (msg.what) { - case CMD_STOP: - break; - - case DhcpClient.CMD_CLEAR_LINKADDRESS: - mInterfaceCtrl.clearIPv4Address(); - break; - - case DhcpClient.CMD_ON_QUIT: - mDhcpClient = null; - transitionTo(mStoppedState); - break; - - default: - deferMessage(msg); - } - - mMsgStateLogger.handled(this, getCurrentState()); - return HANDLED; - } - } - - class StartedState extends State { - @Override - public void enter() { - mStartTimeMillis = SystemClock.elapsedRealtime(); - - if (mConfiguration.mProvisioningTimeoutMs > 0) { - final long alarmTime = SystemClock.elapsedRealtime() + - mConfiguration.mProvisioningTimeoutMs; - mProvisioningTimeoutAlarm.schedule(alarmTime); - } - - if (readyToProceed()) { - transitionTo(mRunningState); - } else { - // Clear all IPv4 and IPv6 before proceeding to RunningState. - // Clean up any leftover state from an abnormal exit from - // tethering or during an IpManager restart. - stopAllIP(); - } - } - - @Override - public void exit() { - mProvisioningTimeoutAlarm.cancel(); - } - - @Override - public boolean processMessage(Message msg) { - switch (msg.what) { - case CMD_STOP: - transitionTo(mStoppingState); - break; - - case EVENT_NETLINK_LINKPROPERTIES_CHANGED: - handleLinkPropertiesUpdate(NO_CALLBACKS); - if (readyToProceed()) { - transitionTo(mRunningState); - } - break; - - case EVENT_PROVISIONING_TIMEOUT: - handleProvisioningFailure(); - break; - - default: - // It's safe to process messages out of order because the - // only message that can both - // a) be received at this time and - // b) affect provisioning state - // is EVENT_NETLINK_LINKPROPERTIES_CHANGED (handled above). - deferMessage(msg); - } - - mMsgStateLogger.handled(this, getCurrentState()); - return HANDLED; - } - - boolean readyToProceed() { - return (!mLinkProperties.hasIPv4Address() && - !mLinkProperties.hasGlobalIPv6Address()); - } - } - - class RunningState extends State { - private ConnectivityPacketTracker mPacketTracker; - private boolean mDhcpActionInFlight; - - @Override - public void enter() { - // Get the Configuration for ApfFilter from Context - boolean filter802_3Frames = - mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames); - - int[] ethTypeBlackList = mContext.getResources().getIntArray( - R.array.config_apfEthTypeBlackList); - - mApfFilter = ApfFilter.maybeCreate(mConfiguration.mApfCapabilities, mNetworkInterface, - mCallback, mMulticastFiltering, filter802_3Frames, ethTypeBlackList); - // TODO: investigate the effects of any multicast filtering racing/interfering with the - // rest of this IP configuration startup. - if (mApfFilter == null) { - mCallback.setFallbackMulticastFilter(mMulticastFiltering); - } - - mPacketTracker = createPacketTracker(); - if (mPacketTracker != null) mPacketTracker.start(); - - if (mConfiguration.mEnableIPv6 && !startIPv6()) { - doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6); - transitionTo(mStoppingState); - return; - } - - if (mConfiguration.mEnableIPv4 && !startIPv4()) { - doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV4); - transitionTo(mStoppingState); - return; - } - - InitialConfiguration config = mConfiguration.mInitialConfig; - if ((config != null) && !applyInitialConfig(config)) { - // TODO introduce a new IpManagerEvent constant to distinguish this error case. - doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING); - transitionTo(mStoppingState); - return; - } - - if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) { - doImmediateProvisioningFailure( - IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR); - transitionTo(mStoppingState); - return; - } - } - - @Override - public void exit() { - stopDhcpAction(); - - if (mIpReachabilityMonitor != null) { - mIpReachabilityMonitor.stop(); - mIpReachabilityMonitor = null; - } - - if (mDhcpClient != null) { - mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP); - mDhcpClient.doQuit(); - } - - if (mPacketTracker != null) { - mPacketTracker.stop(); - mPacketTracker = null; - } - - if (mApfFilter != null) { - mApfFilter.shutdown(); - mApfFilter = null; - } - - resetLinkProperties(); - } - - private ConnectivityPacketTracker createPacketTracker() { - try { - return new ConnectivityPacketTracker( - getHandler(), mNetworkInterface, mConnectivityPacketLog); - } catch (IllegalArgumentException e) { - return null; - } - } - - private void ensureDhcpAction() { - if (!mDhcpActionInFlight) { - mCallback.onPreDhcpAction(); - mDhcpActionInFlight = true; - final long alarmTime = SystemClock.elapsedRealtime() + - mConfiguration.mRequestedPreDhcpActionMs; - mDhcpActionTimeoutAlarm.schedule(alarmTime); - } - } - - private void stopDhcpAction() { - mDhcpActionTimeoutAlarm.cancel(); - if (mDhcpActionInFlight) { - mCallback.onPostDhcpAction(); - mDhcpActionInFlight = false; - } - } - - @Override - public boolean processMessage(Message msg) { - switch (msg.what) { - case CMD_STOP: - transitionTo(mStoppingState); - break; - - case CMD_START: - logError("ALERT: START received in StartedState. Please fix caller."); - break; - - case CMD_CONFIRM: - // TODO: Possibly introduce a second type of confirmation - // that both probes (a) on-link neighbors and (b) does - // a DHCPv4 RENEW. We used to do this on Wi-Fi framework - // roams. - if (mIpReachabilityMonitor != null) { - mIpReachabilityMonitor.probeAll(); - } - break; - - case EVENT_PRE_DHCP_ACTION_COMPLETE: - // It's possible to reach here if, for example, someone - // calls completedPreDhcpAction() after provisioning with - // a static IP configuration. - if (mDhcpClient != null) { - mDhcpClient.sendMessage(DhcpClient.CMD_PRE_DHCP_ACTION_COMPLETE); - } - break; - - case EVENT_NETLINK_LINKPROPERTIES_CHANGED: - if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) { - transitionTo(mStoppingState); - } - break; - - case CMD_UPDATE_TCP_BUFFER_SIZES: - mTcpBufferSizes = (String) msg.obj; - // This cannot possibly change provisioning state. - handleLinkPropertiesUpdate(SEND_CALLBACKS); - break; - - case CMD_UPDATE_HTTP_PROXY: - mHttpProxy = (ProxyInfo) msg.obj; - // This cannot possibly change provisioning state. - handleLinkPropertiesUpdate(SEND_CALLBACKS); - break; - - case CMD_SET_MULTICAST_FILTER: { - mMulticastFiltering = (boolean) msg.obj; - if (mApfFilter != null) { - mApfFilter.setMulticastFilter(mMulticastFiltering); - } else { - mCallback.setFallbackMulticastFilter(mMulticastFiltering); - } - break; - } - - case EVENT_DHCPACTION_TIMEOUT: - stopDhcpAction(); - break; - - case DhcpClient.CMD_PRE_DHCP_ACTION: - if (mConfiguration.mRequestedPreDhcpActionMs > 0) { - ensureDhcpAction(); - } else { - sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE); - } - break; - - case DhcpClient.CMD_CLEAR_LINKADDRESS: - mInterfaceCtrl.clearIPv4Address(); - break; - - case DhcpClient.CMD_CONFIGURE_LINKADDRESS: { - final LinkAddress ipAddress = (LinkAddress) msg.obj; - if (mInterfaceCtrl.setIPv4Address(ipAddress)) { - mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED); - } else { - logError("Failed to set IPv4 address."); - dispatchCallback(ProvisioningChange.LOST_PROVISIONING, - new LinkProperties(mLinkProperties)); - transitionTo(mStoppingState); - } - break; - } - - // This message is only received when: - // - // a) initial address acquisition succeeds, - // b) renew succeeds or is NAK'd, - // c) rebind succeeds or is NAK'd, or - // c) the lease expires, - // - // but never when initial address acquisition fails. The latter - // condition is now governed by the provisioning timeout. - case DhcpClient.CMD_POST_DHCP_ACTION: - stopDhcpAction(); - - switch (msg.arg1) { - case DhcpClient.DHCP_SUCCESS: - handleIPv4Success((DhcpResults) msg.obj); - break; - case DhcpClient.DHCP_FAILURE: - handleIPv4Failure(); - break; - default: - logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1); - } - break; - - case DhcpClient.CMD_ON_QUIT: - // DHCPv4 quit early for some reason. - logError("Unexpected CMD_ON_QUIT."); - mDhcpClient = null; - break; - - default: - return NOT_HANDLED; - } - - mMsgStateLogger.handled(this, getCurrentState()); - return HANDLED; - } - } - - private static class MessageHandlingLogger { - public String processedInState; - public String receivedInState; - - public void reset() { - processedInState = null; - receivedInState = null; - } - - public void handled(State processedIn, IState receivedIn) { - processedInState = processedIn.getClass().getSimpleName(); - receivedInState = receivedIn.getName(); - } - - public String toString() { - return String.format("rcvd_in=%s, proc_in=%s", - receivedInState, processedInState); - } - } - - // TODO: extract out into CollectionUtils. - static <T> boolean any(Iterable<T> coll, Predicate<T> fn) { - for (T t : coll) { - if (fn.test(t)) { - return true; - } - } - return false; - } - - static <T> boolean all(Iterable<T> coll, Predicate<T> fn) { - return !any(coll, not(fn)); - } - - static <T> Predicate<T> not(Predicate<T> fn) { - return (t) -> !fn.test(t); - } - - static <T> String join(String delimiter, Collection<T> coll) { - return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter)); - } - - static <T> T find(Iterable<T> coll, Predicate<T> fn) { - for (T t: coll) { - if (fn.test(t)) { - return t; - } - } - return null; - } - - static <T> List<T> findAll(Collection<T> coll, Predicate<T> fn) { - return coll.stream().filter(fn).collect(Collectors.toList()); + super.startProvisioning((IpClient.ProvisioningConfiguration) req); } } diff --git a/android/net/metrics/ConnectStats.java b/android/net/metrics/ConnectStats.java index 30b26562..2495cab1 100644 --- a/android/net/metrics/ConnectStats.java +++ b/android/net/metrics/ConnectStats.java @@ -20,6 +20,7 @@ import android.net.NetworkCapabilities; import android.system.OsConstants; import android.util.IntArray; import android.util.SparseIntArray; + import com.android.internal.util.BitUtils; import com.android.internal.util.TokenBucket; @@ -43,6 +44,8 @@ public class ConnectStats { public final TokenBucket mLatencyTb; /** Maximum number of latency values recorded. */ public final int mMaxLatencyRecords; + /** Total count of events */ + public int eventCount = 0; /** Total count of successful connects. */ public int connectCount = 0; /** Total count of successful connects done in blocking mode. */ @@ -57,12 +60,15 @@ public class ConnectStats { mMaxLatencyRecords = maxLatencyRecords; } - public void addEvent(int errno, int latencyMs, String ipAddr) { + boolean addEvent(int errno, int latencyMs, String ipAddr) { + eventCount++; if (isSuccess(errno)) { countConnect(errno, ipAddr); countLatency(errno, latencyMs); + return true; } else { countError(errno); + return false; } } @@ -101,7 +107,7 @@ public class ConnectStats { return (errno == 0) || isNonBlocking(errno); } - private static boolean isNonBlocking(int errno) { + static boolean isNonBlocking(int errno) { // On non-blocking TCP sockets, connect() immediately returns EINPROGRESS. // On non-blocking TCP sockets that are connecting, connect() immediately returns EALREADY. return (errno == EINPROGRESS) || (errno == EALREADY); @@ -117,6 +123,7 @@ public class ConnectStats { for (int t : BitUtils.unpackBits(transports)) { builder.append(NetworkCapabilities.transportNameOf(t)).append(", "); } + builder.append(String.format("%d events, ", eventCount)); builder.append(String.format("%d success, ", connectCount)); builder.append(String.format("%d blocking, ", connectBlockingCount)); builder.append(String.format("%d IPv6 dst", ipv6ConnectCount)); diff --git a/android/net/metrics/DnsEvent.java b/android/net/metrics/DnsEvent.java index a4970e4d..81b098bb 100644 --- a/android/net/metrics/DnsEvent.java +++ b/android/net/metrics/DnsEvent.java @@ -17,11 +17,13 @@ package android.net.metrics; import android.net.NetworkCapabilities; -import java.util.Arrays; + import com.android.internal.util.BitUtils; +import java.util.Arrays; + /** - * A DNS event recorded by NetdEventListenerService. + * A batch of DNS events recorded by NetdEventListenerService for a specific network. * {@hide} */ final public class DnsEvent { @@ -38,6 +40,8 @@ final public class DnsEvent { // the eventTypes, returnCodes, and latenciesMs arrays have the same length and the i-th event // is spread across the three array at position i. public int eventCount; + // The number of successful DNS queries recorded. + public int successCount; // The types of DNS queries as defined in INetdEventListener. public byte[] eventTypes; // Current getaddrinfo codes go from 1 to EAI_MAX = 15. gethostbyname returns errno, but there @@ -54,10 +58,11 @@ final public class DnsEvent { latenciesMs = new int[initialCapacity]; } - public void addResult(byte eventType, byte returnCode, int latencyMs) { + boolean addResult(byte eventType, byte returnCode, int latencyMs) { + boolean isSuccess = (returnCode == 0); if (eventCount >= SIZE_LIMIT) { // TODO: implement better rate limiting that does not biases metrics. - return; + return isSuccess; } if (eventCount == eventTypes.length) { resize((int) (1.4 * eventCount)); @@ -66,6 +71,10 @@ final public class DnsEvent { returnCodes[eventCount] = returnCode; latenciesMs[eventCount] = latencyMs; eventCount++; + if (isSuccess) { + successCount++; + } + return isSuccess; } public void resize(int newLength) { @@ -80,6 +89,8 @@ final public class DnsEvent { for (int t : BitUtils.unpackBits(transports)) { builder.append(NetworkCapabilities.transportNameOf(t)).append(", "); } - return builder.append(eventCount).append(" events)").toString(); + builder.append(String.format("%d events, ", eventCount)); + builder.append(String.format("%d success)", successCount)); + return builder.toString(); } } diff --git a/android/net/metrics/NetworkMetrics.java b/android/net/metrics/NetworkMetrics.java new file mode 100644 index 00000000..2b662a0c --- /dev/null +++ b/android/net/metrics/NetworkMetrics.java @@ -0,0 +1,168 @@ +/* + * 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.metrics; + +import android.net.NetworkCapabilities; + +import com.android.internal.util.BitUtils; +import com.android.internal.util.TokenBucket; + +import java.util.StringJoiner; + +/** + * A class accumulating network metrics received from Netd regarding dns queries and + * connect() calls on a given network. + * + * This class also accumulates running sums of dns and connect latency stats and + * error counts for bug report logging. + * + * @hide + */ +public class NetworkMetrics { + + private static final int INITIAL_DNS_BATCH_SIZE = 100; + private static final int CONNECT_LATENCY_MAXIMUM_RECORDS = 20000; + + // The network id of the Android Network. + public final int netId; + // The transport types bitmap of the Android Network, as defined in NetworkCapabilities.java. + public final long transports; + // Accumulated metrics for connect events. + public final ConnectStats connectMetrics; + // Accumulated metrics for dns events. + public final DnsEvent dnsMetrics; + // Running sums of latencies and error counts for connect and dns events. + public final Summary summary; + // Running sums of the most recent latencies and error counts for connect and dns events. + // Starts null until some events are accumulated. + // Allows to collect periodic snapshot of the running summaries for a given network. + public Summary pendingSummary; + + public NetworkMetrics(int netId, long transports, TokenBucket tb) { + this.netId = netId; + this.transports = transports; + this.connectMetrics = + new ConnectStats(netId, transports, tb, CONNECT_LATENCY_MAXIMUM_RECORDS); + this.dnsMetrics = new DnsEvent(netId, transports, INITIAL_DNS_BATCH_SIZE); + this.summary = new Summary(netId, transports); + } + + /** + * Get currently pending Summary statistics, if any, for this NetworkMetrics, merge them + * into the long running Summary statistics of this NetworkMetrics, and also clear them. + */ + public Summary getPendingStats() { + Summary s = pendingSummary; + pendingSummary = null; + if (s != null) { + summary.merge(s); + } + return s; + } + + /** Accumulate a dns query result reported by netd. */ + public void addDnsResult(int eventType, int returnCode, int latencyMs) { + if (pendingSummary == null) { + pendingSummary = new Summary(netId, transports); + } + boolean isSuccess = dnsMetrics.addResult((byte) eventType, (byte) returnCode, latencyMs); + pendingSummary.dnsLatencies.count(latencyMs); + pendingSummary.dnsErrorRate.count(isSuccess ? 0 : 1); + } + + /** Accumulate a connect query result reported by netd. */ + public void addConnectResult(int error, int latencyMs, String ipAddr) { + if (pendingSummary == null) { + pendingSummary = new Summary(netId, transports); + } + boolean isSuccess = connectMetrics.addEvent(error, latencyMs, ipAddr); + pendingSummary.connectErrorRate.count(isSuccess ? 0 : 1); + if (ConnectStats.isNonBlocking(error)) { + pendingSummary.connectLatencies.count(latencyMs); + } + } + + /** Represents running sums for dns and connect average error counts and average latencies. */ + public static class Summary { + + public final int netId; + public final long transports; + // DNS latencies measured in milliseconds. + public final Metrics dnsLatencies = new Metrics(); + // DNS error rate measured in percentage points. + public final Metrics dnsErrorRate = new Metrics(); + // Blocking connect latencies measured in milliseconds. + public final Metrics connectLatencies = new Metrics(); + // Blocking and non blocking connect error rate measured in percentage points. + public final Metrics connectErrorRate = new Metrics(); + + public Summary(int netId, long transports) { + this.netId = netId; + this.transports = transports; + } + + void merge(Summary that) { + dnsLatencies.merge(that.dnsLatencies); + dnsErrorRate.merge(that.dnsErrorRate); + connectLatencies.merge(that.connectLatencies); + connectErrorRate.merge(that.connectErrorRate); + } + + @Override + public String toString() { + StringJoiner j = new StringJoiner(", ", "{", "}"); + j.add("netId=" + netId); + for (int t : BitUtils.unpackBits(transports)) { + j.add(NetworkCapabilities.transportNameOf(t)); + } + j.add(String.format("dns avg=%dms max=%dms err=%.1f%% tot=%d", + (int) dnsLatencies.average(), (int) dnsLatencies.max, + 100 * dnsErrorRate.average(), dnsErrorRate.count)); + j.add(String.format("connect avg=%dms max=%dms err=%.1f%% tot=%d", + (int) connectLatencies.average(), (int) connectLatencies.max, + 100 * connectErrorRate.average(), connectErrorRate.count)); + return j.toString(); + } + } + + /** Tracks a running sum and returns the average of a metric. */ + static class Metrics { + public double sum; + public double max = Double.MIN_VALUE; + public int count; + + void merge(Metrics that) { + this.count += that.count; + this.sum += that.sum; + this.max = Math.max(this.max, that.max); + } + + void count(double value) { + count++; + sum += value; + max = Math.max(max, value); + } + + double average() { + double a = sum / (double) count; + if (Double.isNaN(a)) { + a = 0; + } + return a; + } + } +} diff --git a/android/net/netlink/NetlinkSocket.java b/android/net/netlink/NetlinkSocket.java index a9e0cd99..f5f211d8 100644 --- a/android/net/netlink/NetlinkSocket.java +++ b/android/net/netlink/NetlinkSocket.java @@ -96,7 +96,7 @@ public class NetlinkSocket implements Closeable { mDescriptor = Os.socket( OsConstants.AF_NETLINK, OsConstants.SOCK_DGRAM, nlProto); - Libcore.os.setsockoptInt( + Os.setsockoptInt( mDescriptor, OsConstants.SOL_SOCKET, OsConstants.SO_RCVBUF, SOCKET_RECV_BUFSIZE); } diff --git a/android/net/util/SharedLog.java b/android/net/util/SharedLog.java index 343d237f..bbd3d13e 100644 --- a/android/net/util/SharedLog.java +++ b/android/net/util/SharedLog.java @@ -106,6 +106,10 @@ public class SharedLog { record(Category.NONE, msg); } + public void logf(String fmt, Object... args) { + log(String.format(fmt, args)); + } + public void mark(String msg) { record(Category.MARK, msg); } diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java index b08b4b7c..649b0cef 100644 --- a/android/net/wifi/WifiManager.java +++ b/android/net/wifi/WifiManager.java @@ -1029,6 +1029,26 @@ public class WifiManager { } /** + * Return all matching WifiConfigurations for this ScanResult. + * + * An empty list will be returned when no configurations are installed or if no configurations + * match the ScanResult. + * + * @param scanResult scanResult that represents the BSSID + * @return A list of {@link WifiConfiguration} + * @throws UnsupportedOperationException if Passpoint is not enabled on the device. + * @hide + */ + public List<WifiConfiguration> getAllMatchingWifiConfigs(ScanResult scanResult) { + try { + return mService.getAllMatchingWifiConfigs(scanResult); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** * Returns a list of Hotspot 2.0 OSU (Online Sign-Up) providers associated with the given AP. * * An empty list will be returned if no match is found. diff --git a/android/net/wifi/rtt/RangingRequest.java b/android/net/wifi/rtt/RangingRequest.java index 997b6800..a396281f 100644 --- a/android/net/wifi/rtt/RangingRequest.java +++ b/android/net/wifi/rtt/RangingRequest.java @@ -17,13 +17,22 @@ package android.net.wifi.rtt; import android.net.wifi.ScanResult; +import android.net.wifi.aware.AttachCallback; +import android.net.wifi.aware.DiscoverySessionCallback; +import android.net.wifi.aware.IdentityChangedListener; +import android.net.wifi.aware.PeerHandle; +import android.net.wifi.aware.WifiAwareManager; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import libcore.util.HexEncoding; + import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.StringJoiner; /** @@ -33,8 +42,8 @@ import java.util.StringJoiner; * {@link WifiRttManager#startRanging(RangingRequest, RangingResultCallback, Handler)}. * <p> * The ranging request is a batch request - specifying a set of devices (specified using - * {@link RangingRequest.Builder#addAp(ScanResult)} and - * {@link RangingRequest.Builder#addAps(List)}). + * {@link RangingRequest.Builder#addAccessPoint(ScanResult)} and + * {@link RangingRequest.Builder#addAccessPoints(List)}). * * @hide RTT_API */ @@ -44,8 +53,8 @@ public final class RangingRequest implements Parcelable { /** * Returns the maximum number of peers to range which can be specified in a single {@code * RangingRequest}. The limit applies no matter how the peers are added to the request, e.g. - * through {@link RangingRequest.Builder#addAp(ScanResult)} or - * {@link RangingRequest.Builder#addAps(List)}. + * through {@link RangingRequest.Builder#addAccessPoint(ScanResult)} or + * {@link RangingRequest.Builder#addAccessPoints(List)}. * * @return Maximum number of peers. */ @@ -94,11 +103,34 @@ public final class RangingRequest implements Parcelable { } /** @hide */ - public void enforceValidity() { + public void enforceValidity(boolean awareSupported) { if (mRttPeers.size() > MAX_PEERS) { throw new IllegalArgumentException( "Ranging to too many peers requested. Use getMaxPeers() API to get limit."); } + + for (RttPeer peer: mRttPeers) { + if (peer instanceof RttPeerAp) { + RttPeerAp apPeer = (RttPeerAp) peer; + if (apPeer.scanResult == null || apPeer.scanResult.BSSID == null) { + throw new IllegalArgumentException("Invalid AP peer specification"); + } + } else if (peer instanceof RttPeerAware) { + if (!awareSupported) { + throw new IllegalArgumentException( + "Request contains Aware peers - but Aware isn't supported on this " + + "device"); + } + + RttPeerAware awarePeer = (RttPeerAware) peer; + if (awarePeer.peerMacAddress == null && awarePeer.peerHandle == null) { + throw new IllegalArgumentException("Invalid Aware peer specification"); + } + } else { + throw new IllegalArgumentException( + "Request contains unknown peer specification types"); + } + } } /** @@ -116,7 +148,7 @@ public final class RangingRequest implements Parcelable { * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ - public Builder addAp(ScanResult apInfo) { + public Builder addAccessPoint(ScanResult apInfo) { if (apInfo == null) { throw new IllegalArgumentException("Null ScanResult!"); } @@ -133,17 +165,55 @@ public final class RangingRequest implements Parcelable { * @return The builder to facilitate chaining * {@code builder.setXXX(..).setXXX(..)}. */ - public Builder addAps(List<ScanResult> apInfos) { + public Builder addAccessPoints(List<ScanResult> apInfos) { if (apInfos == null) { throw new IllegalArgumentException("Null list of ScanResults!"); } for (ScanResult scanResult : apInfos) { - addAp(scanResult); + addAccessPoint(scanResult); } return this; } /** + * Add the device specified by the {@code peerMacAddress} to the list of devices with + * which to measure range. + * + * The MAC address may be obtained out-of-band from a peer Wi-Fi Aware device. A Wi-Fi + * Aware device may obtain its MAC address using the {@link IdentityChangedListener} + * provided to + * {@link WifiAwareManager#attach(AttachCallback, IdentityChangedListener, Handler)}. + * + * * Note: in order to use this API the device must support Wi-Fi Aware + * {@link android.net.wifi.aware}. + * + * @param peerMacAddress The MAC address of the Wi-Fi Aware peer. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + */ + public Builder addWifiAwarePeer(byte[] peerMacAddress) { + mRttPeers.add(new RttPeerAware(peerMacAddress)); + return this; + } + + /** + * Add a device specified by a {@link PeerHandle} to the list of devices with which to + * measure range. + * + * The {@link PeerHandle} may be obtained as part of the Wi-Fi Aware discovery process. E.g. + * using {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], List)}. + * + * Note: in order to use this API the device must support Wi-Fi Aware + * {@link android.net.wifi.aware}. + * + * @param peerHandle The peer handler of the peer Wi-Fi Aware device. + * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}. + */ + public Builder addWifiAwarePeer(PeerHandle peerHandle) { + mRttPeers.add(new RttPeerAware(peerHandle)); + return this; + } + + /** * Build {@link RangingRequest} given the current configurations made on the * builder. */ @@ -234,4 +304,89 @@ public final class RangingRequest implements Parcelable { return scanResult.hashCode(); } } -}
\ No newline at end of file + + /** @hide */ + public static class RttPeerAware implements RttPeer, Parcelable { + public PeerHandle peerHandle; + public byte[] peerMacAddress; + + public RttPeerAware(PeerHandle peerHandle) { + if (peerHandle == null) { + throw new IllegalArgumentException("Null peerHandle"); + } + this.peerHandle = peerHandle; + peerMacAddress = null; + } + + public RttPeerAware(byte[] peerMacAddress) { + if (peerMacAddress == null) { + throw new IllegalArgumentException("Null peerMacAddress"); + } + + this.peerMacAddress = peerMacAddress; + peerHandle = null; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (peerHandle == null) { + dest.writeBoolean(false); + dest.writeByteArray(peerMacAddress); + } else { + dest.writeBoolean(true); + dest.writeInt(peerHandle.peerId); + } + } + + public static final Creator<RttPeerAware> CREATOR = new Creator<RttPeerAware>() { + @Override + public RttPeerAware[] newArray(int size) { + return new RttPeerAware[size]; + } + + @Override + public RttPeerAware createFromParcel(Parcel in) { + boolean peerHandleAvail = in.readBoolean(); + if (peerHandleAvail) { + return new RttPeerAware(new PeerHandle(in.readInt())); + } else { + return new RttPeerAware(in.createByteArray()); + } + } + }; + + @Override + public String toString() { + return new StringBuilder("RttPeerAware: peerHandle=").append( + peerHandle == null ? "<null>" : Integer.toString(peerHandle.peerId)).append( + ", peerMacAddress=").append(peerMacAddress == null ? "<null>" + : new String(HexEncoding.encode(peerMacAddress))).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof RttPeerAware)) { + return false; + } + + RttPeerAware lhs = (RttPeerAware) o; + + return Objects.equals(peerHandle, lhs.peerHandle) && Arrays.equals(peerMacAddress, + lhs.peerMacAddress); + } + + @Override + public int hashCode() { + return Objects.hash(peerHandle.peerId, peerMacAddress); + } + } +} diff --git a/android/net/wifi/rtt/RangingResult.java b/android/net/wifi/rtt/RangingResult.java index 918803ef..93e52aeb 100644 --- a/android/net/wifi/rtt/RangingResult.java +++ b/android/net/wifi/rtt/RangingResult.java @@ -16,13 +16,16 @@ package android.net.wifi.rtt; +import android.annotation.IntDef; +import android.net.wifi.aware.PeerHandle; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; import libcore.util.HexEncoding; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -40,28 +43,61 @@ import java.util.Objects; public final class RangingResult implements Parcelable { private static final String TAG = "RangingResult"; + /** @hide */ + @IntDef({STATUS_SUCCESS, STATUS_FAIL}) + @Retention(RetentionPolicy.SOURCE) + public @interface RangeResultStatus { + } + + /** + * Individual range request status, {@link #getStatus()}. Indicates ranging operation was + * successful and distance value is valid. + */ + public static final int STATUS_SUCCESS = 0; + + /** + * Individual range request status, {@link #getStatus()}. Indicates ranging operation failed + * and the distance value is invalid. + */ + public static final int STATUS_FAIL = 1; + private final int mStatus; private final byte[] mMac; - private final int mDistanceCm; - private final int mDistanceStdDevCm; + private final PeerHandle mPeerHandle; + private final int mDistanceMm; + private final int mDistanceStdDevMm; private final int mRssi; private final long mTimestamp; /** @hide */ - public RangingResult(int status, byte[] mac, int distanceCm, int distanceStdDevCm, int rssi, - long timestamp) { + public RangingResult(@RangeResultStatus int status, byte[] mac, int distanceMm, + int distanceStdDevMm, int rssi, long timestamp) { mStatus = status; mMac = mac; - mDistanceCm = distanceCm; - mDistanceStdDevCm = distanceStdDevCm; + mPeerHandle = null; + mDistanceMm = distanceMm; + mDistanceStdDevMm = distanceStdDevMm; + mRssi = rssi; + mTimestamp = timestamp; + } + + /** @hide */ + public RangingResult(@RangeResultStatus int status, PeerHandle peerHandle, int distanceMm, + int distanceStdDevMm, int rssi, long timestamp) { + mStatus = status; + mMac = null; + mPeerHandle = peerHandle; + mDistanceMm = distanceMm; + mDistanceStdDevMm = distanceStdDevMm; mRssi = rssi; mTimestamp = timestamp; } /** - * @return The status of ranging measurement: {@link RangingResultCallback#STATUS_SUCCESS} in - * case of success, and {@link RangingResultCallback#STATUS_FAIL} in case of failure. + * @return The status of ranging measurement: {@link #STATUS_SUCCESS} in case of success, and + * {@link #STATUS_FAIL} in case of failure. */ + @RangeResultStatus public int getStatus() { return mStatus; } @@ -70,57 +106,80 @@ public final class RangingResult implements Parcelable { * @return The MAC address of the device whose range measurement was requested. Will correspond * to the MAC address of the device in the {@link RangingRequest}. * <p> - * Always valid (i.e. when {@link #getStatus()} is either SUCCESS or FAIL. + * Will return a {@code null} for results corresponding to requests issued using a {@code + * PeerHandle}, i.e. using the {@link RangingRequest.Builder#addWifiAwarePeer(PeerHandle)} API. */ public byte[] getMacAddress() { return mMac; } /** - * @return The distance (in cm) to the device specified by {@link #getMacAddress()}. + * @return The PeerHandle of the device whose reange measurement was requested. Will correspond + * to the PeerHandle of the devices requested using + * {@link RangingRequest.Builder#addWifiAwarePeer(PeerHandle)}. * <p> - * Only valid if {@link #getStatus()} returns {@link RangingResultCallback#STATUS_SUCCESS}. + * Will return a {@code null} for results corresponding to requests issued using a MAC address. */ - public int getDistanceCm() { - if (mStatus != RangingResultCallback.STATUS_SUCCESS) { - Log.e(TAG, "getDistanceCm(): invalid value retrieved"); + public PeerHandle getPeerHandle() { + return mPeerHandle; + } + + /** + * @return The distance (in mm) to the device specified by {@link #getMacAddress()} or + * {@link #getPeerHandle()}. + * <p> + * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an + * exception. + */ + public int getDistanceMm() { + if (mStatus != STATUS_SUCCESS) { + throw new IllegalStateException( + "getDistanceMm(): invoked on an invalid result: getStatus()=" + mStatus); } - return mDistanceCm; + return mDistanceMm; } /** - * @return The standard deviation of the measured distance (in cm) to the device specified by - * {@link #getMacAddress()}. The standard deviation is calculated over the measurements - * executed in a single RTT burst. + * @return The standard deviation of the measured distance (in mm) to the device specified by + * {@link #getMacAddress()} or {@link #getPeerHandle()}. The standard deviation is calculated + * over the measurements executed in a single RTT burst. * <p> - * Only valid if {@link #getStatus()} returns {@link RangingResultCallback#STATUS_SUCCESS}. + * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an + * exception. */ - public int getDistanceStdDevCm() { - if (mStatus != RangingResultCallback.STATUS_SUCCESS) { - Log.e(TAG, "getDistanceStdDevCm(): invalid value retrieved"); + public int getDistanceStdDevMm() { + if (mStatus != STATUS_SUCCESS) { + throw new IllegalStateException( + "getDistanceStdDevMm(): invoked on an invalid result: getStatus()=" + mStatus); } - return mDistanceStdDevCm; + return mDistanceStdDevMm; } /** * @return The average RSSI (in units of -0.5dB) observed during the RTT measurement. * <p> - * Only valid if {@link #getStatus()} returns {@link RangingResultCallback#STATUS_SUCCESS}. + * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an + * exception. */ public int getRssi() { - if (mStatus != RangingResultCallback.STATUS_SUCCESS) { - // TODO: should this be an exception? - Log.e(TAG, "getRssi(): invalid value retrieved"); + if (mStatus != STATUS_SUCCESS) { + throw new IllegalStateException( + "getRssi(): invoked on an invalid result: getStatus()=" + mStatus); } return mRssi; } /** - * @return The timestamp (in us) at which the ranging operation was performed + * @return The timestamp, in us since boot, at which the ranging operation was performed. * <p> - * Only valid if {@link #getStatus()} returns {@link RangingResultCallback#STATUS_SUCCESS}. + * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an + * exception. */ - public long getRangingTimestamp() { + public long getRangingTimestampUs() { + if (mStatus != STATUS_SUCCESS) { + throw new IllegalStateException( + "getRangingTimestamp(): invoked on an invalid result: getStatus()=" + mStatus); + } return mTimestamp; } @@ -135,8 +194,14 @@ public final class RangingResult implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mStatus); dest.writeByteArray(mMac); - dest.writeInt(mDistanceCm); - dest.writeInt(mDistanceStdDevCm); + if (mPeerHandle == null) { + dest.writeBoolean(false); + } else { + dest.writeBoolean(true); + dest.writeInt(mPeerHandle.peerId); + } + dest.writeInt(mDistanceMm); + dest.writeInt(mDistanceStdDevMm); dest.writeInt(mRssi); dest.writeLong(mTimestamp); } @@ -152,11 +217,22 @@ public final class RangingResult implements Parcelable { public RangingResult createFromParcel(Parcel in) { int status = in.readInt(); byte[] mac = in.createByteArray(); - int distanceCm = in.readInt(); - int distanceStdDevCm = in.readInt(); + boolean peerHandlePresent = in.readBoolean(); + PeerHandle peerHandle = null; + if (peerHandlePresent) { + peerHandle = new PeerHandle(in.readInt()); + } + int distanceMm = in.readInt(); + int distanceStdDevMm = in.readInt(); int rssi = in.readInt(); long timestamp = in.readLong(); - return new RangingResult(status, mac, distanceCm, distanceStdDevCm, rssi, timestamp); + if (peerHandlePresent) { + return new RangingResult(status, peerHandle, distanceMm, distanceStdDevMm, rssi, + timestamp); + } else { + return new RangingResult(status, mac, distanceMm, distanceStdDevMm, rssi, + timestamp); + } } }; @@ -164,9 +240,10 @@ public final class RangingResult implements Parcelable { @Override public String toString() { return new StringBuilder("RangingResult: [status=").append(mStatus).append(", mac=").append( - mMac == null ? "<null>" : HexEncoding.encodeToString(mMac)).append( - ", distanceCm=").append(mDistanceCm).append(", distanceStdDevCm=").append( - mDistanceStdDevCm).append(", rssi=").append(mRssi).append(", timestamp=").append( + mMac == null ? "<null>" : new String(HexEncoding.encodeToString(mMac))).append( + ", peerHandle=").append(mPeerHandle == null ? "<null>" : mPeerHandle.peerId).append( + ", distanceMm=").append(mDistanceMm).append(", distanceStdDevMm=").append( + mDistanceStdDevMm).append(", rssi=").append(mRssi).append(", timestamp=").append( mTimestamp).append("]").toString(); } @@ -182,13 +259,15 @@ public final class RangingResult implements Parcelable { RangingResult lhs = (RangingResult) o; - return mStatus == lhs.mStatus && Arrays.equals(mMac, lhs.mMac) - && mDistanceCm == lhs.mDistanceCm && mDistanceStdDevCm == lhs.mDistanceStdDevCm - && mRssi == lhs.mRssi && mTimestamp == lhs.mTimestamp; + return mStatus == lhs.mStatus && Arrays.equals(mMac, lhs.mMac) && Objects.equals( + mPeerHandle, lhs.mPeerHandle) && mDistanceMm == lhs.mDistanceMm + && mDistanceStdDevMm == lhs.mDistanceStdDevMm && mRssi == lhs.mRssi + && mTimestamp == lhs.mTimestamp; } @Override public int hashCode() { - return Objects.hash(mStatus, mMac, mDistanceCm, mDistanceStdDevCm, mRssi, mTimestamp); + return Objects.hash(mStatus, mMac, mPeerHandle, mDistanceMm, mDistanceStdDevMm, mRssi, + mTimestamp); } -}
\ No newline at end of file +} diff --git a/android/net/wifi/rtt/RangingResultCallback.java b/android/net/wifi/rtt/RangingResultCallback.java index d7270ad2..7405e82e 100644 --- a/android/net/wifi/rtt/RangingResultCallback.java +++ b/android/net/wifi/rtt/RangingResultCallback.java @@ -16,35 +16,43 @@ package android.net.wifi.rtt; +import android.annotation.IntDef; import android.os.Handler; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** * Base class for ranging result callbacks. Should be extended by applications and set when calling - * {@link WifiRttManager#startRanging(RangingRequest, RangingResultCallback, Handler)}. A single - * result from a range request will be called in this object. + * {@link WifiRttManager#startRanging(RangingRequest, RangingResultCallback, Handler)}. If the + * ranging operation fails in whole (not attempted) then {@link #onRangingFailure(int)} will be + * called with a failure code. If the ranging operation is performed for each of the requested + * peers then the {@link #onRangingResults(List)} will be called with the set of results (@link + * {@link RangingResult}, each of which has its own success/failure code + * {@link RangingResult#getStatus()}. * * @hide RTT_API */ public abstract class RangingResultCallback { - /** - * Individual range request status, {@link RangingResult#getStatus()}. Indicates ranging - * operation was successful and distance value is valid. - */ - public static final int STATUS_SUCCESS = 0; + /** @hide */ + @IntDef({STATUS_CODE_FAIL}) + @Retention(RetentionPolicy.SOURCE) + public @interface RangingOperationStatus { + } /** - * Individual range request status, {@link RangingResult#getStatus()}. Indicates ranging - * operation failed and the distance value is invalid. + * A failure code for the whole ranging request operation. Indicates a failure. */ - public static final int STATUS_FAIL = 1; + public static final int STATUS_CODE_FAIL = 1; /** * Called when a ranging operation failed in whole - i.e. no ranging operation to any of the * devices specified in the request was attempted. + * + * @param code A status code indicating the type of failure. */ - public abstract void onRangingFailure(); + public abstract void onRangingFailure(@RangingOperationStatus int code); /** * Called when a ranging operation was executed. The list of results corresponds to devices diff --git a/android/net/wifi/rtt/WifiRttManager.java b/android/net/wifi/rtt/WifiRttManager.java index a085de17..435bb377 100644 --- a/android/net/wifi/rtt/WifiRttManager.java +++ b/android/net/wifi/rtt/WifiRttManager.java @@ -83,17 +83,18 @@ public class WifiRttManager { } @Override - public void onRangingResults(int status, List<RangingResult> results) throws RemoteException { - if (VDBG) { - Log.v(TAG, "RttCallbackProxy: onRanginResults: status=" + status + ", results=" - + results); - } + public void onRangingFailure(int status) throws RemoteException { + if (VDBG) Log.v(TAG, "RttCallbackProxy: onRangingFailure: status=" + status); mHandler.post(() -> { - if (status == RangingResultCallback.STATUS_SUCCESS) { - mCallback.onRangingResults(results); - } else { - mCallback.onRangingFailure(); - } + mCallback.onRangingFailure(status); + }); + } + + @Override + public void onRangingResults(List<RangingResult> results) throws RemoteException { + if (VDBG) Log.v(TAG, "RttCallbackProxy: onRanginResults: results=" + results); + mHandler.post(() -> { + mCallback.onRangingResults(results); }); } } diff --git a/android/os/BatteryProperty.java b/android/os/BatteryProperty.java index 84119bdc..b7e7b177 100644 --- a/android/os/BatteryProperty.java +++ b/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/android/os/BatteryStats.java b/android/os/BatteryStats.java index 98819279..8682c01e 100644 --- a/android/os/BatteryStats.java +++ b/android/os/BatteryStats.java @@ -447,8 +447,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; @@ -456,14 +455,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. @@ -471,7 +470,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; @@ -600,9 +599,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); @@ -1911,6 +1918,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. * @@ -2019,6 +2033,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} @@ -2026,6 +2048,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). * @@ -2108,6 +2136,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; @@ -2267,6 +2300,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. * @@ -2282,6 +2322,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; /** @@ -2301,6 +2348,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. * @@ -2487,13 +2541,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(); @@ -2626,20 +2680,20 @@ 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); /** * Returns the estimated real battery capacity, which may be less than the capacity @@ -2777,6 +2831,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 @@ -2981,10 +3039,9 @@ 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); } } @@ -3000,17 +3057,31 @@ public abstract class BatteryStats implements Parcelable { * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT */ private static void dumpTimer(ProtoOutputStream proto, long fieldId, - Timer timer, long rawRealtime, int which) { + Timer timer, long rawRealtimeUs, int which) { if (timer == null) { return; } // Convert from microseconds to milliseconds with rounding - final long totalTimeMs = (timer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; + final long timeMs = roundUsToMs(timer.getTotalTimeLocked(rawRealtimeUs, which)); final int count = timer.getCountLocked(which); - if (totalTimeMs != 0 || count != 0) { + 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, totalTimeMs); + 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); } } @@ -3114,71 +3185,104 @@ public abstract class BatteryStats implements Parcelable { final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(which); final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(which); final long powerDrainMaMs = counter.getPowerCounter().getCountLocked(which); + // Battery real time + final long totalControllerActivityTimeMs + = computeBatteryRealtime(SystemClock.elapsedRealtime() * 1000, which) / 1000; long totalTxTimeMs = 0; for (LongCounter txState : counter.getTxTimeCounters()) { totalTxTimeMs += txState.getCountLocked(which); } + final long sleepTimeMs + = totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + totalTxTimeMs); - final long totalTimeMs = idleTimeMs + rxTimeMs + totalTxTimeMs; + sb.setLength(0); + sb.append(prefix); + sb.append(" "); + sb.append(controllerName); + sb.append(" Sleep time: "); + formatTimeMs(sb, sleepTimeMs); + sb.append("("); + sb.append(formatRatioLocked(sleepTimeMs, totalControllerActivityTimeMs)); + sb.append(")"); + pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" "); + sb.append(" "); sb.append(controllerName); sb.append(" Idle time: "); formatTimeMs(sb, idleTimeMs); sb.append("("); - sb.append(formatRatioLocked(idleTimeMs, totalTimeMs)); + sb.append(formatRatioLocked(idleTimeMs, totalControllerActivityTimeMs)); sb.append(")"); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" "); + sb.append(" "); sb.append(controllerName); sb.append(" Rx time: "); formatTimeMs(sb, rxTimeMs); sb.append("("); - sb.append(formatRatioLocked(rxTimeMs, totalTimeMs)); + sb.append(formatRatioLocked(rxTimeMs, totalControllerActivityTimeMs)); sb.append(")"); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" "); + sb.append(" "); sb.append(controllerName); sb.append(" Tx time: "); - formatTimeMs(sb, totalTxTimeMs); - sb.append("("); - sb.append(formatRatioLocked(totalTxTimeMs, totalTimeMs)); - sb.append(")"); - pw.println(sb.toString()); - final int numTxLvls = counter.getTxTimeCounters().length; + String [] powerLevel; + switch(controllerName) { + case "Cellular": + powerLevel = new String[] { + " less than 0dBm: ", + " 0dBm to 8dBm: ", + " 8dBm to 15dBm: ", + " 15dBm to 20dBm: ", + " above 20dBm: "}; + break; + default: + powerLevel = new String[] {"[0]", "[1]", "[2]", "[3]", "[4]"}; + break; + } + final int numTxLvls = Math.min(counter.getTxTimeCounters().length, powerLevel.length); if (numTxLvls > 1) { + pw.println(sb.toString()); for (int lvl = 0; lvl < numTxLvls; lvl++) { final long txLvlTimeMs = counter.getTxTimeCounters()[lvl].getCountLocked(which); sb.setLength(0); sb.append(prefix); - sb.append(" ["); - sb.append(lvl); - sb.append("] "); + sb.append(" "); + sb.append(powerLevel[lvl]); + sb.append(" "); formatTimeMs(sb, txLvlTimeMs); sb.append("("); - sb.append(formatRatioLocked(txLvlTimeMs, totalTxTimeMs)); + sb.append(formatRatioLocked(txLvlTimeMs, totalControllerActivityTimeMs)); sb.append(")"); pw.println(sb.toString()); } + } else { + final long txLvlTimeMs = counter.getTxTimeCounters()[0].getCountLocked(which); + formatTimeMs(sb, txLvlTimeMs); + sb.append("("); + sb.append(formatRatioLocked(txLvlTimeMs, totalControllerActivityTimeMs)); + sb.append(")"); + pw.println(sb.toString()); } - sb.setLength(0); - sb.append(prefix); - sb.append(" "); - sb.append(controllerName); - sb.append(" Power drain: ").append( + if (powerDrainMaMs > 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" "); + sb.append(controllerName); + sb.append(" Battery drain: ").append( BatteryStatsHelper.makemAh(powerDrainMaMs / (double) (1000*60*60))); - sb.append("mAh"); - pw.println(sb.toString()); + sb.append("mAh"); + pw.println(sb.toString()); + } } /** @@ -3191,13 +3295,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); @@ -3220,9 +3324,9 @@ 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 StringBuilder sb = new StringBuilder(128); @@ -3460,9 +3564,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: @@ -3503,6 +3607,9 @@ public abstract class BatteryStats implements Parcelable { case CAMERA: label = "camera"; break; + case MEMORY: + label = "memory"; + break; default: label = "???"; } @@ -3523,6 +3630,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) { @@ -3686,7 +3794,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) { @@ -4020,7 +4128,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); @@ -4030,7 +4138,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); @@ -4040,7 +4148,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); @@ -4246,51 +4354,50 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } + pw.println(""); pw.print(prefix); - pw.print(" Mobile total received: "); pw.print(formatBytesLocked(mobileRxTotalBytes)); - pw.print(", sent: "); pw.print(formatBytesLocked(mobileTxTotalBytes)); - pw.print(" (packets received "); pw.print(mobileRxTotalPackets); - pw.print(", sent "); pw.print(mobileTxTotalPackets); pw.println(")"); sb.setLength(0); sb.append(prefix); - sb.append(" Phone signal levels:"); - didOne = false; - for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { - final long time = getPhoneSignalStrengthTime(i, rawRealtime, which); - if (time == 0) { - continue; - } - sb.append("\n "); - sb.append(prefix); - didOne = true; - sb.append(SignalStrength.SIGNAL_STRENGTH_NAMES[i]); - sb.append(" "); - formatTimeMs(sb, time/1000); - sb.append("("); - sb.append(formatRatioLocked(time, whichBatteryRealtime)); - sb.append(") "); - sb.append(getPhoneSignalStrengthCount(i, which)); - sb.append("x"); - } - if (!didOne) sb.append(" (no activity)"); + sb.append(" CONNECTIVITY POWER SUMMARY START"); pw.println(sb.toString()); + pw.print(prefix); sb.setLength(0); sb.append(prefix); - sb.append(" Signal scanning time: "); - formatTimeMsNoSpace(sb, getPhoneSignalScanningTime(rawRealtime, which) / 1000); + sb.append(" Logging duration for connectivity statistics: "); + formatTimeMs(sb, whichBatteryRealtime / 1000); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" Radio types:"); + sb.append(" Cellular Statistics:"); + pw.println(sb.toString()); + + pw.print(prefix); + sb.setLength(0); + sb.append(prefix); + sb.append(" Cellular kernel active time: "); + final long mobileActiveTime = getMobileRadioActiveTime(rawRealtime, which); + formatTimeMs(sb, mobileActiveTime / 1000); + sb.append("("); sb.append(formatRatioLocked(mobileActiveTime, whichBatteryRealtime)); + sb.append(")"); + pw.println(sb.toString()); + + pw.print(" Cellular data received: "); pw.println(formatBytesLocked(mobileRxTotalBytes)); + pw.print(" Cellular data sent: "); pw.println(formatBytesLocked(mobileTxTotalBytes)); + pw.print(" Cellular packets received: "); pw.println(mobileRxTotalPackets); + pw.print(" Cellular packets sent: "); pw.println(mobileTxTotalPackets); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Cellular Radio Access Technology:"); didOne = false; for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { final long time = getPhoneDataConnectionTime(i, rawRealtime, which); if (time == 0) { continue; } - sb.append("\n "); + sb.append("\n "); sb.append(prefix); didOne = true; sb.append(DATA_CONNECTION_NAMES[i]); @@ -4299,73 +4406,64 @@ public abstract class BatteryStats implements Parcelable { sb.append("("); sb.append(formatRatioLocked(time, whichBatteryRealtime)); sb.append(") "); - sb.append(getPhoneDataConnectionCount(i, which)); - sb.append("x"); } if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" Mobile radio active time: "); - final long mobileActiveTime = getMobileRadioActiveTime(rawRealtime, which); - formatTimeMs(sb, mobileActiveTime / 1000); - sb.append("("); sb.append(formatRatioLocked(mobileActiveTime, whichBatteryRealtime)); - sb.append(") "); sb.append(getMobileRadioActiveCount(which)); - sb.append("x"); - pw.println(sb.toString()); - - final long mobileActiveUnknownTime = getMobileRadioActiveUnknownTime(which); - if (mobileActiveUnknownTime != 0) { - sb.setLength(0); - sb.append(prefix); - sb.append(" Mobile radio active unknown time: "); - formatTimeMs(sb, mobileActiveUnknownTime / 1000); - sb.append("("); - sb.append(formatRatioLocked(mobileActiveUnknownTime, whichBatteryRealtime)); - sb.append(") "); sb.append(getMobileRadioActiveUnknownCount(which)); - sb.append("x"); - pw.println(sb.toString()); - } - - final long mobileActiveAdjustedTime = getMobileRadioActiveAdjustedTime(which); - if (mobileActiveAdjustedTime != 0) { - sb.setLength(0); + sb.append(" Cellular Rx signal strength (RSRP):"); + final String[] cellularRxSignalStrengthDescription = new String[]{ + "very poor (less than -128dBm): ", + "poor (-128dBm to -118dBm): ", + "moderate (-118dBm to -108dBm): ", + "good (-108dBm to -98dBm): ", + "great (greater than -98dBm): "}; + didOne = false; + final int numCellularRxBins = Math.min(SignalStrength.NUM_SIGNAL_STRENGTH_BINS, + cellularRxSignalStrengthDescription.length); + for (int i=0; i<numCellularRxBins; i++) { + final long time = getPhoneSignalStrengthTime(i, rawRealtime, which); + if (time == 0) { + continue; + } + sb.append("\n "); sb.append(prefix); - sb.append(" Mobile radio active adjusted time: "); - formatTimeMs(sb, mobileActiveAdjustedTime / 1000); + didOne = true; + sb.append(cellularRxSignalStrengthDescription[i]); + sb.append(" "); + formatTimeMs(sb, time/1000); sb.append("("); - sb.append(formatRatioLocked(mobileActiveAdjustedTime, whichBatteryRealtime)); - sb.append(")"); - pw.println(sb.toString()); + sb.append(formatRatioLocked(time, whichBatteryRealtime)); + sb.append(") "); } + if (!didOne) sb.append(" (no activity)"); + pw.println(sb.toString()); - printControllerActivity(pw, sb, prefix, "Radio", getModemControllerActivity(), which); + printControllerActivity(pw, sb, prefix, "Cellular", + getModemControllerActivity(), which); pw.print(prefix); - pw.print(" Wi-Fi total received: "); pw.print(formatBytesLocked(wifiRxTotalBytes)); - pw.print(", sent: "); pw.print(formatBytesLocked(wifiTxTotalBytes)); - pw.print(" (packets received "); pw.print(wifiRxTotalPackets); - pw.print(", sent "); pw.print(wifiTxTotalPackets); pw.println(")"); sb.setLength(0); sb.append(prefix); - sb.append(" Wifi on: "); formatTimeMs(sb, wifiOnTime / 1000); - sb.append("("); sb.append(formatRatioLocked(wifiOnTime, whichBatteryRealtime)); - sb.append("), Wifi running: "); formatTimeMs(sb, wifiRunningTime / 1000); - sb.append("("); sb.append(formatRatioLocked(wifiRunningTime, whichBatteryRealtime)); - sb.append(")"); + sb.append(" Wifi Statistics:"); pw.println(sb.toString()); + pw.print(" Wifi data received: "); pw.println(formatBytesLocked(wifiRxTotalBytes)); + pw.print(" Wifi data sent: "); pw.println(formatBytesLocked(wifiTxTotalBytes)); + pw.print(" Wifi packets received: "); pw.println(wifiRxTotalPackets); + pw.print(" Wifi packets sent: "); pw.println(wifiTxTotalPackets); + sb.setLength(0); sb.append(prefix); - sb.append(" Wifi states:"); + sb.append(" Wifi states:"); didOne = false; for (int i=0; i<NUM_WIFI_STATES; i++) { final long time = getWifiStateTime(i, rawRealtime, which); if (time == 0) { continue; } - sb.append("\n "); + sb.append("\n "); didOne = true; sb.append(WIFI_STATE_NAMES[i]); sb.append(" "); @@ -4373,22 +4471,20 @@ public abstract class BatteryStats implements Parcelable { sb.append("("); sb.append(formatRatioLocked(time, whichBatteryRealtime)); sb.append(") "); - sb.append(getWifiStateCount(i, which)); - sb.append("x"); } if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" Wifi supplicant states:"); + sb.append(" Wifi supplicant states:"); didOne = false; for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) { final long time = getWifiSupplStateTime(i, rawRealtime, which); if (time == 0) { continue; } - sb.append("\n "); + sb.append("\n "); didOne = true; sb.append(WIFI_SUPPL_STATE_NAMES[i]); sb.append(" "); @@ -4396,17 +4492,23 @@ public abstract class BatteryStats implements Parcelable { sb.append("("); sb.append(formatRatioLocked(time, whichBatteryRealtime)); sb.append(") "); - sb.append(getWifiSupplStateCount(i, which)); - sb.append("x"); } if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" Wifi signal levels:"); + sb.append(" Wifi Rx signal strength (RSSI):"); + final String[] wifiRxSignalStrengthDescription = new String[]{ + "very poor (less than -88.75dBm): ", + "poor (-88.75 to -77.5dBm): ", + "moderate (-77.5dBm to -66.25dBm): ", + "good (-66.25dBm to -55dBm): ", + "great (greater than -55dBm): "}; didOne = false; - for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) { + final int numWifiRxBins = Math.min(NUM_WIFI_SIGNAL_STRENGTH_BINS, + wifiRxSignalStrengthDescription.length); + for (int i=0; i<numWifiRxBins; i++) { final long time = getWifiSignalStrengthTime(i, rawRealtime, which); if (time == 0) { continue; @@ -4414,15 +4516,12 @@ public abstract class BatteryStats implements Parcelable { sb.append("\n "); sb.append(prefix); didOne = true; - sb.append("level("); - sb.append(i); - sb.append(") "); + sb.append(" "); + sb.append(wifiRxSignalStrengthDescription[i]); formatTimeMs(sb, time/1000); sb.append("("); sb.append(formatRatioLocked(time, whichBatteryRealtime)); sb.append(") "); - sb.append(getWifiSignalStrengthCount(i, which)); - sb.append("x"); } if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); @@ -4430,6 +4529,13 @@ public abstract class BatteryStats implements Parcelable { printControllerActivity(pw, sb, prefix, "WiFi", getWifiControllerActivity(), which); pw.print(prefix); + sb.setLength(0); + sb.append(prefix); + sb.append(" CONNECTIVITY POWER SUMMARY END"); + pw.println(sb.toString()); + pw.println(""); + + pw.print(prefix); pw.print(" Bluetooth total received: "); pw.print(formatBytesLocked(btRxTotalBytes)); pw.print(", sent: "); pw.println(formatBytesLocked(btTxTotalBytes)); @@ -6038,6 +6144,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; @@ -6463,7 +6623,7 @@ public abstract class BatteryStats implements Parcelable { } } - /** Dump batterystats data to a proto. @hide */ + /** Dump #STATS_SINCE_CHARGED batterystats data to a proto. @hide */ public void dumpProtoLocked(Context context, FileDescriptor fd, List<ApplicationInfo> apps, int flags, long historyStart) { final ProtoOutputStream proto = new ProtoOutputStream(fd); @@ -6484,11 +6644,802 @@ public abstract class BatteryStats implements Parcelable { } if ((flags & (DUMP_HISTORY_ONLY | DUMP_DAILY_ONLY)) == 0) { - // TODO: implement dumpProtoAppsLocked(proto, apps); - // TODO: implement dumpProtoSystemLocked(proto); + 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); + } + + // 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.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); + + // 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/android/os/Debug.java b/android/os/Debug.java index b46c6b16..017c2134 100644 --- a/android/os/Debug.java +++ b/android/os/Debug.java @@ -1748,22 +1748,26 @@ public final class Debug public static final int MEMINFO_SHMEM = 4; /** @hide */ public static final int MEMINFO_SLAB = 5; + /** @hide */ + public static final int MEMINFO_SLAB_RECLAIMABLE = 6; + /** @hide */ + public static final int MEMINFO_SLAB_UNRECLAIMABLE = 7; /** @hide */ - public static final int MEMINFO_SWAP_TOTAL = 6; + public static final int MEMINFO_SWAP_TOTAL = 8; /** @hide */ - public static final int MEMINFO_SWAP_FREE = 7; + public static final int MEMINFO_SWAP_FREE = 9; /** @hide */ - public static final int MEMINFO_ZRAM_TOTAL = 8; + public static final int MEMINFO_ZRAM_TOTAL = 10; /** @hide */ - public static final int MEMINFO_MAPPED = 9; + public static final int MEMINFO_MAPPED = 11; /** @hide */ - public static final int MEMINFO_VM_ALLOC_USED = 10; + public static final int MEMINFO_VM_ALLOC_USED = 12; /** @hide */ - public static final int MEMINFO_PAGE_TABLES = 11; + public static final int MEMINFO_PAGE_TABLES = 13; /** @hide */ - public static final int MEMINFO_KERNEL_STACK = 12; + public static final int MEMINFO_KERNEL_STACK = 14; /** @hide */ - public static final int MEMINFO_COUNT = 13; + public static final int MEMINFO_COUNT = 15; /** * Retrieves /proc/meminfo. outSizes is filled with fields diff --git a/android/os/ParcelFileDescriptor.java b/android/os/ParcelFileDescriptor.java index c091420a..7f588adb 100644 --- a/android/os/ParcelFileDescriptor.java +++ b/android/os/ParcelFileDescriptor.java @@ -737,7 +737,9 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { private void closeWithStatus(int status, String msg) { if (mClosed) return; mClosed = true; - mGuard.close(); + if (mGuard != null) { + mGuard.close(); + } // Status MUST be sent before closing actual descriptor writeCommStatusAndClose(status, msg); IoUtils.closeQuietly(mFd); diff --git a/android/os/PatternMatcher.java b/android/os/PatternMatcher.java index 1f3a1e68..76b21426 100644 --- a/android/os/PatternMatcher.java +++ b/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/android/os/ServiceManager.java b/android/os/ServiceManager.java index f41848fa..34c78455 100644 --- a/android/os/ServiceManager.java +++ b/android/os/ServiceManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2007 The Android Open Source Project + * Copyright (C) 2009 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. @@ -16,29 +16,9 @@ package android.os; -import android.util.Log; - -import com.android.internal.os.BinderInternal; - -import java.util.HashMap; 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>(); - - private static IServiceManager getIServiceManager() { - if (sServiceManager != null) { - return sServiceManager; - } - - // Find the service manager - sServiceManager = ServiceManagerNative - .asInterface(Binder.allowBlocking(BinderInternal.getContextObject())); - return sServiceManager; - } /** * Returns a reference to a service with the given name. @@ -47,32 +27,14 @@ public final class ServiceManager { * @return a reference to the service, or <code>null</code> if the service doesn't exist */ public static IBinder getService(String name) { - try { - IBinder service = sCache.get(name); - if (service != null) { - return service; - } else { - return Binder.allowBlocking(getIServiceManager().getService(name)); - } - } catch (RemoteException e) { - Log.e(TAG, "error in getService", e); - } return null; } /** - * Returns a reference to a service with the given name, or throws - * {@link NullPointerException} if none is found. - * - * @hide + * Is not supposed to return null, but that is fine for layoutlib. */ public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException { - final IBinder binder = getService(name); - if (binder != null) { - return binder; - } else { - throw new ServiceNotFoundException(name); - } + throw new ServiceNotFoundException(name); } /** @@ -83,39 +45,7 @@ public final class ServiceManager { * @param service the service object */ public static void addService(String name, IBinder service) { - addService(name, service, false, IServiceManager.DUMP_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_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, dumpPriority); - } catch (RemoteException e) { - Log.e(TAG, "error in addService", e); - } + // pass } /** @@ -123,17 +53,7 @@ public final class ServiceManager { * service manager. Non-blocking. */ public static IBinder checkService(String name) { - try { - IBinder service = sCache.get(name); - if (service != null) { - return service; - } else { - return Binder.allowBlocking(getIServiceManager().checkService(name)); - } - } catch (RemoteException e) { - Log.e(TAG, "error in checkService", e); - return null; - } + return null; } /** @@ -142,12 +62,9 @@ public final class ServiceManager { * case of an exception */ public static String[] listServices() { - try { - return getIServiceManager().listServices(IServiceManager.DUMP_PRIORITY_ALL); - } catch (RemoteException e) { - Log.e(TAG, "error in listServices", e); - return null; - } + // actual implementation returns null sometimes, so it's ok + // to return null instead of an empty list. + return null; } /** @@ -159,10 +76,7 @@ public final class ServiceManager { * @hide */ public static void initServiceCache(Map<String, IBinder> cache) { - if (sCache.size() != 0) { - throw new IllegalStateException("setServiceCache may only be called once"); - } - sCache.putAll(cache); + // pass } /** @@ -173,6 +87,7 @@ public final class ServiceManager { * @hide */ public static class ServiceNotFoundException extends Exception { + // identical to the original implementation public ServiceNotFoundException(String name) { super("No service published for: " + name); } diff --git a/android/os/SomeService.java b/android/os/SomeService.java new file mode 100644 index 00000000..bdfaa444 --- /dev/null +++ b/android/os/SomeService.java @@ -0,0 +1,42 @@ +package android.os; + +import android.app.Service; +import android.content.Intent; +import java.io.File; +import java.io.IOException; + +/** Service in separate process available for calling over binder. */ +public class SomeService extends Service { + + private File mTempFile; + + @Override + public void onCreate() { + super.onCreate(); + try { + mTempFile = File.createTempFile("foo", "bar"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private final ISomeService.Stub mBinder = + new ISomeService.Stub() { + public void readDisk(int times) { + for (int i = 0; i < times; i++) { + mTempFile.exists(); + } + } + }; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mTempFile.delete(); + } +} diff --git a/android/os/StatsLogEventWrapper.java b/android/os/StatsLogEventWrapper.java new file mode 100644 index 00000000..9491bec6 --- /dev/null +++ b/android/os/StatsLogEventWrapper.java @@ -0,0 +1,141 @@ +/* + * 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; + + /** + * 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. + write4Bytes(tag); + mStorage.write(EVENT_TYPE_LIST); // This is required to start the log entry. + mStorage.write(fields); // Indicate number of elements in this list. + } + + /** + * 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/android/os/StrictModeTest.java b/android/os/StrictModeTest.java new file mode 100644 index 00000000..d973c204 --- /dev/null +++ b/android/os/StrictModeTest.java @@ -0,0 +1,127 @@ +/* + * 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.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.runner.AndroidJUnit4; +import com.google.common.util.concurrent.SettableFuture; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class StrictModeTest { + @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Test + public void timeVmViolation() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); + causeVmViolations(state); + } + + @Test + public void timeVmViolationNoStrictMode() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + causeVmViolations(state); + } + + private static void causeVmViolations(BenchmarkState state) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setDataAndType(Uri.parse("content://com.example/foobar"), "image/jpeg"); + final Context context = InstrumentationRegistry.getTargetContext(); + while (state.keepRunning()) { + context.startActivity(intent); + } + } + + @Test + public void timeThreadViolation() throws IOException { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); + causeThreadViolations(state); + } + + @Test + public void timeThreadViolationNoStrictMode() throws IOException { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + causeThreadViolations(state); + } + + private static void causeThreadViolations(BenchmarkState state) throws IOException { + final File test = File.createTempFile("foo", "bar"); + while (state.keepRunning()) { + test.exists(); + } + test.delete(); + } + + @Test + public void timeCrossBinderThreadViolation() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); + causeCrossProcessThreadViolations(state); + } + + @Test + public void timeCrossBinderThreadViolationNoStrictMode() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + causeCrossProcessThreadViolations(state); + } + + private static void causeCrossProcessThreadViolations(BenchmarkState state) + throws ExecutionException, InterruptedException, RemoteException { + final Context context = InstrumentationRegistry.getTargetContext(); + + SettableFuture<IBinder> binder = SettableFuture.create(); + ServiceConnection connection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + binder.set(service); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + binder.set(null); + } + }; + context.bindService( + new Intent(context, SomeService.class), connection, Context.BIND_AUTO_CREATE); + ISomeService someService = ISomeService.Stub.asInterface(binder.get()); + while (state.keepRunning()) { + // Violate strictmode heavily. + someService.readDisk(10); + } + context.unbindService(connection); + } +} diff --git a/android/os/SystemProperties.java b/android/os/SystemProperties.java index 560b4b31..4f6d322b 100644 --- a/android/os/SystemProperties.java +++ b/android/os/SystemProperties.java @@ -84,9 +84,6 @@ public class SystemProperties { /** * Get the String value for the given {@code key}. * - * <b>WARNING:</b> Do not use this method if the value may not be a valid UTF string! This - * method will crash in native code. - * * @param key the key to lookup * @return an empty string if the {@code key} isn't found */ @@ -99,9 +96,6 @@ public class SystemProperties { /** * Get the String value for the given {@code key}. * - * <b>WARNING:</b> Do not use this method if the value may not be a valid UTF string! This - * method will crash in native code. - * * @param key the key to lookup * @param def the default value in case the property is not set or empty * @return if the {@code key} isn't found, return {@code def} if it isn't null, or an empty @@ -163,7 +157,7 @@ public class SystemProperties { * @throws IllegalArgumentException if the {@code val} exceeds 91 characters */ public static void set(@NonNull String key, @Nullable String val) { - if (val != null && val.length() > PROP_VALUE_MAX) { + if (val != null && !val.startsWith("ro.") && val.length() > PROP_VALUE_MAX) { throw new IllegalArgumentException("value of system property '" + key + "' is longer than " + PROP_VALUE_MAX + " characters: " + val); } diff --git a/android/os/UserManager.java b/android/os/UserManager.java index 430a5e3e..8c688713 100644 --- a/android/os/UserManager.java +++ b/android/os/UserManager.java @@ -574,6 +574,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. diff --git a/android/preference/SeekBarVolumizer.java b/android/preference/SeekBarVolumizer.java index ee8eed19..3d2e1d1f 100644 --- a/android/preference/SeekBarVolumizer.java +++ b/android/preference/SeekBarVolumizer.java @@ -206,8 +206,7 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba try { mRingtone.setAudioAttributes(new AudioAttributes.Builder(mRingtone .getAudioAttributes()) - .setFlags(AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY | - AudioAttributes.FLAG_BYPASS_MUTE) + .setFlags(AudioAttributes.FLAG_BYPASS_MUTE) .build()); mRingtone.play(); } catch (Throwable e) { diff --git a/android/provider/Settings.java b/android/provider/Settings.java index a062db43..a27df3a7 100644 --- a/android/provider/Settings.java +++ b/android/provider/Settings.java @@ -442,6 +442,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 @@ -5708,6 +5720,7 @@ public final class Settings { * * @hide */ + @TestApi public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED = "accessibility_display_magnification_enabled"; @@ -6792,14 +6805,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 ':'. @@ -7610,6 +7615,13 @@ public final class Settings { public static final String AIRPLANE_MODE_TOGGLEABLE_RADIOS = "airplane_mode_toggleable_radios"; /** + * An integer representing the Bluetooth Class of Device (CoD). + * + * @hide + */ + public static final String BLUETOOTH_CLASS_OF_DEVICE = "bluetooth_class_of_device"; + + /** * A Long representing a bitmap of profiles that should be disabled when bluetooth starts. * See {@link android.bluetooth.BluetoothProfile}. * {@hide} @@ -9261,6 +9273,13 @@ public final class Settings { */ public static final String DEFAULT_DNS_SERVER = "default_dns_server"; + /** + * Whether to disable DNS over TLS (boolean) + * + * @hide + */ + public static final String DNS_TLS_DISABLED = "dns_tls_disabled"; + /** {@hide} */ public static final String BLUETOOTH_HEADSET_PRIORITY_PREFIX = "bluetooth_headset_priority_"; @@ -9575,6 +9594,22 @@ public final class Settings { public static final String DEVICE_POLICY_CONSTANTS = "device_policy_constants"; /** + * TextClassifier specific settings. + * This is encoded as a key=value list, separated by commas. Ex: + * + * <pre> + * smart_selection_dark_launch (boolean) + * smart_selection_enabled_for_edit_text (boolean) + * </pre> + * + * <p> + * Type: string + * @hide + * see also android.view.textclassifier.TextClassifierConstants + */ + public static final String TEXT_CLASSIFIER_CONSTANTS = "text_classifier_constants"; + + /** * Get the key that retrieves a bluetooth headset's priority. * @hide */ @@ -9621,7 +9656,7 @@ public final class Settings { * Get the key that retrieves a bluetooth Input Device's priority. * @hide */ - public static final String getBluetoothInputDevicePriorityKey(String address) { + public static final String getBluetoothHidHostPriorityKey(String address) { return BLUETOOTH_INPUT_DEVICE_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT); } diff --git a/android/security/net/config/ManifestConfigSource.java b/android/security/net/config/ManifestConfigSource.java index 8fcd5ab5..79115a5a 100644 --- a/android/security/net/config/ManifestConfigSource.java +++ b/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/android/security/net/config/NetworkSecurityConfig.java b/android/security/net/config/NetworkSecurityConfig.java index 789fc273..b9e55054 100644 --- a/android/security/net/config/NetworkSecurityConfig.java +++ b/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 */ @@ -170,22 +170,24 @@ public final class NetworkSecurityConfig { * <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.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/android/security/net/config/XmlConfigSource.java b/android/security/net/config/XmlConfigSource.java index a111fbce..02be403a 100644 --- a/android/security/net/config/XmlConfigSource.java +++ b/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/android/service/autofill/AutofillService.java b/android/service/autofill/AutofillService.java index 2e59f6c5..953501c7 100644 --- a/android/service/autofill/AutofillService.java +++ b/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> @@ -365,6 +365,81 @@ import com.android.internal.os.SomeArgs; * <p><b>Note:</b> The autofill service could also whitelist well-known browser apps and skip the * verifications above, as long as the service can verify the authenticity of the browser app by * checking its signing certificate. + * + * <a name="MultipleStepsSave"></a> + * <h3>Saving when data is split in multiple screens</h3> + * + * Apps often split the user data in multiple screens in the same activity, specially in + * activities used to create a new user account. For example, the first screen asks for a username, + * and if the username is available, it moves to a second screen, which asks for a password. + * + * <p>It's tricky to handle save for autofill in these situations, because the autofill service must + * wait until the user enters both fields before the autofill save UI can be shown. But it can be + * done by following the steps below: + * + * <ol> + * <li>In the first + * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback) fill request}, the service + * adds a {@link FillResponse.Builder#setClientState(android.os.Bundle) client state bundle} in + * the response, containing the autofill ids of the partial fields present in the screen. + * <li>In the second + * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback) fill request}, the service + * retrieves the {@link FillRequest#getClientState() client state bundle}, gets the autofill ids + * set in the previous request from the client state, and adds these ids and the + * {@link SaveInfo#FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} to the {@link SaveInfo} used in the second + * response. + * <li>In the {@link #onSaveRequest(SaveRequest, SaveCallback) save request}, the service uses the + * proper {@link FillContext fill contexts} to get the value of each field (there is one fill + * context per fill request). + * </ol> + * + * <p>For example, in an app that uses 2 steps for the username and password fields, the workflow + * would be: + * <pre class="prettyprint"> + * // On first fill request + * AutofillId usernameId = // parse from AssistStructure; + * Bundle clientState = new Bundle(); + * clientState.putParcelable("usernameId", usernameId); + * fillCallback.onSuccess( + * new FillResponse.Builder() + * .setClientState(clientState) + * .setSaveInfo(new SaveInfo + * .Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME, new AutofillId[] {usernameId}) + * .build()) + * .build()); + * + * // On second fill request + * Bundle clientState = fillRequest.getClientState(); + * AutofillId usernameId = clientState.getParcelable("usernameId"); + * AutofillId passwordId = // parse from AssistStructure + * clientState.putParcelable("passwordId", passwordId); + * fillCallback.onSuccess( + * new FillResponse.Builder() + * .setClientState(clientState) + * .setSaveInfo(new SaveInfo + * .Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD, + * new AutofillId[] {usernameId, passwordId}) + * .setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) + * .build()) + * .build()); + * + * // On save request + * Bundle clientState = saveRequest.getClientState(); + * AutofillId usernameId = clientState.getParcelable("usernameId"); + * AutofillId passwordId = clientState.getParcelable("passwordId"); + * List<FillContext> fillContexts = saveRequest.getFillContexts(); + * + * FillContext usernameContext = fillContexts.get(0); + * ViewNode usernameNode = findNodeByAutofillId(usernameContext.getStructure(), usernameId); + * AutofillValue username = usernameNode.getAutofillValue().getTextValue().toString(); + * + * FillContext passwordContext = fillContexts.get(1); + * ViewNode passwordNode = findNodeByAutofillId(passwordContext.getStructure(), passwordId); + * AutofillValue password = passwordNode.getAutofillValue().getTextValue().toString(); + * + * save(username, password); + * + * </pre> */ public abstract class AutofillService extends Service { private static final String TAG = "AutofillService"; diff --git a/android/service/autofill/BatchUpdates.java b/android/service/autofill/BatchUpdates.java new file mode 100644 index 00000000..90acc881 --- /dev/null +++ b/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/android/service/autofill/CustomDescription.java b/android/service/autofill/CustomDescription.java index 9a4cbc41..fd30857d 100644 --- a/android/service/autofill/CustomDescription.java +++ b/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; @@ -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. @@ -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/android/service/autofill/Dataset.java b/android/service/autofill/Dataset.java index ef9598aa..331130e7 100644 --- a/android/service/autofill/Dataset.java +++ b/android/service/autofill/Dataset.java @@ -150,8 +150,16 @@ 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()) @@ -201,7 +209,8 @@ 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() { } @@ -280,19 +289,24 @@ 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. + * * @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 authentication and you have no access to the value. * @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) { throwIfDestroyed(); - Preconditions.checkState(mPresentation != null, - "Dataset presentation not set on constructor"); setLifeTheUniverseAndEverything(id, value, null, null); return this; } @@ -334,7 +348,7 @@ public final class Dataset implements Parcelable { * * @return this builder. * @throws IllegalStateException if the builder was constructed without a - * {@link RemoteViews presentation}. + * {@link RemoteViews presentation}. */ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @NonNull Pattern filter) { diff --git a/android/service/autofill/FillEventHistory.java b/android/service/autofill/FillEventHistory.java index 3b719ac7..b1857b30 100644 --- a/android/service/autofill/FillEventHistory.java +++ b/android/service/autofill/FillEventHistory.java @@ -17,19 +17,27 @@ package android.service.autofill; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.IntentSender; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.ArraySet; +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.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; /** * Describes what happened after the last @@ -121,6 +129,11 @@ public final class FillEventHistory implements Parcelable { } @Override + public String toString() { + return mEvents == null ? "no events" : mEvents.toString(); + } + + @Override public int describeContents() { return 0; } @@ -136,9 +149,21 @@ public final class FillEventHistory implements Parcelable { int numEvents = mEvents.size(); for (int i = 0; i < numEvents; i++) { Event event = mEvents.get(i); - dest.writeInt(event.getType()); - dest.writeString(event.getDatasetId()); - dest.writeBundle(event.getClientState()); + dest.writeInt(event.mEventType); + dest.writeString(event.mDatasetId); + dest.writeBundle(event.mClientState); + dest.writeStringList(event.mSelectedDatasetIds); + dest.writeArraySet(event.mIgnoredDatasetIds); + dest.writeTypedList(event.mChangedFieldIds); + dest.writeStringList(event.mChangedDatasetIds); + + dest.writeTypedList(event.mManuallyFilledFieldIds); + if (event.mManuallyFilledFieldIds != null) { + final int size = event.mManuallyFilledFieldIds.size(); + for (int j = 0; j < size; j++) { + dest.writeStringList(event.mManuallyFilledDatasetIds.get(j)); + } + } } } } @@ -176,12 +201,40 @@ 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. + */ + 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{} @@ -189,6 +242,17 @@ public final class FillEventHistory implements Parcelable { @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; + /** * Returns the type of the event. * @@ -220,25 +284,202 @@ public final class FillEventHistory implements Parcelable { } /** + * 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; + } + + /** + * 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, @Nullable String datasetId, @Nullable Bundle clientState) { - mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_SAVE_SHOWN, + 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) { + 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; } @Override public String toString() { - return "FillEvent [datasetId=" + mDatasetId + ", type=" + mEventType + "]"; + return "FillEvent [datasetId=" + mDatasetId + + ", type=" + mEventType + + ", selectedDatasets=" + mSelectedDatasetIds + + ", ignoredDatasetIds=" + mIgnoredDatasetIds + + ", changedFieldIds=" + mChangedFieldIds + + ", changedDatasetsIds=" + mChangedDatasetIds + + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds + + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds + + "]"; } } @@ -248,12 +489,37 @@ public final class FillEventHistory implements Parcelable { public FillEventHistory createFromParcel(Parcel parcel) { FillEventHistory selection = new FillEventHistory(0, 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(), - parcel.readBundle())); - } + 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; + } + selection.addEvent(new Event(eventType, datasetId, clientState, + selectedDatasetIds, ignoredDatasets, + changedFieldIds, changedDatasetIds, + manuallyFilledFieldIds, manuallyFilledDatasetIds)); + } return selection; } diff --git a/android/service/autofill/FillResponse.java b/android/service/autofill/FillResponse.java index 6d8a9599..d2033fa9 100644 --- a/android/service/autofill/FillResponse.java +++ b/android/service/autofill/FillResponse.java @@ -19,6 +19,7 @@ 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.app.Activity; @@ -30,6 +31,8 @@ import android.os.Parcelable; import android.view.autofill.AutofillId; import android.widget.RemoteViews; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -42,6 +45,19 @@ import java.util.List; */ public final class FillResponse implements Parcelable { + /** + * Must be set in the last response to generate + * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED} events. + */ + public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1; + + /** @hide */ + @IntDef(flag = true, value = { + FLAG_TRACK_CONTEXT_COMMITED + }) + @Retention(RetentionPolicy.SOURCE) + @interface FillResponseFlags {} + private final @Nullable ParceledListSlice<Dataset> mDatasets; private final @Nullable SaveInfo mSaveInfo; private final @Nullable Bundle mClientState; @@ -49,16 +65,18 @@ public final class FillResponse implements Parcelable { private final @Nullable IntentSender mAuthentication; private final @Nullable AutofillId[] mAuthenticationIds; private final @Nullable AutofillId[] mIgnoredIds; + 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; mAuthentication = builder.mAuthentication; mAuthenticationIds = builder.mAuthenticationIds; mIgnoredIds = builder.mIgnoredIds; + mFlags = builder.mFlags; mRequestId = INVALID_REQUEST_ID; } @@ -97,6 +115,11 @@ public final class FillResponse implements Parcelable { return mIgnoredIds; } + /** @hide */ + public int getFlags() { + return mFlags; + } + /** * Associates a {@link FillResponse} to a request. * @@ -122,11 +145,12 @@ 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 IntentSender mAuthentication; private AutofillId[] mAuthenticationIds; private AutofillId[] mIgnoredIds; + private int mFlags; private boolean mDestroyed; /** @@ -264,7 +288,20 @@ public final class FillResponse implements Parcelable { */ public Builder setClientState(@Nullable Bundle clientState) { throwIfDestroyed(); - mCLientState = clientState; + mClientState = clientState; + return this; + } + + /** + * Sets flags changing the response behavior. + * + * @param flags {@link #FLAG_TRACK_CONTEXT_COMMITED}, or {@code 0}. + * + * @return This builder. + */ + public Builder setFlags(@FillResponseFlags int flags) { + throwIfDestroyed(); + mFlags = flags; return this; } @@ -311,6 +348,7 @@ public final class FillResponse implements Parcelable { .append(", hasAuthentication=").append(mAuthentication != null) .append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds)) .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds)) + .append(", flags=").append(mFlags) .append("]") .toString(); } @@ -333,6 +371,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelable(mAuthentication, flags); parcel.writeParcelable(mPresentation, flags); parcel.writeParcelableArray(mIgnoredIds, flags); + parcel.writeInt(mFlags); parcel.writeInt(mRequestId); } @@ -363,8 +402,9 @@ public final class FillResponse implements Parcelable { } builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class)); - final FillResponse response = builder.build(); + builder.setFlags(parcel.readInt()); + final FillResponse response = builder.build(); response.setRequestId(parcel.readInt()); return response; diff --git a/android/service/autofill/InternalTransformation.java b/android/service/autofill/InternalTransformation.java index 974397b3..c9864a0e 100644 --- a/android/service/autofill/InternalTransformation.java +++ b/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/android/service/autofill/InternalValidator.java b/android/service/autofill/InternalValidator.java index e11cf6ad..e08bb6c1 100644 --- a/android/service/autofill/InternalValidator.java +++ b/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/android/service/autofill/LuhnChecksumValidator.java b/android/service/autofill/LuhnChecksumValidator.java index 0b5930df..c56ae84b 100644 --- a/android/service/autofill/LuhnChecksumValidator.java +++ b/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/android/service/autofill/OptionalValidators.java b/android/service/autofill/OptionalValidators.java index f7edd6e4..7aec59f6 100644 --- a/android/service/autofill/OptionalValidators.java +++ b/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/android/service/autofill/RequiredValidators.java b/android/service/autofill/RequiredValidators.java index ac85c284..9e1db2bc 100644 --- a/android/service/autofill/RequiredValidators.java +++ b/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/android/service/autofill/SaveInfo.java b/android/service/autofill/SaveInfo.java index 1b9240cc..fde2416f 100644 --- a/android/service/autofill/SaveInfo.java +++ b/android/service/autofill/SaveInfo.java @@ -68,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. * @@ -103,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: @@ -123,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 @@ -212,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{} @@ -236,6 +252,7 @@ public final class SaveInfo implements Parcelable { private final InternalValidator mValidator; private final InternalSanitizer[] mSanitizerKeys; private final AutofillId[][] mSanitizerValues; + private final AutofillId mTriggerId; private SaveInfo(Builder builder) { mType = builder.mType; @@ -259,6 +276,7 @@ public final class SaveInfo implements Parcelable { mSanitizerValues[i] = builder.mSanitizers.valueAt(i); } } + mTriggerId = builder.mTriggerId; } /** @hide */ @@ -320,6 +338,12 @@ public final class SaveInfo implements Parcelable { return mSanitizerValues; } + /** @hide */ + @Nullable + public AutofillId getTriggerId() { + return mTriggerId; + } + /** * A builder for {@link SaveInfo} objects. */ @@ -338,6 +362,7 @@ public final class SaveInfo implements Parcelable { private ArrayMap<InternalSanitizer, AutofillId[]> mSanitizers; // Set used to validate against duplicate ids. private ArraySet<AutofillId> mSanitizerIds; + private AutofillId mTriggerId; /** * Creates a new builder. @@ -394,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; } @@ -493,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: * @@ -520,7 +547,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"> @@ -615,6 +642,27 @@ public final class SaveInfo implements Parcelable { 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. * @@ -652,13 +700,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(); } @@ -687,6 +736,7 @@ public final class SaveInfo implements Parcelable { parcel.writeParcelableArray(mSanitizerValues[i], flags); } } + parcel.writeParcelable(mTriggerId, flags); parcel.writeInt(mFlags); } @@ -727,6 +777,10 @@ public final class SaveInfo implements Parcelable { 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/android/service/autofill/SaveRequest.java b/android/service/autofill/SaveRequest.java index 65fdb5c4..f53967bd 100644 --- a/android/service/autofill/SaveRequest.java +++ b/android/service/autofill/SaveRequest.java @@ -19,7 +19,6 @@ 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; @@ -60,9 +59,14 @@ 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 extra returned from the service. + * + * <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)} where 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/android/service/autofill/Validator.java b/android/service/autofill/Validator.java index 854aa1e6..a4036f25 100644 --- a/android/service/autofill/Validator.java +++ b/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/android/service/notification/ZenModeConfig.java b/android/service/notification/ZenModeConfig.java index 7bec898a..735b8223 100644 --- a/android/service/notification/ZenModeConfig.java +++ b/android/service/notification/ZenModeConfig.java @@ -76,10 +76,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 +92,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 +105,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 +126,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 +166,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 +197,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 +229,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 +256,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 +352,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 +370,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 +432,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 +459,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 +492,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) { @@ -654,6 +680,12 @@ 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, @@ -680,10 +712,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); diff --git a/android/service/settings/suggestions/SuggestionService.java b/android/service/settings/suggestions/SuggestionService.java index 2a4c84c2..ce9501d6 100644 --- a/android/service/settings/suggestions/SuggestionService.java +++ b/android/service/settings/suggestions/SuggestionService.java @@ -48,12 +48,20 @@ public abstract class SuggestionService extends Service { } @Override - public void dismissSuggestion(Suggestion suggestion) { + 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); + } }; } @@ -65,7 +73,12 @@ public abstract class SuggestionService extends Service { /** * Dismiss a suggestion. The suggestion will not be included in future * {@link #onGetSuggestions()} calls. - * @param suggestion */ 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/android/support/LibraryVersions.java b/android/support/LibraryVersions.java index a046d95e..2f5730a2 100644 --- a/android/support/LibraryVersions.java +++ b/android/support/LibraryVersions.java @@ -28,7 +28,7 @@ public class LibraryVersions { /** * Version code for flatfoot 1.0 projects (room, lifecycles) */ - private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0-beta2"); + private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0-rc1"); /** * Version code for Room @@ -45,15 +45,17 @@ public class LibraryVersions { */ public static final Version PAGING = new Version("1.0.0-alpha3"); + private static final Version LIFECYCLES = new Version("1.0.3"); + /** * Version code for Lifecycle libs that are required by the support library */ - public static final Version LIFECYCLES_CORE = new Version("1.0.2"); + public static final Version LIFECYCLES_CORE = LIFECYCLES; /** * Version code for Lifecycle runtime libs that are required by the support library */ - public static final Version LIFECYCLES_RUNTIME = new Version("1.0.0"); + public static final Version LIFECYCLES_RUNTIME = LIFECYCLES; /** * Version code for shared code of flatfoot diff --git a/android/support/car/drawer/CarDrawerActivity.java b/android/support/car/drawer/CarDrawerActivity.java new file mode 100644 index 00000000..7100218a --- /dev/null +++ b/android/support/car/drawer/CarDrawerActivity.java @@ -0,0 +1,152 @@ +/* + * 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.support.car.drawer; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; +import android.support.car.R; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +/** + * Common base Activity for car apps that need to present a Drawer. + * + * <p>This Activity manages the overall layout. To use it, sub-classes need to: + * + * <ul> + * <li>Provide the root-items for the Drawer by implementing {@link #getRootAdapter()}. + * <li>Add their main content using {@link #setMainContent(int)} or {@link #setMainContent(View)}. + * They can also add fragments to the main-content container by obtaining its id using + * {@link #getContentContainerId()} + * </ul> + * + * <p>This class will take care of drawer toggling and display. + * + * <p>The rootAdapter can implement nested-navigation, in its click-handling, by passing the + * CarDrawerAdapter for the next level to + * {@link CarDrawerController#switchToAdapter(CarDrawerAdapter)}. + * + * <p>Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a + * derivative. + */ +public abstract class CarDrawerActivity extends AppCompatActivity { + private CarDrawerController mDrawerController; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.car_drawer_activity); + + DrawerLayout drawerLayout = findViewById(R.id.drawer_layout); + ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle( + this /* activity */, + drawerLayout, /* DrawerLayout object */ + R.string.car_drawer_open, + R.string.car_drawer_close); + + Toolbar toolbar = findViewById(R.id.car_toolbar); + setSupportActionBar(toolbar); + + mDrawerController = new CarDrawerController(toolbar, drawerLayout, drawerToggle); + mDrawerController.setRootAdapter(getRootAdapter()); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + } + + /** + * Returns the {@link CarDrawerController} that is responsible for handling events relating + * to the drawer in this Activity. + * + * @return The {@link CarDrawerController} linked to this Activity. This value will be + * {@code null} if this method is called before {@code onCreate()} has been called. + */ + @Nullable + protected CarDrawerController getDrawerController() { + return mDrawerController; + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + mDrawerController.syncState(); + } + + /** + * @return Adapter for root content of the Drawer. + */ + protected abstract CarDrawerAdapter getRootAdapter(); + + /** + * Set main content to display in this Activity. It will be added to R.id.content_frame in + * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(View)}. + * + * @param view View to display as main content. + */ + public void setMainContent(View view) { + ViewGroup parent = findViewById(getContentContainerId()); + parent.addView(view); + } + + /** + * Set main content to display in this Activity. It will be added to R.id.content_frame in + * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(int)}. + * + * @param resourceId Layout to display as main content. + */ + public void setMainContent(@LayoutRes int resourceId) { + ViewGroup parent = findViewById(getContentContainerId()); + LayoutInflater inflater = getLayoutInflater(); + inflater.inflate(resourceId, parent, true); + } + + /** + * Get the id of the main content Container which is a FrameLayout. Subclasses can add their own + * content/fragments inside here. + * + * @return Id of FrameLayout where main content of the subclass Activity can be added. + */ + protected int getContentContainerId() { + return R.id.content_frame; + } + + @Override + protected void onStop() { + super.onStop(); + mDrawerController.closeDrawer(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDrawerController.onConfigurationChanged(newConfig); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return mDrawerController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); + } +} diff --git a/android/support/car/drawer/CarDrawerAdapter.java b/android/support/car/drawer/CarDrawerAdapter.java new file mode 100644 index 00000000..b0fd965d --- /dev/null +++ b/android/support/car/drawer/CarDrawerAdapter.java @@ -0,0 +1,182 @@ +/* + * 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.support.car.drawer; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.car.R; +import android.support.car.widget.PagedListView; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * Base adapter for displaying items in the car navigation drawer, which uses a + * {@link PagedListView}. + * + * <p>Subclasses must set the title that will be displayed when displaying the contents of the + * drawer via {@link #setTitle(CharSequence)}. The title can be updated at any point later on. The + * title of the root adapter will also be the main title showed in the toolbar when the drawer is + * closed. See {@link CarDrawerController#setRootAdapter(CarDrawerAdapter)} for more information. + * + * <p>This class also takes care of implementing the PageListView.ItemCamp contract and subclasses + * should implement {@link #getActualItemCount()}. + */ +public abstract class CarDrawerAdapter extends RecyclerView.Adapter<DrawerItemViewHolder> + implements PagedListView.ItemCap, DrawerItemClickListener { + private final boolean mShowDisabledListOnEmpty; + private final Drawable mEmptyListDrawable; + private int mMaxItems = PagedListView.ItemCap.UNLIMITED; + private CharSequence mTitle; + private TitleChangeListener mTitleChangeListener; + + /** + * Interface for a class that will be notified a new title has been set on this adapter. + */ + interface TitleChangeListener { + /** + * Called when {@link #setTitle(CharSequence)} has been called and the title has been + * changed. + */ + void onTitleChanged(CharSequence newTitle); + } + + protected CarDrawerAdapter(Context context, boolean showDisabledListOnEmpty) { + mShowDisabledListOnEmpty = showDisabledListOnEmpty; + + mEmptyListDrawable = context.getDrawable(R.drawable.ic_list_view_disable); + mEmptyListDrawable.setColorFilter(context.getColor(R.color.car_tint), + PorterDuff.Mode.SRC_IN); + } + + /** Returns the title set via {@link #setTitle(CharSequence)}. */ + CharSequence getTitle() { + return mTitle; + } + + /** Updates the title to display in the toolbar for this Adapter. */ + public final void setTitle(@NonNull CharSequence title) { + if (title == null) { + throw new IllegalArgumentException("setTitle() cannot be passed a null title!"); + } + + mTitle = title; + + if (mTitleChangeListener != null) { + mTitleChangeListener.onTitleChanged(mTitle); + } + } + + /** Sets a listener to be notified whenever the title of this adapter has been changed. */ + void setTitleChangeListener(@Nullable TitleChangeListener listener) { + mTitleChangeListener = listener; + } + + @Override + public final void setMaxItems(int maxItems) { + mMaxItems = maxItems; + } + + @Override + public final int getItemCount() { + if (shouldShowDisabledListItem()) { + return 1; + } + return mMaxItems >= 0 ? Math.min(mMaxItems, getActualItemCount()) : getActualItemCount(); + } + + /** + * Returns the absolute number of items that can be displayed in the list. + * + * <p>A class should implement this method to supply the number of items to be displayed. + * Returning 0 from this method will cause an empty list icon to be displayed in the drawer. + * + * <p>A class should override this method rather than {@link #getItemCount()} because that + * method is handling the logic of when to display the empty list icon. It will return 1 when + * {@link #getActualItemCount()} returns 0. + * + * @return The number of items to be displayed in the list. + */ + protected abstract int getActualItemCount(); + + @Override + public final int getItemViewType(int position) { + if (shouldShowDisabledListItem()) { + return R.layout.car_drawer_list_item_empty; + } + + return usesSmallLayout(position) + ? R.layout.car_drawer_list_item_small + : R.layout.car_drawer_list_item_normal; + } + + /** + * Used to indicate the layout used for the Drawer item at given position. Subclasses can + * override this to use normal layout which includes text element below title. + * + * <p>A small layout is presented by the layout {@code R.layout.car_drawer_list_item_small}. + * Otherwise, the layout {@code R.layout.car_drawer_list_item_normal} will be used. + * + * @param position Adapter position of item. + * @return Whether the item at this position will use a small layout (default) or normal layout. + */ + protected boolean usesSmallLayout(int position) { + return true; + } + + @Override + public final DrawerItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); + return new DrawerItemViewHolder(view); + } + + @Override + public final void onBindViewHolder(DrawerItemViewHolder holder, int position) { + if (shouldShowDisabledListItem()) { + holder.getTitle().setText(null); + holder.getIcon().setImageDrawable(mEmptyListDrawable); + holder.setItemClickListener(null); + } else { + holder.setItemClickListener(this); + populateViewHolder(holder, position); + } + } + + /** + * Whether or not this adapter should be displaying an empty list icon. The icon is shown if it + * has been configured to show and there are no items to be displayed. + */ + private boolean shouldShowDisabledListItem() { + return mShowDisabledListOnEmpty && getActualItemCount() == 0; + } + + /** + * Subclasses should set all elements in {@code holder} to populate the drawer-item. If some + * element is not used, it should be nulled out since these ViewHolder/View's are recycled. + */ + protected abstract void populateViewHolder(DrawerItemViewHolder holder, int position); + + /** + * Called when this adapter has been popped off the stack and is no longer needed. Subclasses + * can override to do any necessary cleanup. + */ + public void cleanup() {} +} diff --git a/android/support/car/drawer/CarDrawerController.java b/android/support/car/drawer/CarDrawerController.java new file mode 100644 index 00000000..4d9f4e99 --- /dev/null +++ b/android/support/car/drawer/CarDrawerController.java @@ -0,0 +1,306 @@ +/* + * 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.support.car.drawer; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.car.R; +import android.support.car.widget.PagedListView; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.widget.Toolbar; +import android.view.Gravity; +import android.view.MenuItem; +import android.view.View; +import android.widget.ProgressBar; + +import java.util.Stack; + +/** + * A controller that will handle the set up of the navigation drawer. It will hook up the + * necessary buttons for up navigation, as well as expose methods to allow for a drill down + * navigation. + */ +public class CarDrawerController { + /** The amount that the drawer has been opened before its color should be switched. */ + private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f; + + /** + * A representation of the hierarchy of navigation being displayed in the list. The ordering of + * this stack is the order that the user has visited each level. When the user navigates up, + * the adapters are poopped from this list. + */ + private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>(); + + private final Context mContext; + + private final Toolbar mToolbar; + private final DrawerLayout mDrawerLayout; + private final ActionBarDrawerToggle mDrawerToggle; + + private final PagedListView mDrawerList; + private final ProgressBar mProgressBar; + private final View mDrawerContent; + + /** + * Creates a {@link CarDrawerController} that will control the navigation of the drawer given by + * {@code drawerLayout}. + * + * <p>The given {@code drawerLayout} should either have a child View that is inflated from + * {@code R.layout.car_drawer} or ensure that it three children that have the IDs found in that + * layout. + * + * @param toolbar The {@link Toolbar} that will serve as the action bar for an Activity. + * @param drawerLayout The top-level container for the window content that shows the + * interactive drawer. + * @param drawerToggle The {@link ActionBarDrawerToggle} that bridges the given {@code toolbar} + * and {@code drawerLayout}. + */ + public CarDrawerController(Toolbar toolbar, + DrawerLayout drawerLayout, + ActionBarDrawerToggle drawerToggle) { + mToolbar = toolbar; + mContext = drawerLayout.getContext(); + + mDrawerLayout = drawerLayout; + + mDrawerContent = drawerLayout.findViewById(R.id.drawer_content); + mDrawerList = drawerLayout.findViewById(R.id.drawer_list); + mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED); + + mProgressBar = drawerLayout.findViewById(R.id.drawer_progress); + + mDrawerToggle = drawerToggle; + setupDrawerToggling(); + } + + /** + * Sets the {@link CarDrawerAdapter} that will function as the root adapter. The contents of + * this root adapter are shown when the drawer is first opened. It is also the top-most level of + * navigation in the drawer. + * + * @param rootAdapter The adapter that will act as the root. If this value is {@code null}, then + * this method will do nothing. + */ + public void setRootAdapter(@Nullable CarDrawerAdapter rootAdapter) { + if (rootAdapter == null) { + return; + } + + mAdapterStack.push(rootAdapter); + setToolbarTitleFrom(rootAdapter); + mDrawerList.setAdapter(rootAdapter); + } + + /** + * Switches to use the given {@link CarDrawerAdapter} as the one to supply the list to display + * in the navigation drawer. The title will also be updated from the adapter. + * + * <p>This switch is treated as a navigation to the next level in the drawer. Navigation away + * from this level will pop the given adapter off and surface contents of the previous adapter + * that was set via this method. If no such adapter exists, then the root adapter set by + * {@link #setRootAdapter(CarDrawerAdapter)} will be used instead. + * + * @param adapter Adapter for next level of content in the drawer. + */ + public final void switchToAdapter(CarDrawerAdapter adapter) { + mAdapterStack.peek().setTitleChangeListener(null); + mAdapterStack.push(adapter); + switchToAdapterInternal(adapter); + } + + /** Close the drawer. */ + public void closeDrawer() { + if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { + mDrawerLayout.closeDrawer(Gravity.LEFT); + } + } + + /** Opens the drawer. */ + public void openDrawer() { + if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) { + mDrawerLayout.openDrawer(Gravity.LEFT); + } + } + + /** Sets a listener to be notified of Drawer events. */ + public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { + mDrawerLayout.addDrawerListener(listener); + } + + /** Removes a listener to be notified of Drawer events. */ + public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) { + mDrawerLayout.removeDrawerListener(listener); + } + + /** + * Sets whether the loading progress bar is displayed in the navigation drawer. If {@code true}, + * the progress bar is displayed and the navigation list is hidden and vice versa. + */ + public void showLoadingProgressBar(boolean show) { + mDrawerList.setVisibility(show ? View.INVISIBLE : View.VISIBLE); + mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE); + } + + /** Scroll to given position in the list. */ + public void scrollToPosition(int position) { + mDrawerList.getRecyclerView().smoothScrollToPosition(position); + } + + /** + * Retrieves the title from the given {@link CarDrawerAdapter} and set its as the title of this + * controller's internal Toolbar. + */ + private void setToolbarTitleFrom(CarDrawerAdapter adapter) { + if (adapter.getTitle() == null) { + throw new RuntimeException("CarDrawerAdapter must supply a title via setTitle()"); + } + + mToolbar.setTitle(adapter.getTitle()); + adapter.setTitleChangeListener(mToolbar::setTitle); + } + + /** + * Sets up the necessary listeners for {@link DrawerLayout} so that the navigation drawer + * hierarchy is properly displayed. + */ + private void setupDrawerToggling() { + mDrawerLayout.addDrawerListener(mDrawerToggle); + mDrawerLayout.addDrawerListener( + new DrawerLayout.DrawerListener() { + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + // Correctly set the title and arrow colors as they are different between + // the open and close states. + updateTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET); + } + + @Override + public void onDrawerClosed(View drawerView) { + // If drawer is closed, revert stack/drawer to initial root state. + cleanupStackAndShowRoot(); + scrollToPosition(0); + } + + @Override + public void onDrawerOpened(View drawerView) {} + + @Override + public void onDrawerStateChanged(int newState) {} + }); + } + + /** Sets the title and arrow color of the drawer depending on if it is open or not. */ + private void updateTitleAndArrowColor(boolean drawerOpen) { + // When the drawer is open, use car_title, which resolves to appropriate color depending on + // day-night mode. When drawer is closed, we always use light color. + int titleColorResId = drawerOpen ? R.color.car_title : R.color.car_title_light; + int titleColor = mContext.getColor(titleColorResId); + mToolbar.setTitleTextColor(titleColor); + mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor); + } + + /** + * Synchronizes the display of the drawer with its linked {@link DrawerLayout}. + * + * <p>This should be called from the associated Activity's + * {@link android.support.v7.app.AppCompatActivity#onPostCreate(Bundle)} method to synchronize + * after teh DRawerLayout's instance state has been restored, and any other time when the + * state may have diverged in such a way that this controller's associated + * {@link ActionBarDrawerToggle} had not been notified. + */ + public void syncState() { + mDrawerToggle.syncState(); + + // In case we're restarting after a config change (e.g. day, night switch), set colors + // again. Doing it here so that Drawer state is fully synced and we know if its open or not. + // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout. + updateTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent)); + } + + /** + * Notify this controller that device configurations may have changed. + * + * <p>This method should be called from the associated Activity's + * {@code onConfigurationChanged()} method. + */ + public void onConfigurationChanged(Configuration newConfig) { + // Pass any configuration change to the drawer toggle. + mDrawerToggle.onConfigurationChanged(newConfig); + } + + /** + * An analog to an Activity's {@code onOptionsItemSelected()}. This method should be called + * when the Activity's method is called and will return {@code true} if the selection has + * been handled. + * + * @return {@code true} if the item processing was handled by this class. + */ + public boolean onOptionsItemSelected(MenuItem item) { + // Handle home-click and see if we can navigate up in the drawer. + if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) { + return true; + } + + // DrawerToggle gets next chance to handle up-clicks (and any other clicks). + return mDrawerToggle.onOptionsItemSelected(item); + } + + /** + * Sets the navigation drawer's title to be the one supplied by the given adapter and updates + * the navigation drawer list with the adapter's contents. + */ + private void switchToAdapterInternal(CarDrawerAdapter adapter) { + setToolbarTitleFrom(adapter); + // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between + // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts. + mDrawerList.getRecyclerView().setAdapter(adapter); + scrollToPosition(0); + } + + /** + * Switches to the previous level in the drawer hierarchy if the current list being displayed + * is not the root adapter. This is analogous to a navigate up. + * + * @return {@code true} if a navigate up was possible and executed. {@code false} otherwise. + */ + private boolean maybeHandleUpClick() { + // Check if already at the root level. + if (mAdapterStack.size() <= 1) { + return false; + } + + CarDrawerAdapter adapter = mAdapterStack.pop(); + adapter.setTitleChangeListener(null); + adapter.cleanup(); + switchToAdapterInternal(mAdapterStack.peek()); + return true; + } + + /** Clears stack down to root adapter and switches to root adapter. */ + private void cleanupStackAndShowRoot() { + while (mAdapterStack.size() > 1) { + CarDrawerAdapter adapter = mAdapterStack.pop(); + adapter.setTitleChangeListener(null); + adapter.cleanup(); + } + switchToAdapterInternal(mAdapterStack.peek()); + } +} diff --git a/android/arch/paging/User.java b/android/support/car/drawer/DrawerItemClickListener.java index a6d965a2..d707dbd0 100644 --- a/android/arch/paging/User.java +++ b/android/support/car/drawer/DrawerItemClickListener.java @@ -14,19 +14,16 @@ * limitations under the License. */ -package android.arch.paging; +package android.support.car.drawer; -public class User { - public final String name; - public final String info; - - public User(String name, String info) { - this.name = name; - this.info = info; - } - - @Override - public String toString() { - return name; - } +/** + * Listener for handling clicks on items/views managed by {@link DrawerItemViewHolder}. + */ +public interface DrawerItemClickListener { + /** + * Callback when item is clicked. + * + * @param position Adapter position of the clicked item. + */ + void onItemClick(int position); } diff --git a/android/support/car/drawer/DrawerItemViewHolder.java b/android/support/car/drawer/DrawerItemViewHolder.java new file mode 100644 index 00000000..d016b2de --- /dev/null +++ b/android/support/car/drawer/DrawerItemViewHolder.java @@ -0,0 +1,87 @@ +/* + * 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.support.car.drawer; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.car.R; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * Re-usable {@link RecyclerView.ViewHolder} for displaying items in the + * {@link android.support.car.drawer.CarDrawerAdapter}. + */ +public class DrawerItemViewHolder extends RecyclerView.ViewHolder { + private final ImageView mIcon; + private final TextView mTitle; + private final TextView mText; + private final ImageView mEndIcon; + + DrawerItemViewHolder(View view) { + super(view); + mIcon = view.findViewById(R.id.icon); + if (mIcon == null) { + throw new IllegalArgumentException("Icon view cannot be null!"); + } + + mTitle = view.findViewById(R.id.title); + if (mTitle == null) { + throw new IllegalArgumentException("Title view cannot be null!"); + } + + // Next two are optional and may be null. + mText = view.findViewById(R.id.text); + mEndIcon = view.findViewById(R.id.end_icon); + } + + /** Returns the view that should be used to display the main icon. */ + @NonNull + public ImageView getIcon() { + return mIcon; + } + + /** Returns the view that will display the main title. */ + @NonNull + public TextView getTitle() { + return mTitle; + } + + /** Returns the view that is used for text that is smaller than the title text. */ + @Nullable + public TextView getText() { + return mText; + } + + /** Returns the icon that is displayed at the end of the view. */ + @Nullable + public ImageView getEndIcon() { + return mEndIcon; + } + + /** + * Sets the listener that will be notified when the view held by this ViewHolder has been + * clicked. Passing {@code null} will clear any previously set listeners. + */ + void setItemClickListener(@Nullable DrawerItemClickListener listener) { + itemView.setOnClickListener(listener != null + ? v -> listener.onItemClick(getAdapterPosition()) + : null); + } +} diff --git a/android/support/car/widget/PagedListView.java b/android/support/car/widget/PagedListView.java index 8527c659..46527001 100644 --- a/android/support/car/widget/PagedListView.java +++ b/android/support/car/widget/PagedListView.java @@ -27,7 +27,6 @@ import android.os.Handler; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.Px; import android.support.annotation.RestrictTo; import android.support.annotation.UiThread; import android.support.car.R; @@ -61,7 +60,6 @@ public class PagedListView extends FrameLayout { protected final CarLayoutManager mLayoutManager; protected final Handler mHandler = new Handler(); private final boolean mScrollBarEnabled; - private final boolean mRightGutterEnabled; private final PagedScrollBarView mScrollBarView; private int mRowsPerPage = -1; @@ -98,6 +96,11 @@ public class PagedListView extends FrameLayout { */ public interface ItemCap { /** + * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit. + */ + int UNLIMITED = -1; + + /** * Sets the maximum number of items available in the adapter. A value less than '0' means * the list should not be capped. */ @@ -139,7 +142,6 @@ public class PagedListView extends FrameLayout { } LayoutInflater.from(context).inflate(layoutId, this /*root*/, true /*attachToRoot*/); - FrameLayout maxWidthLayout = (FrameLayout) findViewById(R.id.recycler_view_container); TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.PagedListView, defStyleAttrs, defStyleRes); mRecyclerView = (CarRecyclerView) findViewById(R.id.recycler_view); @@ -156,6 +158,16 @@ public class PagedListView extends FrameLayout { mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12); mRecyclerView.setItemAnimator(new CarItemAnimator(mLayoutManager)); + boolean offsetScrollBar = a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false); + if (offsetScrollBar) { + MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); + params.setMarginStart(getResources().getDimensionPixelSize( + R.dimen.car_screen_margin_size)); + params.setMarginEnd( + a.getDimensionPixelSize(R.styleable.PagedListView_listEndMargin, 0)); + mRecyclerView.setLayoutParams(params); + } + if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) { int dividerStartMargin = a.getDimensionPixelSize( R.styleable.PagedListView_dividerStartMargin, 0); @@ -199,47 +211,20 @@ public class PagedListView extends FrameLayout { } } }); + mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE); - // Modify the layout if the Gutter or the Scroll Bar are not visible. - mRightGutterEnabled = a.getBoolean(R.styleable.PagedListView_rightGutterEnabled, false); - if (mRightGutterEnabled || !mScrollBarEnabled) { - FrameLayout.LayoutParams maxWidthLayoutLayoutParams = - (FrameLayout.LayoutParams) maxWidthLayout.getLayoutParams(); - if (mRightGutterEnabled) { - maxWidthLayoutLayoutParams.rightMargin = - getResources().getDimensionPixelSize(R.dimen.car_card_margin); - } - if (!mScrollBarEnabled) { - maxWidthLayoutLayoutParams.setMarginStart(0); - } - maxWidthLayout.setLayoutParams(maxWidthLayoutLayoutParams); + // Modify the layout the Scroll Bar is not visible. + if (!mScrollBarEnabled) { + MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); + params.setMarginStart(0); + mRecyclerView.setLayoutParams(params); } setDayNightStyle(DayNightStyle.AUTO); a.recycle(); } - /** - * Sets the starting and ending padding for each view in the list. - * - * @param start The start padding. - * @param end The end padding. - */ - public void setListViewStartEndPadding(@Px int start, @Px int end) { - int carCardMargin = getResources().getDimensionPixelSize(R.dimen.car_card_margin); - int startGutter = mScrollBarEnabled ? carCardMargin : 0; - int startPadding = Math.max(start - startGutter, 0); - int endGutter = mRightGutterEnabled ? carCardMargin : 0; - int endPadding = Math.max(end - endGutter, 0); - mRecyclerView.setPaddingRelative(startPadding, mRecyclerView.getPaddingTop(), - endPadding, mRecyclerView.getPaddingBottom()); - - // Since we're setting padding we'll need to set the clip to padding to the same - // value as clip children to ensure that the cards fly off the screen. - mRecyclerView.setClipToPadding(mRecyclerView.getClipChildren()); - } - @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); diff --git a/android/support/media/tv/BasePreviewProgram.java b/android/support/media/tv/BasePreviewProgram.java index 1423d9d6..39c30140 100644 --- a/android/support/media/tv/BasePreviewProgram.java +++ b/android/support/media/tv/BasePreviewProgram.java @@ -23,14 +23,13 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.support.annotation.IntDef; import android.support.annotation.RestrictTo; import android.support.media.tv.TvContractCompat.PreviewProgramColumns; -import android.support.media.tv.TvContractCompat.PreviewProgramColumns.AspectRatio; -import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Availability; -import android.support.media.tv.TvContractCompat.PreviewProgramColumns.InteractionType; -import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Type; import android.support.media.tv.TvContractCompat.PreviewPrograms; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.util.Date; @@ -55,6 +54,89 @@ public abstract class BasePreviewProgram extends BaseProgram { private static final int IS_LIVE = 1; private static final int IS_BROWSABLE = 1; + /** @hide */ + @IntDef({ + TYPE_UNKNOWN, + PreviewProgramColumns.TYPE_MOVIE, + PreviewProgramColumns.TYPE_TV_SERIES, + PreviewProgramColumns.TYPE_TV_SEASON, + PreviewProgramColumns.TYPE_TV_EPISODE, + PreviewProgramColumns.TYPE_CLIP, + PreviewProgramColumns.TYPE_EVENT, + PreviewProgramColumns.TYPE_CHANNEL, + PreviewProgramColumns.TYPE_TRACK, + PreviewProgramColumns.TYPE_ALBUM, + PreviewProgramColumns.TYPE_ARTIST, + PreviewProgramColumns.TYPE_PLAYLIST, + PreviewProgramColumns.TYPE_STATION, + PreviewProgramColumns.TYPE_GAME + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface Type {} + + /** + * The unknown program type. + */ + private static final int TYPE_UNKNOWN = -1; + + /** @hide */ + @IntDef({ + ASPECT_RATIO_UNKNOWN, + PreviewProgramColumns.ASPECT_RATIO_16_9, + PreviewProgramColumns.ASPECT_RATIO_3_2, + PreviewProgramColumns.ASPECT_RATIO_4_3, + PreviewProgramColumns.ASPECT_RATIO_1_1, + PreviewProgramColumns.ASPECT_RATIO_2_3, + PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface AspectRatio {} + + /** + * The aspect ratio for unknown aspect ratios. + */ + private static final int ASPECT_RATIO_UNKNOWN = -1; + + /** @hide */ + @IntDef({ + AVAILABILITY_UNKNOWN, + PreviewProgramColumns.AVAILABILITY_AVAILABLE, + PreviewProgramColumns.AVAILABILITY_FREE_WITH_SUBSCRIPTION, + PreviewProgramColumns.AVAILABILITY_PAID_CONTENT, + PreviewProgramColumns.AVAILABILITY_PURCHASED, + PreviewProgramColumns.AVAILABILITY_FREE + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface Availability {} + + /** + * The unknown availability. + */ + private static final int AVAILABILITY_UNKNOWN = -1; + + /** @hide */ + @IntDef({ + INTERACTION_TYPE_UNKNOWN, + PreviewProgramColumns.INTERACTION_TYPE_VIEWS, + PreviewProgramColumns.INTERACTION_TYPE_LISTENS, + PreviewProgramColumns.INTERACTION_TYPE_FOLLOWERS, + PreviewProgramColumns.INTERACTION_TYPE_FANS, + PreviewProgramColumns.INTERACTION_TYPE_LIKES, + PreviewProgramColumns.INTERACTION_TYPE_THUMBS, + PreviewProgramColumns.INTERACTION_TYPE_VIEWERS, + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface InteractionType {} + + /** + * The unknown interaction type. + */ + private static final int INTERACTION_TYPE_UNKNOWN = -1; + BasePreviewProgram(Builder builder) { super(builder); } @@ -127,7 +209,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @Type int getType() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_TYPE); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? TYPE_UNKNOWN : i; } /** @@ -137,7 +219,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @AspectRatio int getPosterArtAspectRatio() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? ASPECT_RATIO_UNKNOWN : i; } /** @@ -147,7 +229,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @AspectRatio int getThumbnailAspectRatio() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? ASPECT_RATIO_UNKNOWN : i; } /** @@ -165,7 +247,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @Availability int getAvailability() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_AVAILABILITY); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? AVAILABILITY_UNKNOWN : i; } /** @@ -216,7 +298,7 @@ public abstract class BasePreviewProgram extends BaseProgram { */ public @InteractionType int getInteractionType() { Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_INTERACTION_TYPE); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? INTERACTION_TYPE_UNKNOWN : i; } /** diff --git a/android/support/media/tv/BaseProgram.java b/android/support/media/tv/BaseProgram.java index e4ce9d1f..23b5cf9c 100644 --- a/android/support/media/tv/BaseProgram.java +++ b/android/support/media/tv/BaseProgram.java @@ -22,13 +22,16 @@ import android.database.Cursor; import android.media.tv.TvContentRating; import android.net.Uri; import android.os.Build; +import android.support.annotation.IntDef; import android.support.annotation.RestrictTo; import android.support.media.tv.TvContractCompat.BaseTvColumns; import android.support.media.tv.TvContractCompat.ProgramColumns; -import android.support.media.tv.TvContractCompat.ProgramColumns.ReviewRatingStyle; import android.support.media.tv.TvContractCompat.Programs; import android.support.media.tv.TvContractCompat.Programs.Genres.Genre; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Base class for derived classes that want to have common fields for programs defined in * {@link TvContractCompat}. @@ -46,6 +49,22 @@ public abstract class BaseProgram { private static final int IS_SEARCHABLE = 1; /** @hide */ + @IntDef({ + REVIEW_RATING_STYLE_UNKNOWN, + ProgramColumns.REVIEW_RATING_STYLE_STARS, + ProgramColumns.REVIEW_RATING_STYLE_THUMBS_UP_DOWN, + ProgramColumns.REVIEW_RATING_STYLE_PERCENTAGE, + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + @interface ReviewRatingStyle {} + + /** + * The unknown review rating style. + */ + private static final int REVIEW_RATING_STYLE_UNKNOWN = -1; + + /** @hide */ @RestrictTo(LIBRARY_GROUP) protected ContentValues mValues; @@ -254,7 +273,7 @@ public abstract class BaseProgram { */ public @ReviewRatingStyle int getReviewRatingStyle() { Integer i = mValues.getAsInteger(Programs.COLUMN_REVIEW_RATING_STYLE); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? REVIEW_RATING_STYLE_UNKNOWN : i; } /** diff --git a/android/support/media/tv/Program.java b/android/support/media/tv/Program.java index 4e3bd7ac..233f1bab 100644 --- a/android/support/media/tv/Program.java +++ b/android/support/media/tv/Program.java @@ -25,6 +25,7 @@ import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import android.support.media.tv.TvContractCompat.Programs; +import android.support.media.tv.TvContractCompat.Programs.Genres.Genre; /** * A convenience class to access {@link TvContractCompat.Programs} entries in the system content @@ -282,7 +283,7 @@ public final class Program extends BaseProgram implements Comparable<Program> { * @return This Builder object to allow for chaining of calls to builder methods. * @see Programs#COLUMN_BROADCAST_GENRE */ - public Builder setBroadcastGenres(String[] genres) { + public Builder setBroadcastGenres(@Genre String[] genres) { mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres)); return this; } diff --git a/android/support/media/tv/TvContractCompat.java b/android/support/media/tv/TvContractCompat.java index 5a46e791..de4fd04f 100644 --- a/android/support/media/tv/TvContractCompat.java +++ b/android/support/media/tv/TvContractCompat.java @@ -30,7 +30,6 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.BaseColumns; -import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; @@ -606,16 +605,6 @@ public final class TvContractCompat { */ @RestrictTo(LIBRARY_GROUP) interface ProgramColumns { - /** @hide */ - @IntDef({ - REVIEW_RATING_STYLE_STARS, - REVIEW_RATING_STYLE_THUMBS_UP_DOWN, - REVIEW_RATING_STYLE_PERCENTAGE, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - @interface ReviewRatingStyle {} - /** * The review rating style for five star rating. * @@ -934,27 +923,6 @@ public final class TvContractCompat { */ @RestrictTo(LIBRARY_GROUP) public interface PreviewProgramColumns { - - /** @hide */ - @IntDef({ - TYPE_MOVIE, - TYPE_TV_SERIES, - TYPE_TV_SEASON, - TYPE_TV_EPISODE, - TYPE_CLIP, - TYPE_EVENT, - TYPE_CHANNEL, - TYPE_TRACK, - TYPE_ALBUM, - TYPE_ARTIST, - TYPE_PLAYLIST, - TYPE_STATION, - TYPE_GAME - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface Type {} - /** * The program type for movie. * @@ -1046,19 +1014,6 @@ public final class TvContractCompat { */ int TYPE_GAME = 12; - /** @hide */ - @IntDef({ - ASPECT_RATIO_16_9, - ASPECT_RATIO_3_2, - ASPECT_RATIO_4_3, - ASPECT_RATIO_1_1, - ASPECT_RATIO_2_3, - ASPECT_RATIO_MOVIE_POSTER, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface AspectRatio {} - /** * The aspect ratio for 16:9. * @@ -1107,18 +1062,6 @@ public final class TvContractCompat { */ int ASPECT_RATIO_MOVIE_POSTER = 5; - /** @hide */ - @IntDef({ - AVAILABILITY_AVAILABLE, - AVAILABILITY_FREE_WITH_SUBSCRIPTION, - AVAILABILITY_PAID_CONTENT, - AVAILABILITY_PURCHASED, - AVAILABILITY_FREE, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface Availability {} - /** * The availability for "available to this user". * @@ -1155,20 +1098,6 @@ public final class TvContractCompat { */ int AVAILABILITY_FREE = 4; - /** @hide */ - @IntDef({ - INTERACTION_TYPE_VIEWS, - INTERACTION_TYPE_LISTENS, - INTERACTION_TYPE_FOLLOWERS, - INTERACTION_TYPE_FANS, - INTERACTION_TYPE_LIKES, - INTERACTION_TYPE_THUMBS, - INTERACTION_TYPE_VIEWERS, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface InteractionType {} - /** * The interaction type for "views". * @@ -2895,17 +2824,6 @@ public final class TvContractCompat { /** The MIME type of a single preview TV program. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/watch_next_program"; - /** @hide */ - @IntDef({ - WATCH_NEXT_TYPE_CONTINUE, - WATCH_NEXT_TYPE_NEXT, - WATCH_NEXT_TYPE_NEW, - WATCH_NEXT_TYPE_WATCHLIST, - }) - @Retention(RetentionPolicy.SOURCE) - @RestrictTo(LIBRARY_GROUP) - public @interface WatchNextType {} - /** * The watch next type for CONTINUE. Use this type when the user has already watched more * than 1 minute of this content. diff --git a/android/support/media/tv/WatchNextProgram.java b/android/support/media/tv/WatchNextProgram.java index f4665846..c192745c 100644 --- a/android/support/media/tv/WatchNextProgram.java +++ b/android/support/media/tv/WatchNextProgram.java @@ -22,12 +22,15 @@ import android.content.ContentValues; import android.database.Cursor; import android.media.tv.TvContentRating; // For javadoc gen of super class import android.os.Build; +import android.support.annotation.IntDef; import android.support.annotation.RestrictTo; import android.support.media.tv.TvContractCompat.PreviewPrograms; // For javadoc gen of super class import android.support.media.tv.TvContractCompat.Programs; // For javadoc gen of super class import android.support.media.tv.TvContractCompat.Programs.Genres; // For javadoc gen of super class import android.support.media.tv.TvContractCompat.WatchNextPrograms; -import android.support.media.tv.TvContractCompat.WatchNextPrograms.WatchNextType; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * A convenience class to access {@link WatchNextPrograms} entries in the system content @@ -87,16 +90,34 @@ public final class WatchNextProgram extends BasePreviewProgram { private static final long INVALID_LONG_VALUE = -1; private static final int INVALID_INT_VALUE = -1; + /** @hide */ + @IntDef({ + WATCH_NEXT_TYPE_UNKNOWN, + WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE, + WatchNextPrograms.WATCH_NEXT_TYPE_NEXT, + WatchNextPrograms.WATCH_NEXT_TYPE_NEW, + WatchNextPrograms.WATCH_NEXT_TYPE_WATCHLIST, + }) + @Retention(RetentionPolicy.SOURCE) + @RestrictTo(LIBRARY_GROUP) + public @interface WatchNextType {} + + /** + * The unknown watch next type. Use this type when the actual type is not known. + */ + public static final int WATCH_NEXT_TYPE_UNKNOWN = -1; + private WatchNextProgram(Builder builder) { super(builder); } /** - * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program. + * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program, + * or {@link #WATCH_NEXT_TYPE_UNKNOWN} if it's unknown. */ public @WatchNextType int getWatchNextType() { Integer i = mValues.getAsInteger(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE); - return i == null ? INVALID_INT_VALUE : i; + return i == null ? WATCH_NEXT_TYPE_UNKNOWN : i; } /** diff --git a/android/support/mediacompat/testlib/IntentConstants.java b/android/support/mediacompat/testlib/IntentConstants.java index bc35935e..b0e3d5fe 100644 --- a/android/support/mediacompat/testlib/IntentConstants.java +++ b/android/support/mediacompat/testlib/IntentConstants.java @@ -17,11 +17,18 @@ package android.support.mediacompat.testlib; /** - * Constants used for sending intent between client and service apks. + * Constants used for sending intent between client and service apps. */ public class IntentConstants { public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD = "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"; + public static final String ACTION_CALL_MEDIA_SESSION_METHOD = + "android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD"; + public static final String ACTION_CALL_MEDIA_CONTROLLER_METHOD = + "android.support.mediacompat.client.action.CALL_MEDIA_CONTROLLER_METHOD"; + public static final String ACTION_CALL_TRANSPORT_CONTROLS_METHOD = + "android.support.mediacompat.client.action.CALL_TRANSPORT_CONTROLS_METHOD"; public static final String KEY_METHOD_ID = "method_id"; public static final String KEY_ARGUMENT = "argument"; + public static final String KEY_SESSION_TOKEN = "session_token"; } diff --git a/android/support/mediacompat/testlib/MediaControllerConstants.java b/android/support/mediacompat/testlib/MediaControllerConstants.java new file mode 100644 index 00000000..1de00efc --- /dev/null +++ b/android/support/mediacompat/testlib/MediaControllerConstants.java @@ -0,0 +1,53 @@ +/* + * 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.support.mediacompat.testlib; + +/** + * Constants for testing the media controller. + */ +public class MediaControllerConstants { + + // MediaControllerCompat methods. + public static final int SEND_COMMAND = 201; + public static final int ADD_QUEUE_ITEM = 202; + public static final int ADD_QUEUE_ITEM_WITH_INDEX = 203; + public static final int REMOVE_QUEUE_ITEM = 204; + + // TransportControls methods. + public static final int PLAY = 301; + public static final int PAUSE = 302; + public static final int STOP = 303; + public static final int FAST_FORWARD = 304; + public static final int REWIND = 305; + public static final int SKIP_TO_PREVIOUS = 306; + public static final int SKIP_TO_NEXT = 307; + public static final int SEEK_TO = 308; + public static final int SET_RATING = 309; + public static final int PLAY_FROM_MEDIA_ID = 310; + public static final int PLAY_FROM_SEARCH = 311; + public static final int PLAY_FROM_URI = 312; + public static final int SEND_CUSTOM_ACTION = 313; + public static final int SEND_CUSTOM_ACTION_PARCELABLE = 314; + public static final int SKIP_TO_QUEUE_ITEM = 315; + public static final int PREPARE = 316; + public static final int PREPARE_FROM_MEDIA_ID = 317; + public static final int PREPARE_FROM_SEARCH = 318; + public static final int PREPARE_FROM_URI = 319; + public static final int SET_CAPTIONING_ENABLED = 320; + public static final int SET_REPEAT_MODE = 321; + public static final int SET_SHUFFLE_MODE = 322; +} diff --git a/android/support/mediacompat/testlib/MediaSessionConstants.java b/android/support/mediacompat/testlib/MediaSessionConstants.java new file mode 100644 index 00000000..79381e5e --- /dev/null +++ b/android/support/mediacompat/testlib/MediaSessionConstants.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 android.support.mediacompat.testlib; + +/** + * Constants for testing the media session. + */ +public class MediaSessionConstants { + + // MediaSessionCompat methods. + public static final int SET_EXTRAS = 101; + public static final int SET_FLAGS = 102; + public static final int SET_METADATA = 103; + public static final int SET_PLAYBACK_STATE = 104; + public static final int SET_QUEUE = 105; + public static final int SET_QUEUE_TITLE = 106; + public static final int SET_SESSION_ACTIVITY = 107; + public static final int SET_CAPTIONING_ENABLED = 108; + public static final int SET_REPEAT_MODE = 109; + public static final int SET_SHUFFLE_MODE = 110; + public static final int SEND_SESSION_EVENT = 112; + public static final int SET_ACTIVE = 113; + public static final int RELEASE = 114; + public static final int SET_PLAYBACK_TO_LOCAL = 115; + public static final int SET_PLAYBACK_TO_REMOTE = 116; + public static final int SET_RATING_TYPE = 117; + + public static final String TEST_SESSION_TAG = "test-session-tag"; + public static final String SERVICE_PACKAGE_NAME = "android.support.mediacompat.service.test"; + public static final String TEST_KEY = "test-key"; + public static final String TEST_VALUE = "test-val"; + public static final String TEST_SESSION_EVENT = "test-session-event"; + public static final String TEST_COMMAND = "test-command"; + public static final int TEST_FLAGS = 5; + public static final int TEST_CURRENT_VOLUME = 10; + public static final int TEST_MAX_VOLUME = 11; + public static final long TEST_QUEUE_ID_1 = 10L; + public static final long TEST_QUEUE_ID_2 = 20L; + public static final String TEST_MEDIA_ID_1 = "media_id_1"; + public static final String TEST_MEDIA_ID_2 = "media_id_2"; + public static final long TEST_ACTION = 55L; + + public static final int TEST_ERROR_CODE = 0x3; + public static final String TEST_ERROR_MSG = "test-error-msg"; +} diff --git a/android/support/transition/AutoTransition.java b/android/support/transition/AutoTransition.java index 02b49e26..bf39c3c3 100644 --- a/android/support/transition/AutoTransition.java +++ b/android/support/transition/AutoTransition.java @@ -45,9 +45,9 @@ public class AutoTransition extends TransitionSet { private void init() { setOrdering(ORDERING_SEQUENTIAL); - addTransition(new Fade(Fade.OUT)). - addTransition(new ChangeBounds()). - addTransition(new Fade(Fade.IN)); + addTransition(new Fade(Fade.OUT)) + .addTransition(new ChangeBounds()) + .addTransition(new Fade(Fade.IN)); } } diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java index 81431972..af37f77a 100644 --- a/android/support/v17/leanback/widget/GridLayoutManager.java +++ b/android/support/v17/leanback/widget/GridLayoutManager.java @@ -2240,10 +2240,24 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { focusToViewInLayout(hadFocus, scrollToFocus, -deltaPrimary, -deltaSecondary); appendVisibleItems(); prependVisibleItems(); - removeInvisibleViewsAtFront(); - removeInvisibleViewsAtEnd(); + // b/67370222: do not removeInvisibleViewsAtFront/End() in the loop, otherwise + // loop may bounce between scroll forward and scroll backward forever. Example: + // Assuming there are 19 items, child#18 and child#19 are both in RV, we are + // trying to focus to child#18 and there are 200px remaining scroll distance. + // 1 focusToViewInLayout() tries scroll forward 50 px to align focused child#18 on + // right edge, but there to compensate remaining scroll 200px, also scroll + // backward 200px, 150px pushes last child#19 out side of right edge. + // 2 removeInvisibleViewsAtEnd() remove last child#19, updateScrollLimits() + // invalidates scroll max + // 3 In next iteration, when scroll max/min is unknown, focusToViewInLayout() will + // align focused child#18 at center of screen. + // 4 Because #18 is aligned at center, appendVisibleItems() will fill child#19 to + // the right. + // 5 (back to 1 and loop forever) } while (mGrid.getFirstVisibleIndex() != oldFirstVisible || mGrid.getLastVisibleIndex() != oldLastVisible); + removeInvisibleViewsAtFront(); + removeInvisibleViewsAtEnd(); if (state.willRunPredictiveAnimations()) { fillScrapViewsInPostLayout(); diff --git a/android/support/v4/content/res/ResourcesCompat.java b/android/support/v4/content/res/ResourcesCompat.java index 4c70ce93..15b8ce9a 100644 --- a/android/support/v4/content/res/ResourcesCompat.java +++ b/android/support/v4/content/res/ResourcesCompat.java @@ -307,11 +307,11 @@ public final class ResourcesCompat { */ @RestrictTo(LIBRARY_GROUP) public static Typeface getFont(@NonNull Context context, @FontRes int id, TypedValue value, - int style) throws NotFoundException { + int style, @Nullable FontCallback fontCallback) throws NotFoundException { if (context.isRestricted()) { return null; } - return loadFont(context, id, value, style, null /* callback */, null /* handler */, + return loadFont(context, id, value, style, fontCallback, null /* handler */, true /* isXmlRequest */); } diff --git a/android/support/v4/media/RatingCompat.java b/android/support/v4/media/RatingCompat.java index b538cac4..e70243f8 100644 --- a/android/support/v4/media/RatingCompat.java +++ b/android/support/v4/media/RatingCompat.java @@ -18,6 +18,7 @@ package android.support.v4.media; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; +import android.media.Rating; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; @@ -326,25 +327,25 @@ public final class RatingCompat implements Parcelable { */ public static RatingCompat fromRating(Object ratingObj) { if (ratingObj != null && Build.VERSION.SDK_INT >= 19) { - final int ratingStyle = RatingCompatKitkat.getRatingStyle(ratingObj); + final int ratingStyle = ((Rating) ratingObj).getRatingStyle(); final RatingCompat rating; - if (RatingCompatKitkat.isRated(ratingObj)) { + if (((Rating) ratingObj).isRated()) { switch (ratingStyle) { case RATING_HEART: - rating = newHeartRating(RatingCompatKitkat.hasHeart(ratingObj)); + rating = newHeartRating(((Rating) ratingObj).hasHeart()); break; case RATING_THUMB_UP_DOWN: - rating = newThumbRating(RatingCompatKitkat.isThumbUp(ratingObj)); + rating = newThumbRating(((Rating) ratingObj).isThumbUp()); break; case RATING_3_STARS: case RATING_4_STARS: case RATING_5_STARS: rating = newStarRating(ratingStyle, - RatingCompatKitkat.getStarRating(ratingObj)); + ((Rating) ratingObj).getStarRating()); break; case RATING_PERCENTAGE: rating = newPercentageRating( - RatingCompatKitkat.getPercentRating(ratingObj)); + ((Rating) ratingObj).getPercentRating()); break; default: return null; @@ -372,25 +373,25 @@ public final class RatingCompat implements Parcelable { if (isRated()) { switch (mRatingStyle) { case RATING_HEART: - mRatingObj = RatingCompatKitkat.newHeartRating(hasHeart()); + mRatingObj = Rating.newHeartRating(hasHeart()); break; case RATING_THUMB_UP_DOWN: - mRatingObj = RatingCompatKitkat.newThumbRating(isThumbUp()); + mRatingObj = Rating.newThumbRating(isThumbUp()); break; case RATING_3_STARS: case RATING_4_STARS: case RATING_5_STARS: - mRatingObj = RatingCompatKitkat.newStarRating(mRatingStyle, + mRatingObj = Rating.newStarRating(mRatingStyle, getStarRating()); break; case RATING_PERCENTAGE: - mRatingObj = RatingCompatKitkat.newPercentageRating(getPercentRating()); + mRatingObj = Rating.newPercentageRating(getPercentRating()); break; default: return null; } } else { - mRatingObj = RatingCompatKitkat.newUnratedRating(mRatingStyle); + mRatingObj = Rating.newUnratedRating(mRatingStyle); } } return mRatingObj; diff --git a/android/support/v4/media/RatingCompatKitkat.java b/android/support/v4/media/RatingCompatKitkat.java deleted file mode 100644 index 1d3fa505..00000000 --- a/android/support/v4/media/RatingCompatKitkat.java +++ /dev/null @@ -1,67 +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.support.v4.media; - -import android.media.Rating; -import android.support.annotation.RequiresApi; - -@RequiresApi(19) -class RatingCompatKitkat { - public static Object newUnratedRating(int ratingStyle) { - return Rating.newUnratedRating(ratingStyle); - } - - public static Object newHeartRating(boolean hasHeart) { - return Rating.newHeartRating(hasHeart); - } - - public static Object newThumbRating(boolean thumbIsUp) { - return Rating.newThumbRating(thumbIsUp); - } - - public static Object newStarRating(int starRatingStyle, float starRating) { - return Rating.newStarRating(starRatingStyle, starRating); - } - - public static Object newPercentageRating(float percent) { - return Rating.newPercentageRating(percent); - } - - public static boolean isRated(Object ratingObj) { - return ((Rating)ratingObj).isRated(); - } - - public static int getRatingStyle(Object ratingObj) { - return ((Rating)ratingObj).getRatingStyle(); - } - - public static boolean hasHeart(Object ratingObj) { - return ((Rating)ratingObj).hasHeart(); - } - - public static boolean isThumbUp(Object ratingObj) { - return ((Rating)ratingObj).isThumbUp(); - } - - public static float getStarRating(Object ratingObj) { - return ((Rating)ratingObj).getStarRating(); - } - - public static float getPercentRating(Object ratingObj) { - return ((Rating)ratingObj).getPercentRating(); - } -} diff --git a/android/support/v4/provider/FontsContractCompat.java b/android/support/v4/provider/FontsContractCompat.java index 9ef1b0b0..09261869 100644 --- a/android/support/v4/provider/FontsContractCompat.java +++ b/android/support/v4/provider/FontsContractCompat.java @@ -303,6 +303,9 @@ public class FontsContractCompat { final ArrayList<ReplyCallback<TypefaceResult>> replies; synchronized (sLock) { replies = sPendingReplies.get(id); + if (replies == null) { + return; // Nobody requested replies. Do nothing. + } sPendingReplies.remove(id); } for (int i = 0; i < replies.size(); ++i) { diff --git a/android/support/v7/app/MediaRouteButton.java b/android/support/v7/app/MediaRouteButton.java index d3f7020b..fdbcf9ad 100644 --- a/android/support/v7/app/MediaRouteButton.java +++ b/android/support/v7/app/MediaRouteButton.java @@ -121,8 +121,7 @@ public class MediaRouteButton extends View { } public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { - super(MediaRouterThemeHelper.createThemedContext(context, defStyleAttr), attrs, - defStyleAttr); + super(MediaRouterThemeHelper.createThemedButtonContext(context), attrs, defStyleAttr); context = getContext(); mRouter = MediaRouter.getInstance(context); diff --git a/android/support/v7/app/MediaRouteChooserDialog.java b/android/support/v7/app/MediaRouteChooserDialog.java index 0ab2eb11..17364efb 100644 --- a/android/support/v7/app/MediaRouteChooserDialog.java +++ b/android/support/v7/app/MediaRouteChooserDialog.java @@ -92,10 +92,8 @@ public class MediaRouteChooserDialog extends AppCompatDialog { } public MediaRouteChooserDialog(Context context, int theme) { - // If we pass theme ID of 0 to AppCompatDialog, it will apply dialogTheme on the context, - // which may override our style settings. Passes our uppermost theme ID to prevent this. - super(MediaRouterThemeHelper.createThemedContext(context, theme), - theme == 0 ? MediaRouterThemeHelper.createThemeForDialog(context, theme) : theme); + super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, false), + MediaRouterThemeHelper.createThemedDialogStyle(context)); context = getContext(); mRouter = MediaRouter.getInstance(context); diff --git a/android/support/v7/app/MediaRouteControllerDialog.java b/android/support/v7/app/MediaRouteControllerDialog.java index 4b9a17a3..d89bf21e 100644 --- a/android/support/v7/app/MediaRouteControllerDialog.java +++ b/android/support/v7/app/MediaRouteControllerDialog.java @@ -201,12 +201,8 @@ public class MediaRouteControllerDialog extends AlertDialog { } public MediaRouteControllerDialog(Context context, int theme) { - // If we pass theme ID of 0 to AppCompatDialog, it will apply dialogTheme on the context, - // which may override our style settings. Passes our uppermost theme ID to prevent this. - super(MediaRouterThemeHelper.createThemedContext(context, - MediaRouterThemeHelper.getAlertDialogResolvedTheme(context, theme)), theme == 0 - ? MediaRouterThemeHelper.createThemeForDialog(context, MediaRouterThemeHelper - .getAlertDialogResolvedTheme(context, theme)) : theme); + super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, true), + MediaRouterThemeHelper.createThemedDialogStyle(context)); mContext = getContext(); mControllerCallback = new MediaControllerCallback(); diff --git a/android/support/v7/app/MediaRouterThemeHelper.java b/android/support/v7/app/MediaRouterThemeHelper.java index 9ef218e0..69e40ac7 100644 --- a/android/support/v7/app/MediaRouterThemeHelper.java +++ b/android/support/v7/app/MediaRouterThemeHelper.java @@ -42,47 +42,76 @@ final class MediaRouterThemeHelper { private MediaRouterThemeHelper() { } - /** - * Creates a themed context based on the explicit style resource or the parent context's default - * theme. - * <p> - * The theme which will be applied on top of the parent {@code context}'s theme is determined - * by the primary color defined in the given {@code style}, or in the parent {@code context}. + static Context createThemedButtonContext(Context context) { + // Apply base Media Router theme. + context = new ContextThemeWrapper(context, getRouterThemeId(context)); + + // Apply custom Media Router theme. + int style = getThemeResource(context, R.attr.mediaRouteTheme); + if (style != 0) { + context = new ContextThemeWrapper(context, style); + } + + return context; + } + + /* + * The following two methods are to be used in conjunction. They should be used to prepare + * the context and theme for a super class constructor (the latter method relies on the + * former method to properly prepare the context): + * super(context = createThemedDialogContext(context, theme), + * createThemedDialogStyle(context)); * - * @param context the parent context - * @param style the resource ID of the style against which to inflate this context, or - * {@code 0} to use the parent {@code context}'s default theme. - * @return The themed context. + * It will apply theme in the following order (style lookups will be done in reverse): + * 1) Current theme + * 2) Supplied theme + * 3) Base Media Router theme + * 4) Custom Media Router theme, if provided */ - static Context createThemedContext(Context context, int style) { - // First, apply dialog property overlay. - Context themedContext = - new ContextThemeWrapper(context, getStyledRouterThemeId(context, style)); - int customizedThemeId = getThemeResource(context, R.attr.mediaRouteTheme); - return customizedThemeId == 0 ? themedContext - : new ContextThemeWrapper(themedContext, customizedThemeId); + static Context createThemedDialogContext(Context context, int theme, boolean alertDialog) { + // 1) Current theme is already applied to the context + + // 2) If no theme is supplied, look it up from the context (dialogTheme/alertDialogTheme) + if (theme == 0) { + theme = getThemeResource(context, !alertDialog + ? android.support.v7.appcompat.R.attr.dialogTheme + : android.support.v7.appcompat.R.attr.alertDialogTheme); + } + // Apply it + context = new ContextThemeWrapper(context, theme); + + // 3) If a custom Media Router theme is provided then apply the base theme + if (getThemeResource(context, R.attr.mediaRouteTheme) != 0) { + context = new ContextThemeWrapper(context, getRouterThemeId(context)); + } + + return context; } + // This method should be used in conjunction with the previous method. + static int createThemedDialogStyle(Context context) { + // 4) Apply the custom Media Router theme + int theme = getThemeResource(context, R.attr.mediaRouteTheme); + if (theme == 0) { + // 3) No custom MediaRouther theme was provided so apply the base theme instead + theme = getRouterThemeId(context); + } - /** - * Creates the theme resource ID intended to be used by dialogs. - */ - static int createThemeForDialog(Context context, int style) { - int customizedThemeId = getThemeResource(context, R.attr.mediaRouteTheme); - return customizedThemeId != 0 ? customizedThemeId : getStyledRouterThemeId(context, style); + return theme; } + // END. Previous two methods should be used in conjunction. - public static int getThemeResource(Context context, int attr) { + static int getThemeResource(Context context, int attr) { TypedValue value = new TypedValue(); return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0; } - public static float getDisabledAlpha(Context context) { + static float getDisabledAlpha(Context context) { TypedValue value = new TypedValue(); return context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true) ? value.getFloat() : 0.5f; } - public static @ControllerColorType int getControllerColor(Context context, int style) { + static @ControllerColorType int getControllerColor(Context context, int style) { int primaryColor = getThemeColor(context, style, android.support.v7.appcompat.R.attr.colorPrimary); if (ColorUtils.calculateContrast(COLOR_WHITE_ON_DARK_BACKGROUND, primaryColor) @@ -92,7 +121,7 @@ final class MediaRouterThemeHelper { return COLOR_DARK_ON_LIGHT_BACKGROUND; } - public static int getButtonTextColor(Context context) { + static int getButtonTextColor(Context context) { int primaryColor = getThemeColor(context, 0, android.support.v7.appcompat.R.attr.colorPrimary); int backgroundColor = getThemeColor(context, 0, android.R.attr.colorBackground); @@ -104,7 +133,7 @@ final class MediaRouterThemeHelper { return primaryColor; } - public static void setMediaControlsBackgroundColor( + static void setMediaControlsBackgroundColor( Context context, View mainControls, View groupControls, boolean hasGroup) { int primaryColor = getThemeColor(context, 0, android.support.v7.appcompat.R.attr.colorPrimary); @@ -124,7 +153,7 @@ final class MediaRouterThemeHelper { groupControls.setTag(primaryDarkColor); } - public static void setVolumeSliderColor( + static void setVolumeSliderColor( Context context, MediaRouteVolumeSlider volumeSlider, View backgroundView) { int controllerColor = getControllerColor(context, 0); if (Color.alpha(controllerColor) != 0xFF) { @@ -136,23 +165,10 @@ final class MediaRouterThemeHelper { volumeSlider.setColor(controllerColor); } - // This is copied from {@link AlertDialog#resolveDialogTheme} to pre-evaluate theme in advance. - public static int getAlertDialogResolvedTheme(Context context, int themeResId) { - if (themeResId >= 0x01000000) { // start of real resource IDs. - return themeResId; - } else { - TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute( - android.support.v7.appcompat.R.attr.alertDialogTheme, outValue, true); - return outValue.resourceId; - } - } - private static boolean isLightTheme(Context context) { TypedValue value = new TypedValue(); - return context.getTheme().resolveAttribute( - android.support.v7.appcompat.R.attr.isLightTheme, value, true) - && value.data != 0; + return context.getTheme().resolveAttribute(android.support.v7.appcompat.R.attr.isLightTheme, + value, true) && value.data != 0; } private static int getThemeColor(Context context, int style, int attr) { @@ -173,16 +189,16 @@ final class MediaRouterThemeHelper { return value.data; } - private static int getStyledRouterThemeId(Context context, int style) { + private static int getRouterThemeId(Context context) { int themeId; if (isLightTheme(context)) { - if (getControllerColor(context, style) == COLOR_DARK_ON_LIGHT_BACKGROUND) { + if (getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) { themeId = R.style.Theme_MediaRouter_Light; } else { themeId = R.style.Theme_MediaRouter_Light_DarkControlPanel; } } else { - if (getControllerColor(context, style) == COLOR_DARK_ON_LIGHT_BACKGROUND) { + if (getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) { themeId = R.style.Theme_MediaRouter_LightControlPanel; } else { themeId = R.style.Theme_MediaRouter; diff --git a/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.java b/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.java deleted file mode 100644 index aab74172..00000000 --- a/android/support/v7/recyclerview/extensions/ListAdapterHelperTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.v7.recyclerview.extensions; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; - -import android.arch.paging.TestExecutor; -import android.support.annotation.NonNull; -import android.support.test.filters.SmallTest; -import android.support.v7.util.ListUpdateCallback; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.util.Arrays; - -@SmallTest -@RunWith(JUnit4.class) -public class ListAdapterHelperTest { - private TestExecutor mMainThread = new TestExecutor(); - private TestExecutor mBackgroundThread = new TestExecutor(); - - - private static final DiffCallback<String> STRING_DIFF_CALLBACK = new DiffCallback<String>() { - @Override - public boolean areItemsTheSame(@NonNull String oldItem, @NonNull String newItem) { - return oldItem.equals(newItem); - } - - @Override - public boolean areContentsTheSame(@NonNull String oldItem, @NonNull String newItem) { - return oldItem.equals(newItem); - } - }; - - private static final ListUpdateCallback IGNORE_CALLBACK = new ListUpdateCallback() { - @Override - public void onInserted(int position, int count) { - } - - @Override - public void onRemoved(int position, int count) { - } - - @Override - public void onMoved(int fromPosition, int toPosition) { - } - - @Override - public void onChanged(int position, int count, Object payload) { - } - }; - - - private <T> ListAdapterHelper<T> createHelper( - ListUpdateCallback listUpdateCallback, DiffCallback<T> diffCallback) { - return new ListAdapterHelper<T>(listUpdateCallback, - new ListAdapterConfig.Builder<T>() - .setDiffCallback(diffCallback) - .setMainThreadExecutor(mMainThread) - .setBackgroundThreadExecutor(mBackgroundThread) - .build()); - } - - @Test - public void initialState() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - ListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - assertEquals(0, helper.getItemCount()); - verifyZeroInteractions(callback); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getEmpty() { - ListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.getItem(0); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getNegative() { - ListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.setList(Arrays.asList("a", "b")); - helper.getItem(-1); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getPastEnd() { - ListAdapterHelper<String> helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK); - helper.setList(Arrays.asList("a", "b")); - helper.getItem(2); - } - - @Test - public void setListSimple() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - ListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - - helper.setList(Arrays.asList("a", "b")); - - assertEquals(2, helper.getItemCount()); - assertEquals("a", helper.getItem(0)); - assertEquals("b", helper.getItem(1)); - - verify(callback).onInserted(0, 2); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - } - - @Test - public void setListUpdate() { - ListUpdateCallback callback = mock(ListUpdateCallback.class); - ListAdapterHelper<String> helper = createHelper(callback, STRING_DIFF_CALLBACK); - - // initial list (immediate) - helper.setList(Arrays.asList("a", "b")); - verify(callback).onInserted(0, 2); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - - // update (deferred) - helper.setList(Arrays.asList("a", "b", "c")); - verifyNoMoreInteractions(callback); - drain(); - verify(callback).onInserted(2, 1); - verifyNoMoreInteractions(callback); - - // clear (immediate) - helper.setList(null); - verify(callback).onRemoved(0, 3); - verifyNoMoreInteractions(callback); - drain(); - verifyNoMoreInteractions(callback); - - } - - private void drain() { - boolean executed; - do { - executed = mBackgroundThread.executeAll(); - executed |= mMainThread.executeAll(); - } while (executed); - } -} diff --git a/android/support/v7/util/DiffUtil.java b/android/support/v7/util/DiffUtil.java index 6302666f..ebc33f31 100644 --- a/android/support/v7/util/DiffUtil.java +++ b/android/support/v7/util/DiffUtil.java @@ -16,6 +16,7 @@ package android.support.v7.util; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; @@ -348,6 +349,72 @@ public class DiffUtil { } /** + * Callback for calculating the diff between two non-null items in a list. + * <p> + * {@link Callback} serves two roles - list indexing, and item diffing. ItemCallback handles + * just the second of these, which allows separation of code that indexes into an array or List + * from the presentation-layer and content specific diffing code. + * + * @param <T> Type of items to compare. + */ + public abstract static class ItemCallback<T> { + /** + * Called to check whether two objects represent the same item. + * <p> + * For example, if your items have unique ids, this method should check their id equality. + * + * @param oldItem The item in the old list. + * @param newItem The item in the new list. + * @return True if the two items represent the same object or false if they are different. + * + * @see Callback#areItemsTheSame(int, int) + */ + public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem); + + /** + * Called to check whether two items have the same data. + * <p> + * This information is used to detect if the contents of an item have changed. + * <p> + * This method to check equality instead of {@link Object#equals(Object)} so that you can + * change its behavior depending on your UI. + * <p> + * For example, if you are using DiffUtil with a + * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should + * return whether the items' visual representations are the same. + * <p> + * This method is called only if {@link #areItemsTheSame(T, T)} returns {@code true} for + * these items. + * + * @param oldItem The item in the old list. + * @param newItem The item in the new list. + * @return True if the contents of the items are the same or false if they are different. + * + * @see Callback#areContentsTheSame(int, int) + */ + public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem); + + /** + * When {@link #areItemsTheSame(T, T)} returns {@code true} for two items and + * {@link #areContentsTheSame(T, T)} returns false for them, this method is called to + * get a payload about the change. + * <p> + * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the + * particular field that changed in the item and your + * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that + * information to run the correct animation. + * <p> + * Default implementation returns {@code null}. + * + * @see Callback#getChangePayload(int, int) + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) { + return null; + } + } + + /** * Snakes represent a match between two lists. It is optionally prefixed or postfixed with an * add or remove operation. See the Myers' paper for details. */ diff --git a/android/support/v7/widget/AppCompatTextHelper.java b/android/support/v7/widget/AppCompatTextHelper.java index 51510aa2..fa6196f5 100644 --- a/android/support/v7/widget/AppCompatTextHelper.java +++ b/android/support/v7/widget/AppCompatTextHelper.java @@ -29,6 +29,7 @@ import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; +import android.support.v4.content.res.ResourcesCompat; import android.support.v4.widget.TextViewCompat; import android.support.v7.appcompat.R; import android.text.method.PasswordTransformationMethod; @@ -36,6 +37,8 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.widget.TextView; +import java.lang.ref.WeakReference; + @RequiresApi(9) class AppCompatTextHelper { @@ -63,6 +66,7 @@ class AppCompatTextHelper { private int mStyle = Typeface.NORMAL; private Typeface mFontTypeface; + private boolean mAsyncFontPending; AppCompatTextHelper(TextView view) { mView = view; @@ -213,8 +217,23 @@ class AppCompatTextHelper { ? R.styleable.TextAppearance_android_fontFamily : R.styleable.TextAppearance_fontFamily; if (!context.isRestricted()) { + final WeakReference<TextView> textViewWeak = new WeakReference<>(mView); + ResourcesCompat.FontCallback replyCallback = new ResourcesCompat.FontCallback() { + @Override + public void onFontRetrieved(@NonNull Typeface typeface) { + onAsyncTypefaceReceived(textViewWeak, typeface); + } + + @Override + public void onFontRetrievalFailed(int reason) { + // Do nothing. + } + }; try { - mFontTypeface = a.getFont(fontFamilyId, mStyle); + // Note the callback will be triggered on the UI thread. + mFontTypeface = a.getFont(fontFamilyId, mStyle, replyCallback); + // If this call gave us an immediate result, ignore any pending callbacks. + mAsyncFontPending = mFontTypeface == null; } catch (UnsupportedOperationException | Resources.NotFoundException e) { // Expected if it is not a font resource. } @@ -222,12 +241,16 @@ class AppCompatTextHelper { if (mFontTypeface == null) { // Try with String. This is done by TextView JB+, but fails in ICS String fontFamilyName = a.getString(fontFamilyId); - mFontTypeface = Typeface.create(fontFamilyName, mStyle); + if (fontFamilyName != null) { + mFontTypeface = Typeface.create(fontFamilyName, mStyle); + } } return; } if (a.hasValue(R.styleable.TextAppearance_android_typeface)) { + // Ignore previous pending fonts + mAsyncFontPending = false; int typefaceIndex = a.getInt(R.styleable.TextAppearance_android_typeface, SANS); switch (typefaceIndex) { case SANS: @@ -245,6 +268,16 @@ class AppCompatTextHelper { } } + private void onAsyncTypefaceReceived(WeakReference<TextView> textViewWeak, Typeface typeface) { + if (mAsyncFontPending) { + mFontTypeface = typeface; + final TextView textView = textViewWeak.get(); + if (textView != null) { + textView.setTypeface(typeface, mStyle); + } + } + } + void onSetTextAppearance(Context context, int resId) { final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, resId, R.styleable.TextAppearance); diff --git a/android/support/v7/widget/RecyclerView.java b/android/support/v7/widget/RecyclerView.java index 7009733c..4bc17a86 100644 --- a/android/support/v7/widget/RecyclerView.java +++ b/android/support/v7/widget/RecyclerView.java @@ -6408,16 +6408,16 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro } void markKnownViewsInvalid() { - if (mAdapter != null && mAdapter.hasStableIds()) { - final int cachedCount = mCachedViews.size(); - for (int i = 0; i < cachedCount; i++) { - final ViewHolder holder = mCachedViews.get(i); - if (holder != null) { - holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); - holder.addChangePayload(null); - } + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); + holder.addChangePayload(null); } - } else { + } + + if (mAdapter == null || !mAdapter.hasStableIds()) { // we cannot re-use cached views in this case. Recycle them all recycleAndClearCachedViews(); } diff --git a/android/support/v7/widget/TintTypedArray.java b/android/support/v7/widget/TintTypedArray.java index 22709551..384c4615 100644 --- a/android/support/v7/widget/TintTypedArray.java +++ b/android/support/v7/widget/TintTypedArray.java @@ -106,7 +106,8 @@ public class TintTypedArray { * not a font resource. */ @Nullable - public Typeface getFont(@StyleableRes int index, int style) { + public Typeface getFont(@StyleableRes int index, int style, + @Nullable ResourcesCompat.FontCallback fontCallback) { final int resourceId = mWrapped.getResourceId(index, 0); if (resourceId == 0) { return null; @@ -114,7 +115,7 @@ public class TintTypedArray { if (mTypedValue == null) { mTypedValue = new TypedValue(); } - return ResourcesCompat.getFont(mContext, resourceId, mTypedValue, style); + return ResourcesCompat.getFont(mContext, resourceId, mTypedValue, style, fontCallback); } public int length() { diff --git a/android/support/wear/ambient/AmbientMode.java b/android/support/wear/ambient/AmbientMode.java index 7fbbbb3f..db53dfc1 100644 --- a/android/support/wear/ambient/AmbientMode.java +++ b/android/support/wear/ambient/AmbientMode.java @@ -22,7 +22,6 @@ import android.content.Context; import android.os.Bundle; import android.support.annotation.CallSuper; import android.support.annotation.VisibleForTesting; -import android.util.Log; import com.google.android.wearable.compat.WearableActivityController; @@ -263,20 +262,6 @@ public final class AmbientMode extends Fragment { AmbientController() {} /** - * Sets whether this activity's task should be moved to the front when the system exits - * ambient mode. If true, the activity's task may be moved to the front if it was the last - * activity to be running when ambient started, depending on how much time the system spent - * in ambient mode. - */ - public void setAutoResumeEnabled(boolean enabled) { - if (mDelegate != null) { - mDelegate.setAutoResumeEnabled(enabled); - } else { - Log.w(TAG, "The fragment is not yet fully initialized, this call is a no-op"); - } - } - - /** * @return {@code true} if the activity is currently in ambient. */ public boolean isAmbient() { diff --git a/android/system/ErrnoException.java b/android/system/ErrnoException.java index 90155c89..e60ac8f3 100644 --- a/android/system/ErrnoException.java +++ b/android/system/ErrnoException.java @@ -26,57 +26,57 @@ import libcore.io.Libcore; * callers need to adjust their behavior based on the exact failure. */ public final class ErrnoException extends Exception { - private final String functionName; + private final String functionName; - /** - * The errno value, for comparison with the {@code E} constants in {@link OsConstants}. - */ - public final int errno; + /** + * The errno value, for comparison with the {@code E} constants in {@link OsConstants}. + */ + public final int errno; - /** - * Constructs an instance with the given function name and errno value. - */ - public ErrnoException(String functionName, int errno) { - this.functionName = functionName; - this.errno = errno; - } + /** + * Constructs an instance with the given function name and errno value. + */ + public ErrnoException(String functionName, int errno) { + this.functionName = functionName; + this.errno = errno; + } - /** - * Constructs an instance with the given function name, errno value, and cause. - */ - public ErrnoException(String functionName, int errno, Throwable cause) { - super(cause); - this.functionName = functionName; - this.errno = errno; - } + /** + * Constructs an instance with the given function name, errno value, and cause. + */ + public ErrnoException(String functionName, int errno, Throwable cause) { + super(cause); + this.functionName = functionName; + this.errno = errno; + } - /** - * Converts the stashed function name and errno value to a human-readable string. - * We do this here rather than in the constructor so that callers only pay for - * this if they need it. - */ - @Override public String getMessage() { - String errnoName = OsConstants.errnoName(errno); - if (errnoName == null) { - errnoName = "errno " + errno; + /** + * Converts the stashed function name and errno value to a human-readable string. + * We do this here rather than in the constructor so that callers only pay for + * this if they need it. + */ + @Override public String getMessage() { + String errnoName = OsConstants.errnoName(errno); + if (errnoName == null) { + errnoName = "errno " + errno; + } + String description = Libcore.os.strerror(errno); + return functionName + " failed: " + errnoName + " (" + description + ")"; } - String description = Libcore.os.strerror(errno); - return functionName + " failed: " + errnoName + " (" + description + ")"; - } - /** - * @hide - internal use only. - */ - public IOException rethrowAsIOException() throws IOException { - IOException newException = new IOException(getMessage()); - newException.initCause(this); - throw newException; - } + /** + * @hide - internal use only. + */ + public IOException rethrowAsIOException() throws IOException { + IOException newException = new IOException(getMessage()); + newException.initCause(this); + throw newException; + } - /** - * @hide - internal use only. - */ - public SocketException rethrowAsSocketException() throws SocketException { - throw new SocketException(getMessage(), this); - } + /** + * @hide - internal use only. + */ + public SocketException rethrowAsSocketException() throws SocketException { + throw new SocketException(getMessage(), this); + } } diff --git a/android/system/GaiException.java b/android/system/GaiException.java index dc105668..182cc3eb 100644 --- a/android/system/GaiException.java +++ b/android/system/GaiException.java @@ -27,57 +27,57 @@ import libcore.io.Libcore; * @hide */ public final class GaiException extends RuntimeException { - private final String functionName; + private final String functionName; - /** - * The native error value, for comparison with the {@code GAI_} constants in {@link OsConstants}. - */ - public final int error; + /** + * The native error value, for comparison with the {@code GAI_} constants in {@link OsConstants}. + */ + public final int error; - /** - * Constructs an instance with the given function name and error value. - */ - public GaiException(String functionName, int error) { - this.functionName = functionName; - this.error = error; - } + /** + * Constructs an instance with the given function name and error value. + */ + public GaiException(String functionName, int error) { + this.functionName = functionName; + this.error = error; + } - /** - * Constructs an instance with the given function name, error value, and cause. - */ - public GaiException(String functionName, int error, Throwable cause) { - super(cause); - this.functionName = functionName; - this.error = error; - } + /** + * Constructs an instance with the given function name, error value, and cause. + */ + public GaiException(String functionName, int error, Throwable cause) { + super(cause); + this.functionName = functionName; + this.error = error; + } - /** - * Converts the stashed function name and error value to a human-readable string. - * We do this here rather than in the constructor so that callers only pay for - * this if they need it. - */ - @Override public String getMessage() { - String gaiName = OsConstants.gaiName(error); - if (gaiName == null) { - gaiName = "GAI_ error " + error; + /** + * Converts the stashed function name and error value to a human-readable string. + * We do this here rather than in the constructor so that callers only pay for + * this if they need it. + */ + @Override public String getMessage() { + String gaiName = OsConstants.gaiName(error); + if (gaiName == null) { + gaiName = "GAI_ error " + error; + } + String description = Libcore.os.gai_strerror(error); + return functionName + " failed: " + gaiName + " (" + description + ")"; } - String description = Libcore.os.gai_strerror(error); - return functionName + " failed: " + gaiName + " (" + description + ")"; - } - /** - * @hide - internal use only. - */ - public UnknownHostException rethrowAsUnknownHostException(String detailMessage) throws UnknownHostException { - UnknownHostException newException = new UnknownHostException(detailMessage); - newException.initCause(this); - throw newException; - } + /** + * @hide - internal use only. + */ + public UnknownHostException rethrowAsUnknownHostException(String detailMessage) throws UnknownHostException { + UnknownHostException newException = new UnknownHostException(detailMessage); + newException.initCause(this); + throw newException; + } - /** - * @hide - internal use only. - */ - public UnknownHostException rethrowAsUnknownHostException() throws UnknownHostException { - throw rethrowAsUnknownHostException(getMessage()); - } + /** + * @hide - internal use only. + */ + public UnknownHostException rethrowAsUnknownHostException() throws UnknownHostException { + throw rethrowAsUnknownHostException(getMessage()); + } } diff --git a/android/system/Os.java b/android/system/Os.java index 8e312ddc..2dabae2f 100644 --- a/android/system/Os.java +++ b/android/system/Os.java @@ -35,592 +35,597 @@ import libcore.io.Libcore; * <p>The corresponding constants can be found in {@link OsConstants}. */ public final class Os { - private Os() {} - - /** - * See <a href="http://man7.org/linux/man-pages/man2/accept.2.html">accept(2)</a>. - */ - public static FileDescriptor accept(FileDescriptor fd, InetSocketAddress peerAddress) throws ErrnoException, SocketException { return Libcore.os.accept(fd, peerAddress); } - - /** - * TODO Change the public API by removing the overload above and unhiding this version. - * @hide - */ - public static FileDescriptor accept(FileDescriptor fd, SocketAddress peerAddress) throws ErrnoException, SocketException { return Libcore.os.accept(fd, peerAddress); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/access.2.html">access(2)</a>. - */ - public static boolean access(String path, int mode) throws ErrnoException { return Libcore.os.access(path, mode); } - - /** @hide */ public static InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException { return Libcore.os.android_getaddrinfo(node, hints, netId); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/bind.2.html">bind(2)</a>. - */ - public static void bind(FileDescriptor fd, InetAddress address, int port) throws ErrnoException, SocketException { Libcore.os.bind(fd, address, port); } - - /** @hide */ public static void bind(FileDescriptor fd, SocketAddress address) throws ErrnoException, SocketException { Libcore.os.bind(fd, address); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/capget.2.html">capget(2)</a>. - * - * @hide - */ - public static StructCapUserData[] capget(StructCapUserHeader hdr) throws ErrnoException { - return Libcore.os.capget(hdr); - } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/capset.2.html">capset(2)</a>. - * - * @hide - */ - public static void capset(StructCapUserHeader hdr, StructCapUserData[] data) - throws ErrnoException { - Libcore.os.capset(hdr, data); - } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/chmod.2.html">chmod(2)</a>. - */ - public static void chmod(String path, int mode) throws ErrnoException { Libcore.os.chmod(path, mode); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/chown.2.html">chown(2)</a>. - */ - public static void chown(String path, int uid, int gid) throws ErrnoException { Libcore.os.chown(path, uid, gid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/close.2.html">close(2)</a>. - */ - public static void close(FileDescriptor fd) throws ErrnoException { Libcore.os.close(fd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/connect.2.html">connect(2)</a>. - */ - public static void connect(FileDescriptor fd, InetAddress address, int port) throws ErrnoException, SocketException { Libcore.os.connect(fd, address, port); } - - /** @hide */ public static void connect(FileDescriptor fd, SocketAddress address) throws ErrnoException, SocketException { Libcore.os.connect(fd, address); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/dup.2.html">dup(2)</a>. - */ - public static FileDescriptor dup(FileDescriptor oldFd) throws ErrnoException { return Libcore.os.dup(oldFd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/dup2.2.html">dup2(2)</a>. - */ - public static FileDescriptor dup2(FileDescriptor oldFd, int newFd) throws ErrnoException { return Libcore.os.dup2(oldFd, newFd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/environ.3.html">environ(3)</a>. - */ - public static String[] environ() { return Libcore.os.environ(); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/execv.2.html">execv(2)</a>. - */ - public static void execv(String filename, String[] argv) throws ErrnoException { Libcore.os.execv(filename, argv); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/execve.2.html">execve(2)</a>. - */ - public static void execve(String filename, String[] argv, String[] envp) throws ErrnoException { Libcore.os.execve(filename, argv, envp); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/fchmod.2.html">fchmod(2)</a>. - */ - public static void fchmod(FileDescriptor fd, int mode) throws ErrnoException { Libcore.os.fchmod(fd, mode); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/fchown.2.html">fchown(2)</a>. - */ - public static void fchown(FileDescriptor fd, int uid, int gid) throws ErrnoException { Libcore.os.fchown(fd, uid, gid); } - - /** @hide */ public static int fcntlFlock(FileDescriptor fd, int cmd, StructFlock arg) throws ErrnoException, InterruptedIOException { return Libcore.os.fcntlFlock(fd, cmd, arg); } - /** @hide */ public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException { return Libcore.os.fcntlInt(fd, cmd, arg); } - /** @hide */ public static int fcntlVoid(FileDescriptor fd, int cmd) throws ErrnoException { return Libcore.os.fcntlVoid(fd, cmd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/fdatasync.2.html">fdatasync(2)</a>. - */ - public static void fdatasync(FileDescriptor fd) throws ErrnoException { Libcore.os.fdatasync(fd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/fstat.2.html">fstat(2)</a>. - */ - public static StructStat fstat(FileDescriptor fd) throws ErrnoException { return Libcore.os.fstat(fd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/fstatvfs.2.html">fstatvfs(2)</a>. - */ - public static StructStatVfs fstatvfs(FileDescriptor fd) throws ErrnoException { return Libcore.os.fstatvfs(fd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/fsync.2.html">fsync(2)</a>. - */ - public static void fsync(FileDescriptor fd) throws ErrnoException { Libcore.os.fsync(fd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/ftruncate.2.html">ftruncate(2)</a>. - */ - public static void ftruncate(FileDescriptor fd, long length) throws ErrnoException { Libcore.os.ftruncate(fd, length); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/gai_strerror.3.html">gai_strerror(3)</a>. - */ - public static String gai_strerror(int error) { return Libcore.os.gai_strerror(error); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getegid.2.html">getegid(2)</a>. - */ - public static int getegid() { return Libcore.os.getegid(); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/geteuid.2.html">geteuid(2)</a>. - */ - public static int geteuid() { return Libcore.os.geteuid(); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getgid.2.html">getgid(2)</a>. - */ - public static int getgid() { return Libcore.os.getgid(); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/getenv.3.html">getenv(3)</a>. - */ - public static String getenv(String name) { return Libcore.os.getenv(name); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/getifaddrs.3.html">getifaddrs(3)</a>. - */ - /** @hide */ public static StructIfaddrs[] getifaddrs() throws ErrnoException { return Libcore.os.getifaddrs(); } - - /** @hide */ public static String getnameinfo(InetAddress address, int flags) throws GaiException { return Libcore.os.getnameinfo(address, flags); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getpeername.2.html">getpeername(2)</a>. - */ - public static SocketAddress getpeername(FileDescriptor fd) throws ErrnoException { return Libcore.os.getpeername(fd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getpgid.2.html">getpgid(2)</a>. - */ - /** @hide */ public static int getpgid(int pid) throws ErrnoException { return Libcore.os.getpgid(pid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getpid.2.html">getpid(2)</a>. - */ - public static int getpid() { return Libcore.os.getpid(); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getppid.2.html">getppid(2)</a>. - */ - public static int getppid() { return Libcore.os.getppid(); } - - /** @hide */ public static StructPasswd getpwnam(String name) throws ErrnoException { return Libcore.os.getpwnam(name); } - - /** @hide */ public static StructPasswd getpwuid(int uid) throws ErrnoException { return Libcore.os.getpwuid(uid); } - - /** @hide */ public static StructRlimit getrlimit(int resource) throws ErrnoException { return Libcore.os.getrlimit(resource); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getsockname.2.html">getsockname(2)</a>. - */ - public static SocketAddress getsockname(FileDescriptor fd) throws ErrnoException { return Libcore.os.getsockname(fd); } - - /** @hide */ public static int getsockoptByte(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptByte(fd, level, option); } - /** @hide */ public static InetAddress getsockoptInAddr(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptInAddr(fd, level, option); } - /** @hide */ public static int getsockoptInt(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptInt(fd, level, option); } - /** @hide */ public static StructLinger getsockoptLinger(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptLinger(fd, level, option); } - /** @hide */ public static StructTimeval getsockoptTimeval(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptTimeval(fd, level, option); } - /** @hide */ public static StructUcred getsockoptUcred(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptUcred(fd, level, option); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/gettid.2.html">gettid(2)</a>. - */ - public static int gettid() { return Libcore.os.gettid(); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getuid.2.html">getuid(2)</a>. - */ - public static int getuid() { return Libcore.os.getuid(); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/getxattr.2.html">getxattr(2)</a> - */ - public static byte[] getxattr(String path, String name) throws ErrnoException { return Libcore.os.getxattr(path, name); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/if_indextoname.3.html">if_indextoname(3)</a>. - */ - public static String if_indextoname(int index) { return Libcore.os.if_indextoname(index); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/if_nametoindex.3.html">if_nametoindex(3)</a>. - */ - public static int if_nametoindex(String name) { return Libcore.os.if_nametoindex(name); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/inet_pton.3.html">inet_pton(3)</a>. - */ - public static InetAddress inet_pton(int family, String address) { return Libcore.os.inet_pton(family, address); } - - /** @hide */ public static InetAddress ioctlInetAddress(FileDescriptor fd, int cmd, String interfaceName) throws ErrnoException { return Libcore.os.ioctlInetAddress(fd, cmd, interfaceName); } - /** @hide */ public static int ioctlInt(FileDescriptor fd, int cmd, MutableInt arg) throws ErrnoException { return Libcore.os.ioctlInt(fd, cmd, arg); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/isatty.3.html">isatty(3)</a>. - */ - public static boolean isatty(FileDescriptor fd) { return Libcore.os.isatty(fd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/kill.2.html">kill(2)</a>. - */ - public static void kill(int pid, int signal) throws ErrnoException { Libcore.os.kill(pid, signal); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/lchown.2.html">lchown(2)</a>. - */ - public static void lchown(String path, int uid, int gid) throws ErrnoException { Libcore.os.lchown(path, uid, gid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/link.2.html">link(2)</a>. - */ - public static void link(String oldPath, String newPath) throws ErrnoException { Libcore.os.link(oldPath, newPath); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/listen.2.html">listen(2)</a>. - */ - public static void listen(FileDescriptor fd, int backlog) throws ErrnoException { Libcore.os.listen(fd, backlog); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/listxattr.2.html">listxattr(2)</a> - */ - public static String[] listxattr(String path) throws ErrnoException { return Libcore.os.listxattr(path); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/lseek.2.html">lseek(2)</a>. - */ - public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException { return Libcore.os.lseek(fd, offset, whence); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/lstat.2.html">lstat(2)</a>. - */ - public static StructStat lstat(String path) throws ErrnoException { return Libcore.os.lstat(path); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/mincore.2.html">mincore(2)</a>. - */ - public static void mincore(long address, long byteCount, byte[] vector) throws ErrnoException { Libcore.os.mincore(address, byteCount, vector); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/mkdir.2.html">mkdir(2)</a>. - */ - public static void mkdir(String path, int mode) throws ErrnoException { Libcore.os.mkdir(path, mode); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/mkfifo.3.html">mkfifo(3)</a>. - */ - public static void mkfifo(String path, int mode) throws ErrnoException { Libcore.os.mkfifo(path, mode); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/mlock.2.html">mlock(2)</a>. - */ - public static void mlock(long address, long byteCount) throws ErrnoException { Libcore.os.mlock(address, byteCount); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/mmap.2.html">mmap(2)</a>. - */ - public static long mmap(long address, long byteCount, int prot, int flags, FileDescriptor fd, long offset) throws ErrnoException { return Libcore.os.mmap(address, byteCount, prot, flags, fd, offset); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/msync.2.html">msync(2)</a>. - */ - public static void msync(long address, long byteCount, int flags) throws ErrnoException { Libcore.os.msync(address, byteCount, flags); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/munlock.2.html">munlock(2)</a>. - */ - public static void munlock(long address, long byteCount) throws ErrnoException { Libcore.os.munlock(address, byteCount); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/munmap.2.html">munmap(2)</a>. - */ - public static void munmap(long address, long byteCount) throws ErrnoException { Libcore.os.munmap(address, byteCount); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>. - */ - public static FileDescriptor open(String path, int flags, int mode) throws ErrnoException { return Libcore.os.open(path, flags, mode); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/pipe.2.html">pipe(2)</a>. - */ - public static FileDescriptor[] pipe() throws ErrnoException { return Libcore.os.pipe2(0); } - - /** @hide */ public static FileDescriptor[] pipe2(int flags) throws ErrnoException { return Libcore.os.pipe2(flags); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/poll.2.html">poll(2)</a>. - * - * <p>Note that in Lollipop this could throw an {@code ErrnoException} with {@code EINTR}. - * In later releases, the implementation will automatically just restart the system call with - * an appropriately reduced timeout. - */ - public static int poll(StructPollfd[] fds, int timeoutMs) throws ErrnoException { return Libcore.os.poll(fds, timeoutMs); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/posix_fallocate.3.html">posix_fallocate(3)</a>. - */ - public static void posix_fallocate(FileDescriptor fd, long offset, long length) throws ErrnoException { Libcore.os.posix_fallocate(fd, offset, length); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/prctl.2.html">prctl(2)</a>. - */ - public static int prctl(int option, long arg2, long arg3, long arg4, long arg5) throws ErrnoException { return Libcore.os.prctl(option, arg2, arg3, arg4, arg5); }; - - /** - * See <a href="http://man7.org/linux/man-pages/man2/pread.2.html">pread(2)</a>. - */ - public static int pread(FileDescriptor fd, ByteBuffer buffer, long offset) throws ErrnoException, InterruptedIOException { return Libcore.os.pread(fd, buffer, offset); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/pread.2.html">pread(2)</a>. - */ - public static int pread(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, long offset) throws ErrnoException, InterruptedIOException { return Libcore.os.pread(fd, bytes, byteOffset, byteCount, offset); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/pwrite.2.html">pwrite(2)</a>. - */ - public static int pwrite(FileDescriptor fd, ByteBuffer buffer, long offset) throws ErrnoException, InterruptedIOException { return Libcore.os.pwrite(fd, buffer, offset); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/pwrite.2.html">pwrite(2)</a>. - */ - public static int pwrite(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, long offset) throws ErrnoException, InterruptedIOException { return Libcore.os.pwrite(fd, bytes, byteOffset, byteCount, offset); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/read.2.html">read(2)</a>. - */ - public static int read(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException { return Libcore.os.read(fd, buffer); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/read.2.html">read(2)</a>. - */ - public static int read(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws ErrnoException, InterruptedIOException { return Libcore.os.read(fd, bytes, byteOffset, byteCount); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/readlink.2.html">readlink(2)</a>. - */ - public static String readlink(String path) throws ErrnoException { return Libcore.os.readlink(path); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/readv.2.html">readv(2)</a>. - */ - public static int readv(FileDescriptor fd, Object[] buffers, int[] offsets, int[] byteCounts) throws ErrnoException, InterruptedIOException { return Libcore.os.readv(fd, buffers, offsets, byteCounts); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/recvfrom.2.html">recvfrom(2)</a>. - */ - public static int recvfrom(FileDescriptor fd, ByteBuffer buffer, int flags, InetSocketAddress srcAddress) throws ErrnoException, SocketException { return Libcore.os.recvfrom(fd, buffer, flags, srcAddress); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/recvfrom.2.html">recvfrom(2)</a>. - */ - public static int recvfrom(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetSocketAddress srcAddress) throws ErrnoException, SocketException { return Libcore.os.recvfrom(fd, bytes, byteOffset, byteCount, flags, srcAddress); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/remove.3.html">remove(3)</a>. - */ - public static void remove(String path) throws ErrnoException { Libcore.os.remove(path); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/removexattr.2.html">removexattr(2)</a>. - */ - public static void removexattr(String path, String name) throws ErrnoException { Libcore.os.removexattr(path, name); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/rename.2.html">rename(2)</a>. - */ - public static void rename(String oldPath, String newPath) throws ErrnoException { Libcore.os.rename(oldPath, newPath); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/sendfile.2.html">sendfile(2)</a>. - */ - public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, MutableLong inOffset, long byteCount) throws ErrnoException { return Libcore.os.sendfile(outFd, inFd, inOffset, byteCount); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/sendto.2.html">sendto(2)</a>. - */ - public static int sendto(FileDescriptor fd, ByteBuffer buffer, int flags, InetAddress inetAddress, int port) throws ErrnoException, SocketException { return Libcore.os.sendto(fd, buffer, flags, inetAddress, port); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/sendto.2.html">sendto(2)</a>. - */ - public static int sendto(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port) throws ErrnoException, SocketException { return Libcore.os.sendto(fd, bytes, byteOffset, byteCount, flags, inetAddress, port); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/sendto.2.html">sendto(2)</a>. - */ - /** @hide */ public static int sendto(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, SocketAddress address) throws ErrnoException, SocketException { return Libcore.os.sendto(fd, bytes, byteOffset, byteCount, flags, address); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setegid.2.html">setegid(2)</a>. - */ - public static void setegid(int egid) throws ErrnoException { Libcore.os.setegid(egid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/setenv.3.html">setenv(3)</a>. - */ - public static void setenv(String name, String value, boolean overwrite) throws ErrnoException { Libcore.os.setenv(name, value, overwrite); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/seteuid.2.html">seteuid(2)</a>. - */ - public static void seteuid(int euid) throws ErrnoException { Libcore.os.seteuid(euid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setgid.2.html">setgid(2)</a>. - */ - public static void setgid(int gid) throws ErrnoException { Libcore.os.setgid(gid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setpgid.2.html">setpgid(2)</a>. - */ - /** @hide */ public static void setpgid(int pid, int pgid) throws ErrnoException { Libcore.os.setpgid(pid, pgid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setregid.2.html">setregid(2)</a>. - */ - /** @hide */ public static void setregid(int rgid, int egid) throws ErrnoException { Libcore.os.setregid(rgid, egid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setreuid.2.html">setreuid(2)</a>. - */ - /** @hide */ public static void setreuid(int ruid, int euid) throws ErrnoException { Libcore.os.setreuid(ruid, euid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setsid.2.html">setsid(2)</a>. - */ - public static int setsid() throws ErrnoException { return Libcore.os.setsid(); } - - /** @hide */ public static void setsockoptByte(FileDescriptor fd, int level, int option, int value) throws ErrnoException { Libcore.os.setsockoptByte(fd, level, option, value); } - /** @hide */ public static void setsockoptIfreq(FileDescriptor fd, int level, int option, String value) throws ErrnoException { Libcore.os.setsockoptIfreq(fd, level, option, value); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setsockopt.2.html">setsockopt(2)</a>. - */ - public static void setsockoptInt(FileDescriptor fd, int level, int option, int value) throws ErrnoException { Libcore.os.setsockoptInt(fd, level, option, value); } - - /** @hide */ public static void setsockoptIpMreqn(FileDescriptor fd, int level, int option, int value) throws ErrnoException { Libcore.os.setsockoptIpMreqn(fd, level, option, value); } - /** @hide */ public static void setsockoptGroupReq(FileDescriptor fd, int level, int option, StructGroupReq value) throws ErrnoException { Libcore.os.setsockoptGroupReq(fd, level, option, value); } - /** @hide */ public static void setsockoptLinger(FileDescriptor fd, int level, int option, StructLinger value) throws ErrnoException { Libcore.os.setsockoptLinger(fd, level, option, value); } - /** @hide */ public static void setsockoptTimeval(FileDescriptor fd, int level, int option, StructTimeval value) throws ErrnoException { Libcore.os.setsockoptTimeval(fd, level, option, value); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setuid.2.html">setuid(2)</a>. - */ - public static void setuid(int uid) throws ErrnoException { Libcore.os.setuid(uid); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/setxattr.2.html">setxattr(2)</a> - */ - public static void setxattr(String path, String name, byte[] value, int flags) throws ErrnoException { Libcore.os.setxattr(path, name, value, flags); }; - - /** - * See <a href="http://man7.org/linux/man-pages/man2/shutdown.2.html">shutdown(2)</a>. - */ - public static void shutdown(FileDescriptor fd, int how) throws ErrnoException { Libcore.os.shutdown(fd, how); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/socket.2.html">socket(2)</a>. - */ - public static FileDescriptor socket(int domain, int type, int protocol) throws ErrnoException { return Libcore.os.socket(domain, type, protocol); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/socketpair.2.html">socketpair(2)</a>. - */ - public static void socketpair(int domain, int type, int protocol, FileDescriptor fd1, FileDescriptor fd2) throws ErrnoException { Libcore.os.socketpair(domain, type, protocol, fd1, fd2); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/stat.2.html">stat(2)</a>. - */ - public static StructStat stat(String path) throws ErrnoException { return Libcore.os.stat(path); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/statvfs.2.html">statvfs(2)</a>. - */ - public static StructStatVfs statvfs(String path) throws ErrnoException { return Libcore.os.statvfs(path); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/strerror.3.html">strerror(2)</a>. - */ - public static String strerror(int errno) { return Libcore.os.strerror(errno); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/strsignal.3.html">strsignal(3)</a>. - */ - public static String strsignal(int signal) { return Libcore.os.strsignal(signal); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/symlink.2.html">symlink(2)</a>. - */ - public static void symlink(String oldPath, String newPath) throws ErrnoException { Libcore.os.symlink(oldPath, newPath); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/sysconf.3.html">sysconf(3)</a>. - */ - public static long sysconf(int name) { return Libcore.os.sysconf(name); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/tcdrain.3.html">tcdrain(3)</a>. - */ - public static void tcdrain(FileDescriptor fd) throws ErrnoException { Libcore.os.tcdrain(fd); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/tcsendbreak.3.html">tcsendbreak(3)</a>. - */ - public static void tcsendbreak(FileDescriptor fd, int duration) throws ErrnoException { Libcore.os.tcsendbreak(fd, duration); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/umask.2.html">umask(2)</a>. - */ - public static int umask(int mask) { return Libcore.os.umask(mask); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/uname.2.html">uname(2)</a>. - */ - public static StructUtsname uname() { return Libcore.os.uname(); } - - /** - * @hide See <a href="http://man7.org/linux/man-pages/man2/unlink.2.html">unlink(2)</a>. - */ - public static void unlink(String pathname) throws ErrnoException { Libcore.os.unlink(pathname); } - - /** - * See <a href="http://man7.org/linux/man-pages/man3/unsetenv.3.html">unsetenv(3)</a>. - */ - public static void unsetenv(String name) throws ErrnoException { Libcore.os.unsetenv(name); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/waitpid.2.html">waitpid(2)</a>. - */ - public static int waitpid(int pid, MutableInt status, int options) throws ErrnoException { return Libcore.os.waitpid(pid, status, options); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/write.2.html">write(2)</a>. - */ - public static int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException { return Libcore.os.write(fd, buffer); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/write.2.html">write(2)</a>. - */ - public static int write(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws ErrnoException, InterruptedIOException { return Libcore.os.write(fd, bytes, byteOffset, byteCount); } - - /** - * See <a href="http://man7.org/linux/man-pages/man2/writev.2.html">writev(2)</a>. - */ - public static int writev(FileDescriptor fd, Object[] buffers, int[] offsets, int[] byteCounts) throws ErrnoException, InterruptedIOException { return Libcore.os.writev(fd, buffers, offsets, byteCounts); } + private Os() {} + + /** + * See <a href="http://man7.org/linux/man-pages/man2/accept.2.html">accept(2)</a>. + */ + public static FileDescriptor accept(FileDescriptor fd, InetSocketAddress peerAddress) throws ErrnoException, SocketException { return Libcore.os.accept(fd, peerAddress); } + + /** + * TODO Change the public API by removing the overload above and unhiding this version. + * @hide + */ + public static FileDescriptor accept(FileDescriptor fd, SocketAddress peerAddress) throws ErrnoException, SocketException { return Libcore.os.accept(fd, peerAddress); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/access.2.html">access(2)</a>. + */ + public static boolean access(String path, int mode) throws ErrnoException { return Libcore.os.access(path, mode); } + + /** @hide */ public static InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException { return Libcore.os.android_getaddrinfo(node, hints, netId); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/bind.2.html">bind(2)</a>. + */ + public static void bind(FileDescriptor fd, InetAddress address, int port) throws ErrnoException, SocketException { Libcore.os.bind(fd, address, port); } + + /** @hide */ public static void bind(FileDescriptor fd, SocketAddress address) throws ErrnoException, SocketException { Libcore.os.bind(fd, address); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/capget.2.html">capget(2)</a>. + * + * @hide + */ + public static StructCapUserData[] capget(StructCapUserHeader hdr) throws ErrnoException { + return Libcore.os.capget(hdr); + } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/capset.2.html">capset(2)</a>. + * + * @hide + */ + public static void capset(StructCapUserHeader hdr, StructCapUserData[] data) + throws ErrnoException { + Libcore.os.capset(hdr, data); + } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/chmod.2.html">chmod(2)</a>. + */ + public static void chmod(String path, int mode) throws ErrnoException { Libcore.os.chmod(path, mode); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/chown.2.html">chown(2)</a>. + */ + public static void chown(String path, int uid, int gid) throws ErrnoException { Libcore.os.chown(path, uid, gid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/close.2.html">close(2)</a>. + */ + public static void close(FileDescriptor fd) throws ErrnoException { Libcore.os.close(fd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/connect.2.html">connect(2)</a>. + */ + public static void connect(FileDescriptor fd, InetAddress address, int port) throws ErrnoException, SocketException { Libcore.os.connect(fd, address, port); } + + /** @hide */ public static void connect(FileDescriptor fd, SocketAddress address) throws ErrnoException, SocketException { Libcore.os.connect(fd, address); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/dup.2.html">dup(2)</a>. + */ + public static FileDescriptor dup(FileDescriptor oldFd) throws ErrnoException { return Libcore.os.dup(oldFd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/dup2.2.html">dup2(2)</a>. + */ + public static FileDescriptor dup2(FileDescriptor oldFd, int newFd) throws ErrnoException { return Libcore.os.dup2(oldFd, newFd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/environ.3.html">environ(3)</a>. + */ + public static String[] environ() { return Libcore.os.environ(); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/execv.2.html">execv(2)</a>. + */ + public static void execv(String filename, String[] argv) throws ErrnoException { Libcore.os.execv(filename, argv); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/execve.2.html">execve(2)</a>. + */ + public static void execve(String filename, String[] argv, String[] envp) throws ErrnoException { Libcore.os.execve(filename, argv, envp); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/fchmod.2.html">fchmod(2)</a>. + */ + public static void fchmod(FileDescriptor fd, int mode) throws ErrnoException { Libcore.os.fchmod(fd, mode); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/fchown.2.html">fchown(2)</a>. + */ + public static void fchown(FileDescriptor fd, int uid, int gid) throws ErrnoException { Libcore.os.fchown(fd, uid, gid); } + + /** @hide */ public static int fcntlFlock(FileDescriptor fd, int cmd, StructFlock arg) throws ErrnoException, InterruptedIOException { return Libcore.os.fcntlFlock(fd, cmd, arg); } + /** @hide */ public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException { return Libcore.os.fcntlInt(fd, cmd, arg); } + /** @hide */ public static int fcntlVoid(FileDescriptor fd, int cmd) throws ErrnoException { return Libcore.os.fcntlVoid(fd, cmd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/fdatasync.2.html">fdatasync(2)</a>. + */ + public static void fdatasync(FileDescriptor fd) throws ErrnoException { Libcore.os.fdatasync(fd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/fstat.2.html">fstat(2)</a>. + */ + public static StructStat fstat(FileDescriptor fd) throws ErrnoException { return Libcore.os.fstat(fd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/fstatvfs.2.html">fstatvfs(2)</a>. + */ + public static StructStatVfs fstatvfs(FileDescriptor fd) throws ErrnoException { return Libcore.os.fstatvfs(fd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/fsync.2.html">fsync(2)</a>. + */ + public static void fsync(FileDescriptor fd) throws ErrnoException { Libcore.os.fsync(fd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/ftruncate.2.html">ftruncate(2)</a>. + */ + public static void ftruncate(FileDescriptor fd, long length) throws ErrnoException { Libcore.os.ftruncate(fd, length); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/gai_strerror.3.html">gai_strerror(3)</a>. + */ + public static String gai_strerror(int error) { return Libcore.os.gai_strerror(error); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getegid.2.html">getegid(2)</a>. + */ + public static int getegid() { return Libcore.os.getegid(); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/geteuid.2.html">geteuid(2)</a>. + */ + public static int geteuid() { return Libcore.os.geteuid(); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getgid.2.html">getgid(2)</a>. + */ + public static int getgid() { return Libcore.os.getgid(); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/getenv.3.html">getenv(3)</a>. + */ + public static String getenv(String name) { return Libcore.os.getenv(name); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/getifaddrs.3.html">getifaddrs(3)</a>. + */ + /** @hide */ public static StructIfaddrs[] getifaddrs() throws ErrnoException { return Libcore.os.getifaddrs(); } + + /** @hide */ public static String getnameinfo(InetAddress address, int flags) throws GaiException { return Libcore.os.getnameinfo(address, flags); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getpeername.2.html">getpeername(2)</a>. + */ + public static SocketAddress getpeername(FileDescriptor fd) throws ErrnoException { return Libcore.os.getpeername(fd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getpgid.2.html">getpgid(2)</a>. + */ + /** @hide */ public static int getpgid(int pid) throws ErrnoException { return Libcore.os.getpgid(pid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getpid.2.html">getpid(2)</a>. + */ + public static int getpid() { return Libcore.os.getpid(); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getppid.2.html">getppid(2)</a>. + */ + public static int getppid() { return Libcore.os.getppid(); } + + /** @hide */ public static StructPasswd getpwnam(String name) throws ErrnoException { return Libcore.os.getpwnam(name); } + + /** @hide */ public static StructPasswd getpwuid(int uid) throws ErrnoException { return Libcore.os.getpwuid(uid); } + + /** @hide */ public static StructRlimit getrlimit(int resource) throws ErrnoException { return Libcore.os.getrlimit(resource); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getsockname.2.html">getsockname(2)</a>. + */ + public static SocketAddress getsockname(FileDescriptor fd) throws ErrnoException { return Libcore.os.getsockname(fd); } + + /** @hide */ public static int getsockoptByte(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptByte(fd, level, option); } + /** @hide */ public static InetAddress getsockoptInAddr(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptInAddr(fd, level, option); } + /** @hide */ public static int getsockoptInt(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptInt(fd, level, option); } + /** @hide */ public static StructLinger getsockoptLinger(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptLinger(fd, level, option); } + /** @hide */ public static StructTimeval getsockoptTimeval(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptTimeval(fd, level, option); } + /** @hide */ public static StructUcred getsockoptUcred(FileDescriptor fd, int level, int option) throws ErrnoException { return Libcore.os.getsockoptUcred(fd, level, option); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/gettid.2.html">gettid(2)</a>. + */ + public static int gettid() { return Libcore.os.gettid(); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getuid.2.html">getuid(2)</a>. + */ + public static int getuid() { return Libcore.os.getuid(); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/getxattr.2.html">getxattr(2)</a> + */ + public static byte[] getxattr(String path, String name) throws ErrnoException { return Libcore.os.getxattr(path, name); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/if_indextoname.3.html">if_indextoname(3)</a>. + */ + public static String if_indextoname(int index) { return Libcore.os.if_indextoname(index); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/if_nametoindex.3.html">if_nametoindex(3)</a>. + */ + public static int if_nametoindex(String name) { return Libcore.os.if_nametoindex(name); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/inet_pton.3.html">inet_pton(3)</a>. + */ + public static InetAddress inet_pton(int family, String address) { return Libcore.os.inet_pton(family, address); } + + /** @hide */ public static InetAddress ioctlInetAddress(FileDescriptor fd, int cmd, String interfaceName) throws ErrnoException { return Libcore.os.ioctlInetAddress(fd, cmd, interfaceName); } + /** @hide */ public static int ioctlInt(FileDescriptor fd, int cmd, MutableInt arg) throws ErrnoException { return Libcore.os.ioctlInt(fd, cmd, arg); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/isatty.3.html">isatty(3)</a>. + */ + public static boolean isatty(FileDescriptor fd) { return Libcore.os.isatty(fd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/kill.2.html">kill(2)</a>. + */ + public static void kill(int pid, int signal) throws ErrnoException { Libcore.os.kill(pid, signal); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/lchown.2.html">lchown(2)</a>. + */ + public static void lchown(String path, int uid, int gid) throws ErrnoException { Libcore.os.lchown(path, uid, gid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/link.2.html">link(2)</a>. + */ + public static void link(String oldPath, String newPath) throws ErrnoException { Libcore.os.link(oldPath, newPath); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/listen.2.html">listen(2)</a>. + */ + public static void listen(FileDescriptor fd, int backlog) throws ErrnoException { Libcore.os.listen(fd, backlog); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/listxattr.2.html">listxattr(2)</a> + */ + public static String[] listxattr(String path) throws ErrnoException { return Libcore.os.listxattr(path); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/lseek.2.html">lseek(2)</a>. + */ + public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException { return Libcore.os.lseek(fd, offset, whence); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/lstat.2.html">lstat(2)</a>. + */ + public static StructStat lstat(String path) throws ErrnoException { return Libcore.os.lstat(path); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/mincore.2.html">mincore(2)</a>. + */ + public static void mincore(long address, long byteCount, byte[] vector) throws ErrnoException { Libcore.os.mincore(address, byteCount, vector); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/mkdir.2.html">mkdir(2)</a>. + */ + public static void mkdir(String path, int mode) throws ErrnoException { Libcore.os.mkdir(path, mode); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/mkfifo.3.html">mkfifo(3)</a>. + */ + public static void mkfifo(String path, int mode) throws ErrnoException { Libcore.os.mkfifo(path, mode); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/mlock.2.html">mlock(2)</a>. + */ + public static void mlock(long address, long byteCount) throws ErrnoException { Libcore.os.mlock(address, byteCount); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/mmap.2.html">mmap(2)</a>. + */ + public static long mmap(long address, long byteCount, int prot, int flags, FileDescriptor fd, long offset) throws ErrnoException { return Libcore.os.mmap(address, byteCount, prot, flags, fd, offset); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/msync.2.html">msync(2)</a>. + */ + public static void msync(long address, long byteCount, int flags) throws ErrnoException { Libcore.os.msync(address, byteCount, flags); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/munlock.2.html">munlock(2)</a>. + */ + public static void munlock(long address, long byteCount) throws ErrnoException { Libcore.os.munlock(address, byteCount); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/munmap.2.html">munmap(2)</a>. + */ + public static void munmap(long address, long byteCount) throws ErrnoException { Libcore.os.munmap(address, byteCount); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>. + */ + public static FileDescriptor open(String path, int flags, int mode) throws ErrnoException { return Libcore.os.open(path, flags, mode); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/pipe.2.html">pipe(2)</a>. + */ + public static FileDescriptor[] pipe() throws ErrnoException { return Libcore.os.pipe2(0); } + + /** @hide */ public static FileDescriptor[] pipe2(int flags) throws ErrnoException { return Libcore.os.pipe2(flags); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/poll.2.html">poll(2)</a>. + * + * <p>Note that in Lollipop this could throw an {@code ErrnoException} with {@code EINTR}. + * In later releases, the implementation will automatically just restart the system call with + * an appropriately reduced timeout. + */ + public static int poll(StructPollfd[] fds, int timeoutMs) throws ErrnoException { return Libcore.os.poll(fds, timeoutMs); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/posix_fallocate.3.html">posix_fallocate(3)</a>. + */ + public static void posix_fallocate(FileDescriptor fd, long offset, long length) throws ErrnoException { Libcore.os.posix_fallocate(fd, offset, length); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/prctl.2.html">prctl(2)</a>. + */ + public static int prctl(int option, long arg2, long arg3, long arg4, long arg5) throws ErrnoException { return Libcore.os.prctl(option, arg2, arg3, arg4, arg5); }; + + /** + * See <a href="http://man7.org/linux/man-pages/man2/pread.2.html">pread(2)</a>. + */ + public static int pread(FileDescriptor fd, ByteBuffer buffer, long offset) throws ErrnoException, InterruptedIOException { return Libcore.os.pread(fd, buffer, offset); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/pread.2.html">pread(2)</a>. + */ + public static int pread(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, long offset) throws ErrnoException, InterruptedIOException { return Libcore.os.pread(fd, bytes, byteOffset, byteCount, offset); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/pwrite.2.html">pwrite(2)</a>. + */ + public static int pwrite(FileDescriptor fd, ByteBuffer buffer, long offset) throws ErrnoException, InterruptedIOException { return Libcore.os.pwrite(fd, buffer, offset); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/pwrite.2.html">pwrite(2)</a>. + */ + public static int pwrite(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, long offset) throws ErrnoException, InterruptedIOException { return Libcore.os.pwrite(fd, bytes, byteOffset, byteCount, offset); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/read.2.html">read(2)</a>. + */ + public static int read(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException { return Libcore.os.read(fd, buffer); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/read.2.html">read(2)</a>. + */ + public static int read(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws ErrnoException, InterruptedIOException { return Libcore.os.read(fd, bytes, byteOffset, byteCount); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/readlink.2.html">readlink(2)</a>. + */ + public static String readlink(String path) throws ErrnoException { return Libcore.os.readlink(path); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/realpath.3.html">realpath(3)</a>. + */ + /** @hide */ public static String realpath(String path) throws ErrnoException { return Libcore.os.realpath(path); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/readv.2.html">readv(2)</a>. + */ + public static int readv(FileDescriptor fd, Object[] buffers, int[] offsets, int[] byteCounts) throws ErrnoException, InterruptedIOException { return Libcore.os.readv(fd, buffers, offsets, byteCounts); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/recvfrom.2.html">recvfrom(2)</a>. + */ + public static int recvfrom(FileDescriptor fd, ByteBuffer buffer, int flags, InetSocketAddress srcAddress) throws ErrnoException, SocketException { return Libcore.os.recvfrom(fd, buffer, flags, srcAddress); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/recvfrom.2.html">recvfrom(2)</a>. + */ + public static int recvfrom(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetSocketAddress srcAddress) throws ErrnoException, SocketException { return Libcore.os.recvfrom(fd, bytes, byteOffset, byteCount, flags, srcAddress); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/remove.3.html">remove(3)</a>. + */ + public static void remove(String path) throws ErrnoException { Libcore.os.remove(path); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/removexattr.2.html">removexattr(2)</a>. + */ + public static void removexattr(String path, String name) throws ErrnoException { Libcore.os.removexattr(path, name); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/rename.2.html">rename(2)</a>. + */ + public static void rename(String oldPath, String newPath) throws ErrnoException { Libcore.os.rename(oldPath, newPath); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/sendfile.2.html">sendfile(2)</a>. + */ + public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, MutableLong inOffset, long byteCount) throws ErrnoException { return Libcore.os.sendfile(outFd, inFd, inOffset, byteCount); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/sendto.2.html">sendto(2)</a>. + */ + public static int sendto(FileDescriptor fd, ByteBuffer buffer, int flags, InetAddress inetAddress, int port) throws ErrnoException, SocketException { return Libcore.os.sendto(fd, buffer, flags, inetAddress, port); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/sendto.2.html">sendto(2)</a>. + */ + public static int sendto(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, InetAddress inetAddress, int port) throws ErrnoException, SocketException { return Libcore.os.sendto(fd, bytes, byteOffset, byteCount, flags, inetAddress, port); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/sendto.2.html">sendto(2)</a>. + */ + /** @hide */ public static int sendto(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, int flags, SocketAddress address) throws ErrnoException, SocketException { return Libcore.os.sendto(fd, bytes, byteOffset, byteCount, flags, address); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setegid.2.html">setegid(2)</a>. + */ + public static void setegid(int egid) throws ErrnoException { Libcore.os.setegid(egid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/setenv.3.html">setenv(3)</a>. + */ + public static void setenv(String name, String value, boolean overwrite) throws ErrnoException { Libcore.os.setenv(name, value, overwrite); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/seteuid.2.html">seteuid(2)</a>. + */ + public static void seteuid(int euid) throws ErrnoException { Libcore.os.seteuid(euid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setgid.2.html">setgid(2)</a>. + */ + public static void setgid(int gid) throws ErrnoException { Libcore.os.setgid(gid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setpgid.2.html">setpgid(2)</a>. + */ + /** @hide */ public static void setpgid(int pid, int pgid) throws ErrnoException { Libcore.os.setpgid(pid, pgid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setregid.2.html">setregid(2)</a>. + */ + /** @hide */ public static void setregid(int rgid, int egid) throws ErrnoException { Libcore.os.setregid(rgid, egid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setreuid.2.html">setreuid(2)</a>. + */ + /** @hide */ public static void setreuid(int ruid, int euid) throws ErrnoException { Libcore.os.setreuid(ruid, euid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setsid.2.html">setsid(2)</a>. + */ + public static int setsid() throws ErrnoException { return Libcore.os.setsid(); } + + /** @hide */ public static void setsockoptByte(FileDescriptor fd, int level, int option, int value) throws ErrnoException { Libcore.os.setsockoptByte(fd, level, option, value); } + /** @hide */ public static void setsockoptIfreq(FileDescriptor fd, int level, int option, String value) throws ErrnoException { Libcore.os.setsockoptIfreq(fd, level, option, value); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setsockopt.2.html">setsockopt(2)</a>. + */ + public static void setsockoptInt(FileDescriptor fd, int level, int option, int value) throws ErrnoException { Libcore.os.setsockoptInt(fd, level, option, value); } + + /** @hide */ public static void setsockoptIpMreqn(FileDescriptor fd, int level, int option, int value) throws ErrnoException { Libcore.os.setsockoptIpMreqn(fd, level, option, value); } + /** @hide */ public static void setsockoptGroupReq(FileDescriptor fd, int level, int option, StructGroupReq value) throws ErrnoException { Libcore.os.setsockoptGroupReq(fd, level, option, value); } + /** @hide */ public static void setsockoptLinger(FileDescriptor fd, int level, int option, StructLinger value) throws ErrnoException { Libcore.os.setsockoptLinger(fd, level, option, value); } + /** @hide */ public static void setsockoptTimeval(FileDescriptor fd, int level, int option, StructTimeval value) throws ErrnoException { Libcore.os.setsockoptTimeval(fd, level, option, value); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setuid.2.html">setuid(2)</a>. + */ + public static void setuid(int uid) throws ErrnoException { Libcore.os.setuid(uid); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/setxattr.2.html">setxattr(2)</a> + */ + public static void setxattr(String path, String name, byte[] value, int flags) throws ErrnoException { Libcore.os.setxattr(path, name, value, flags); }; + + /** + * See <a href="http://man7.org/linux/man-pages/man2/shutdown.2.html">shutdown(2)</a>. + */ + public static void shutdown(FileDescriptor fd, int how) throws ErrnoException { Libcore.os.shutdown(fd, how); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/socket.2.html">socket(2)</a>. + */ + public static FileDescriptor socket(int domain, int type, int protocol) throws ErrnoException { return Libcore.os.socket(domain, type, protocol); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/socketpair.2.html">socketpair(2)</a>. + */ + public static void socketpair(int domain, int type, int protocol, FileDescriptor fd1, FileDescriptor fd2) throws ErrnoException { Libcore.os.socketpair(domain, type, protocol, fd1, fd2); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/stat.2.html">stat(2)</a>. + */ + public static StructStat stat(String path) throws ErrnoException { return Libcore.os.stat(path); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/statvfs.2.html">statvfs(2)</a>. + */ + public static StructStatVfs statvfs(String path) throws ErrnoException { return Libcore.os.statvfs(path); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/strerror.3.html">strerror(2)</a>. + */ + public static String strerror(int errno) { return Libcore.os.strerror(errno); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/strsignal.3.html">strsignal(3)</a>. + */ + public static String strsignal(int signal) { return Libcore.os.strsignal(signal); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/symlink.2.html">symlink(2)</a>. + */ + public static void symlink(String oldPath, String newPath) throws ErrnoException { Libcore.os.symlink(oldPath, newPath); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/sysconf.3.html">sysconf(3)</a>. + */ + public static long sysconf(int name) { return Libcore.os.sysconf(name); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/tcdrain.3.html">tcdrain(3)</a>. + */ + public static void tcdrain(FileDescriptor fd) throws ErrnoException { Libcore.os.tcdrain(fd); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/tcsendbreak.3.html">tcsendbreak(3)</a>. + */ + public static void tcsendbreak(FileDescriptor fd, int duration) throws ErrnoException { Libcore.os.tcsendbreak(fd, duration); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/umask.2.html">umask(2)</a>. + */ + public static int umask(int mask) { return Libcore.os.umask(mask); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/uname.2.html">uname(2)</a>. + */ + public static StructUtsname uname() { return Libcore.os.uname(); } + + /** + * @hide See <a href="http://man7.org/linux/man-pages/man2/unlink.2.html">unlink(2)</a>. + */ + public static void unlink(String pathname) throws ErrnoException { Libcore.os.unlink(pathname); } + + /** + * See <a href="http://man7.org/linux/man-pages/man3/unsetenv.3.html">unsetenv(3)</a>. + */ + public static void unsetenv(String name) throws ErrnoException { Libcore.os.unsetenv(name); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/waitpid.2.html">waitpid(2)</a>. + */ + public static int waitpid(int pid, MutableInt status, int options) throws ErrnoException { return Libcore.os.waitpid(pid, status, options); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/write.2.html">write(2)</a>. + */ + public static int write(FileDescriptor fd, ByteBuffer buffer) throws ErrnoException, InterruptedIOException { return Libcore.os.write(fd, buffer); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/write.2.html">write(2)</a>. + */ + public static int write(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws ErrnoException, InterruptedIOException { return Libcore.os.write(fd, bytes, byteOffset, byteCount); } + + /** + * See <a href="http://man7.org/linux/man-pages/man2/writev.2.html">writev(2)</a>. + */ + public static int writev(FileDescriptor fd, Object[] buffers, int[] offsets, int[] byteCounts) throws ErrnoException, InterruptedIOException { return Libcore.os.writev(fd, buffers, offsets, byteCounts); } } diff --git a/android/system/StructAddrinfo.java b/android/system/StructAddrinfo.java index 2425946f..dab7590e 100644 --- a/android/system/StructAddrinfo.java +++ b/android/system/StructAddrinfo.java @@ -28,31 +28,31 @@ import libcore.util.Objects; * @hide */ public final class StructAddrinfo { - /** Flags describing the kind of lookup to be done. (Such as AI_ADDRCONFIG.) */ - public int ai_flags; + /** Flags describing the kind of lookup to be done. (Such as AI_ADDRCONFIG.) */ + public int ai_flags; - /** Desired address family for results. (Such as AF_INET6 for IPv6. AF_UNSPEC means "any".) */ - public int ai_family; + /** Desired address family for results. (Such as AF_INET6 for IPv6. AF_UNSPEC means "any".) */ + public int ai_family; - /** Socket type. (Such as SOCK_DGRAM. 0 means "any".) */ - public int ai_socktype; + /** Socket type. (Such as SOCK_DGRAM. 0 means "any".) */ + public int ai_socktype; - /** Protocol. (Such as IPPROTO_IPV6 IPv6. 0 means "any".) */ - public int ai_protocol; + /** Protocol. (Such as IPPROTO_IPV6 IPv6. 0 means "any".) */ + public int ai_protocol; - /** Address length. (Not useful in Java.) */ - // public int ai_addrlen; + /** Address length. (Not useful in Java.) */ + // public int ai_addrlen; - /** Address. */ - public InetAddress ai_addr; + /** Address. */ + public InetAddress ai_addr; - /** Canonical name of service location (if AI_CANONNAME provided in ai_flags). */ - // public String ai_canonname; + /** Canonical name of service location (if AI_CANONNAME provided in ai_flags). */ + // public String ai_canonname; - /** Next element in linked list. */ - public StructAddrinfo ai_next; + /** Next element in linked list. */ + public StructAddrinfo ai_next; - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructCapUserData.java b/android/system/StructCapUserData.java index af63caf7..331e2ead 100644 --- a/android/system/StructCapUserData.java +++ b/android/system/StructCapUserData.java @@ -24,25 +24,25 @@ import libcore.util.Objects; * @hide */ public final class StructCapUserData { - /** Effective capability mask. */ - public final int effective; /* __u32 */ + /** Effective capability mask. */ + public final int effective; /* __u32 */ - /** Permitted capability mask. */ - public final int permitted; /* __u32 */ + /** Permitted capability mask. */ + public final int permitted; /* __u32 */ - /** Inheritable capability mask. */ - public final int inheritable; /* __u32 */ + /** Inheritable capability mask. */ + public final int inheritable; /* __u32 */ - /** - * Constructs an instance with the given field values. - */ - public StructCapUserData(int effective, int permitted, int inheritable) { - this.effective = effective; - this.permitted = permitted; - this.inheritable = inheritable; - } + /** + * Constructs an instance with the given field values. + */ + public StructCapUserData(int effective, int permitted, int inheritable) { + this.effective = effective; + this.permitted = permitted; + this.inheritable = inheritable; + } - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructCapUserHeader.java b/android/system/StructCapUserHeader.java index abbb3954..fb03aa89 100644 --- a/android/system/StructCapUserHeader.java +++ b/android/system/StructCapUserHeader.java @@ -24,25 +24,25 @@ import libcore.util.Objects; * @hide */ public final class StructCapUserHeader { - /** - * Version of the header. Note this is not final as capget() may mutate the field when an - * invalid version is provided. See - * <a href="http://man7.org/linux/man-pages/man2/capget.2.html">capget(2)</a>. - */ - public int version; /* __u32 */ + /** + * Version of the header. Note this is not final as capget() may mutate the field when an + * invalid version is provided. See + * <a href="http://man7.org/linux/man-pages/man2/capget.2.html">capget(2)</a>. + */ + public int version; /* __u32 */ - /** Pid of the header. The pid a call applies to. */ - public final int pid; + /** Pid of the header. The pid a call applies to. */ + public final int pid; - /** - * Constructs an instance with the given field values. - */ - public StructCapUserHeader(int version, int pid) { - this.version = version; - this.pid = pid; - } + /** + * Constructs an instance with the given field values. + */ + public StructCapUserHeader(int version, int pid) { + this.version = version; + this.pid = pid; + } - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructFlock.java b/android/system/StructFlock.java index 92cd95ab..0d934250 100644 --- a/android/system/StructFlock.java +++ b/android/system/StructFlock.java @@ -26,22 +26,22 @@ import libcore.util.Objects; * @hide */ public final class StructFlock { - /** The operation type, one of F_RDLCK, F_WRLCK, or F_UNLCK. */ - public short l_type; + /** The operation type, one of F_RDLCK, F_WRLCK, or F_UNLCK. */ + public short l_type; - /** How to interpret l_start, one of SEEK_CUR, SEEK_END, SEEK_SET. */ - public short l_whence; + /** How to interpret l_start, one of SEEK_CUR, SEEK_END, SEEK_SET. */ + public short l_whence; - /** Start offset. */ - public long l_start; /*off_t*/ + /** Start offset. */ + public long l_start; /*off_t*/ - /** Byte count to operate on. */ - public long l_len; /*off_t*/ + /** Byte count to operate on. */ + public long l_len; /*off_t*/ - /** Process blocking our lock (filled in by F_GETLK, otherwise unused). */ - public int l_pid; /*pid_t*/ + /** Process blocking our lock (filled in by F_GETLK, otherwise unused). */ + public int l_pid; /*pid_t*/ - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructGroupReq.java b/android/system/StructGroupReq.java index 8ed5950b..78db5f53 100644 --- a/android/system/StructGroupReq.java +++ b/android/system/StructGroupReq.java @@ -25,15 +25,15 @@ import libcore.util.Objects; * @hide */ public final class StructGroupReq { - public final int gr_interface; - public final InetAddress gr_group; + public final int gr_interface; + public final InetAddress gr_group; - public StructGroupReq(int gr_interface, InetAddress gr_group) { - this.gr_interface = gr_interface; - this.gr_group = gr_group; - } + public StructGroupReq(int gr_interface, InetAddress gr_group) { + this.gr_interface = gr_interface; + this.gr_group = gr_group; + } - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructLinger.java b/android/system/StructLinger.java index 55ffc5c9..46d41325 100644 --- a/android/system/StructLinger.java +++ b/android/system/StructLinger.java @@ -25,22 +25,22 @@ import libcore.util.Objects; * @hide */ public final class StructLinger { - /** Whether or not linger is enabled. Non-zero is on. */ - public final int l_onoff; + /** Whether or not linger is enabled. Non-zero is on. */ + public final int l_onoff; - /** Linger time in seconds. */ - public final int l_linger; + /** Linger time in seconds. */ + public final int l_linger; - public StructLinger(int l_onoff, int l_linger) { - this.l_onoff = l_onoff; - this.l_linger = l_linger; - } + public StructLinger(int l_onoff, int l_linger) { + this.l_onoff = l_onoff; + this.l_linger = l_linger; + } - public boolean isOn() { - return l_onoff != 0; - } + public boolean isOn() { + return l_onoff != 0; + } - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructPasswd.java b/android/system/StructPasswd.java index 93655c19..633a607d 100644 --- a/android/system/StructPasswd.java +++ b/android/system/StructPasswd.java @@ -25,24 +25,24 @@ import libcore.util.Objects; * @hide */ public final class StructPasswd { - public final String pw_name; - public final int pw_uid; - public final int pw_gid; - public final String pw_dir; - public final String pw_shell; + public final String pw_name; + public final int pw_uid; + public final int pw_gid; + public final String pw_dir; + public final String pw_shell; - /** - * Constructs an instance with the given field values. - */ - public StructPasswd(String pw_name, int pw_uid, int pw_gid, String pw_dir, String pw_shell) { - this.pw_name = pw_name; - this.pw_uid = pw_uid; - this.pw_gid = pw_gid; - this.pw_dir = pw_dir; - this.pw_shell = pw_shell; - } + /** + * Constructs an instance with the given field values. + */ + public StructPasswd(String pw_name, int pw_uid, int pw_gid, String pw_dir, String pw_shell) { + this.pw_name = pw_name; + this.pw_uid = pw_uid; + this.pw_gid = pw_gid; + this.pw_dir = pw_dir; + this.pw_shell = pw_shell; + } - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructPollfd.java b/android/system/StructPollfd.java index 2ca117e9..2f40b8bc 100644 --- a/android/system/StructPollfd.java +++ b/android/system/StructPollfd.java @@ -24,26 +24,26 @@ import libcore.util.Objects; * Corresponds to C's {@code struct pollfd} from {@code <poll.h>}. */ public final class StructPollfd { - /** The file descriptor to poll. */ - public FileDescriptor fd; + /** The file descriptor to poll. */ + public FileDescriptor fd; - /** - * The events we're interested in. POLLIN corresponds to being in select(2)'s read fd set, - * POLLOUT to the write fd set. - */ - public short events; + /** + * The events we're interested in. POLLIN corresponds to being in select(2)'s read fd set, + * POLLOUT to the write fd set. + */ + public short events; - /** The events that actually happened. */ - public short revents; + /** The events that actually happened. */ + public short revents; - /** - * A non-standard extension that lets callers conveniently map back to the object - * their fd belongs to. This is used by Selector, for example, to associate each - * FileDescriptor with the corresponding SelectionKey. - */ - public Object userData; + /** + * A non-standard extension that lets callers conveniently map back to the object + * their fd belongs to. This is used by Selector, for example, to associate each + * FileDescriptor with the corresponding SelectionKey. + */ + public Object userData; - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructRlimit.java b/android/system/StructRlimit.java index 6bb05a9c..007757f4 100644 --- a/android/system/StructRlimit.java +++ b/android/system/StructRlimit.java @@ -25,15 +25,15 @@ import libcore.util.Objects; * @hide */ public final class StructRlimit { - public final long rlim_cur; - public final long rlim_max; + public final long rlim_cur; + public final long rlim_max; - public StructRlimit(long rlim_cur, long rlim_max) { - this.rlim_cur = rlim_cur; - this.rlim_max = rlim_max; - } + public StructRlimit(long rlim_cur, long rlim_max) { + this.rlim_cur = rlim_cur; + this.rlim_max = rlim_max; + } - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructStat.java b/android/system/StructStat.java index a1e87296..a8b1fca4 100644 --- a/android/system/StructStat.java +++ b/android/system/StructStat.java @@ -23,99 +23,99 @@ import libcore.util.Objects; * Corresponds to C's {@code struct stat} from {@code <stat.h>}. */ public final class StructStat { - /** Device ID of device containing file. */ - public final long st_dev; /*dev_t*/ - - /** File serial number (inode). */ - public final long st_ino; /*ino_t*/ - - /** Mode (permissions) of file. */ - public final int st_mode; /*mode_t*/ - - /** Number of hard links to the file. */ - public final long st_nlink; /*nlink_t*/ - - /** User ID of file. */ - public final int st_uid; /*uid_t*/ - - /** Group ID of file. */ - public final int st_gid; /*gid_t*/ - - /** Device ID (if file is character or block special). */ - public final long st_rdev; /*dev_t*/ - - /** - * For regular files, the file size in bytes. - * For symbolic links, the length in bytes of the pathname contained in the symbolic link. - * For a shared memory object, the length in bytes. - * For a typed memory object, the length in bytes. - * For other file types, the use of this field is unspecified. - */ - public final long st_size; /*off_t*/ - - /** Seconds part of time of last access. */ - public final long st_atime; /*time_t*/ - - /** StructTimespec with time of last access. */ - public final StructTimespec st_atim; - - /** Seconds part of time of last data modification. */ - public final long st_mtime; /*time_t*/ - - /** StructTimespec with time of last modification. */ - public final StructTimespec st_mtim; - - /** Seconds part of time of last status change */ - public final long st_ctime; /*time_t*/ - - /** StructTimespec with time of last status change. */ - public final StructTimespec st_ctim; - - /** - * A file system-specific preferred I/O block size for this object. - * For some file system types, this may vary from file to file. - */ - public final long st_blksize; /*blksize_t*/ - - /** Number of blocks allocated for this object. */ - public final long st_blocks; /*blkcnt_t*/ - - /** - * Constructs an instance with the given field values. - */ - public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid, - long st_rdev, long st_size, long st_atime, long st_mtime, long st_ctime, - long st_blksize, long st_blocks) { - this(st_dev, st_ino, st_mode, st_nlink, st_uid, st_gid, - st_rdev, st_size, new StructTimespec(st_atime, 0L), new StructTimespec(st_mtime, 0L), - new StructTimespec(st_ctime, 0L), st_blksize, st_blocks); - } - - /** - * Constructs an instance with the given field values. - */ - public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid, - long st_rdev, long st_size, StructTimespec st_atim, StructTimespec st_mtim, - StructTimespec st_ctim, long st_blksize, long st_blocks) { - this.st_dev = st_dev; - this.st_ino = st_ino; - this.st_mode = st_mode; - this.st_nlink = st_nlink; - this.st_uid = st_uid; - this.st_gid = st_gid; - this.st_rdev = st_rdev; - this.st_size = st_size; - this.st_atime = st_atim.tv_sec; - this.st_mtime = st_mtim.tv_sec; - this.st_ctime = st_ctim.tv_sec; - this.st_atim = st_atim; - this.st_mtim = st_mtim; - this.st_ctim = st_ctim; - this.st_blksize = st_blksize; - this.st_blocks = st_blocks; - } - - @Override public String toString() { - return Objects.toString(this); - } + /** Device ID of device containing file. */ + public final long st_dev; /*dev_t*/ + + /** File serial number (inode). */ + public final long st_ino; /*ino_t*/ + + /** Mode (permissions) of file. */ + public final int st_mode; /*mode_t*/ + + /** Number of hard links to the file. */ + public final long st_nlink; /*nlink_t*/ + + /** User ID of file. */ + public final int st_uid; /*uid_t*/ + + /** Group ID of file. */ + public final int st_gid; /*gid_t*/ + + /** Device ID (if file is character or block special). */ + public final long st_rdev; /*dev_t*/ + + /** + * For regular files, the file size in bytes. + * For symbolic links, the length in bytes of the pathname contained in the symbolic link. + * For a shared memory object, the length in bytes. + * For a typed memory object, the length in bytes. + * For other file types, the use of this field is unspecified. + */ + public final long st_size; /*off_t*/ + + /** Seconds part of time of last access. */ + public final long st_atime; /*time_t*/ + + /** StructTimespec with time of last access. */ + public final StructTimespec st_atim; + + /** Seconds part of time of last data modification. */ + public final long st_mtime; /*time_t*/ + + /** StructTimespec with time of last modification. */ + public final StructTimespec st_mtim; + + /** Seconds part of time of last status change */ + public final long st_ctime; /*time_t*/ + + /** StructTimespec with time of last status change. */ + public final StructTimespec st_ctim; + + /** + * A file system-specific preferred I/O block size for this object. + * For some file system types, this may vary from file to file. + */ + public final long st_blksize; /*blksize_t*/ + + /** Number of blocks allocated for this object. */ + public final long st_blocks; /*blkcnt_t*/ + + /** + * Constructs an instance with the given field values. + */ + public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid, + long st_rdev, long st_size, long st_atime, long st_mtime, long st_ctime, + long st_blksize, long st_blocks) { + this(st_dev, st_ino, st_mode, st_nlink, st_uid, st_gid, + st_rdev, st_size, new StructTimespec(st_atime, 0L), new StructTimespec(st_mtime, 0L), + new StructTimespec(st_ctime, 0L), st_blksize, st_blocks); + } + + /** + * Constructs an instance with the given field values. + */ + public StructStat(long st_dev, long st_ino, int st_mode, long st_nlink, int st_uid, int st_gid, + long st_rdev, long st_size, StructTimespec st_atim, StructTimespec st_mtim, + StructTimespec st_ctim, long st_blksize, long st_blocks) { + this.st_dev = st_dev; + this.st_ino = st_ino; + this.st_mode = st_mode; + this.st_nlink = st_nlink; + this.st_uid = st_uid; + this.st_gid = st_gid; + this.st_rdev = st_rdev; + this.st_size = st_size; + this.st_atime = st_atim.tv_sec; + this.st_mtime = st_mtim.tv_sec; + this.st_ctime = st_ctim.tv_sec; + this.st_atim = st_atim; + this.st_mtim = st_mtim; + this.st_ctim = st_ctim; + this.st_blksize = st_blksize; + this.st_blocks = st_blocks; + } + + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructStatVfs.java b/android/system/StructStatVfs.java index 942a39a8..3b192e7c 100644 --- a/android/system/StructStatVfs.java +++ b/android/system/StructStatVfs.java @@ -22,59 +22,59 @@ import libcore.util.Objects; * File information returned by {@link Os#fstatvfs} and {@link Os#statvfs}. */ public final class StructStatVfs { - /** File system block size (used for block counts). */ - public final long f_bsize; /*unsigned long*/ + /** File system block size (used for block counts). */ + public final long f_bsize; /*unsigned long*/ - /** Fundamental file system block size. */ - public final long f_frsize; /*unsigned long*/ + /** Fundamental file system block size. */ + public final long f_frsize; /*unsigned long*/ - /** Total block count. */ - public final long f_blocks; /*fsblkcnt_t*/ + /** Total block count. */ + public final long f_blocks; /*fsblkcnt_t*/ - /** Free block count. */ - public final long f_bfree; /*fsblkcnt_t*/ + /** Free block count. */ + public final long f_bfree; /*fsblkcnt_t*/ - /** Free block count available to non-root. */ - public final long f_bavail; /*fsblkcnt_t*/ + /** Free block count available to non-root. */ + public final long f_bavail; /*fsblkcnt_t*/ - /** Total file (inode) count. */ - public final long f_files; /*fsfilcnt_t*/ + /** Total file (inode) count. */ + public final long f_files; /*fsfilcnt_t*/ - /** Free file (inode) count. */ - public final long f_ffree; /*fsfilcnt_t*/ + /** Free file (inode) count. */ + public final long f_ffree; /*fsfilcnt_t*/ - /** Free file (inode) count available to non-root. */ - public final long f_favail; /*fsfilcnt_t*/ + /** Free file (inode) count available to non-root. */ + public final long f_favail; /*fsfilcnt_t*/ - /** File system id. */ - public final long f_fsid; /*unsigned long*/ + /** File system id. */ + public final long f_fsid; /*unsigned long*/ - /** Bit mask of ST_* flags. */ - public final long f_flag; /*unsigned long*/ + /** Bit mask of ST_* flags. */ + public final long f_flag; /*unsigned long*/ - /** Maximum filename length. */ - public final long f_namemax; /*unsigned long*/ + /** Maximum filename length. */ + public final long f_namemax; /*unsigned long*/ - /** - * Constructs an instance with the given field values. - */ - public StructStatVfs(long f_bsize, long f_frsize, long f_blocks, long f_bfree, long f_bavail, - long f_files, long f_ffree, long f_favail, - long f_fsid, long f_flag, long f_namemax) { - this.f_bsize = f_bsize; - this.f_frsize = f_frsize; - this.f_blocks = f_blocks; - this.f_bfree = f_bfree; - this.f_bavail = f_bavail; - this.f_files = f_files; - this.f_ffree = f_ffree; - this.f_favail = f_favail; - this.f_fsid = f_fsid; - this.f_flag = f_flag; - this.f_namemax = f_namemax; - } + /** + * Constructs an instance with the given field values. + */ + public StructStatVfs(long f_bsize, long f_frsize, long f_blocks, long f_bfree, long f_bavail, + long f_files, long f_ffree, long f_favail, + long f_fsid, long f_flag, long f_namemax) { + this.f_bsize = f_bsize; + this.f_frsize = f_frsize; + this.f_blocks = f_blocks; + this.f_bfree = f_bfree; + this.f_bavail = f_bavail; + this.f_files = f_files; + this.f_ffree = f_ffree; + this.f_favail = f_favail; + this.f_fsid = f_fsid; + this.f_flag = f_flag; + this.f_namemax = f_namemax; + } - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructTimespec.java b/android/system/StructTimespec.java index b5e192e5..c1067805 100644 --- a/android/system/StructTimespec.java +++ b/android/system/StructTimespec.java @@ -22,58 +22,58 @@ import libcore.util.Objects;; * Corresponds to C's {@code struct timespec} from {@code <time.h>}. */ public final class StructTimespec implements Comparable<StructTimespec> { - /** Seconds part of time of last data modification. */ - public final long tv_sec; /*time_t*/ + /** Seconds part of time of last data modification. */ + public final long tv_sec; /*time_t*/ - /** Nanoseconds (values are [0, 999999999]). */ - public final long tv_nsec; + /** Nanoseconds (values are [0, 999999999]). */ + public final long tv_nsec; - public StructTimespec(long tv_sec, long tv_nsec) { - this.tv_sec = tv_sec; - this.tv_nsec = tv_nsec; - if (tv_nsec < 0 || tv_nsec > 999_999_999) { - throw new IllegalArgumentException( - "tv_nsec value " + tv_nsec + " is not in [0, 999999999]"); - } - } + public StructTimespec(long tv_sec, long tv_nsec) { + this.tv_sec = tv_sec; + this.tv_nsec = tv_nsec; + if (tv_nsec < 0 || tv_nsec > 999_999_999) { + throw new IllegalArgumentException( + "tv_nsec value " + tv_nsec + " is not in [0, 999999999]"); + } + } - @Override - public int compareTo(StructTimespec other) { - if (tv_sec > other.tv_sec) { - return 1; - } - if (tv_sec < other.tv_sec) { - return -1; - } - if (tv_nsec > other.tv_nsec) { - return 1; - } - if (tv_nsec < other.tv_nsec) { - return -1; - } - return 0; - } + @Override + public int compareTo(StructTimespec other) { + if (tv_sec > other.tv_sec) { + return 1; + } + if (tv_sec < other.tv_sec) { + return -1; + } + if (tv_nsec > other.tv_nsec) { + return 1; + } + if (tv_nsec < other.tv_nsec) { + return -1; + } + return 0; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; - StructTimespec that = (StructTimespec) o; + StructTimespec that = (StructTimespec) o; - if (tv_sec != that.tv_sec) return false; - return tv_nsec == that.tv_nsec; - } + if (tv_sec != that.tv_sec) return false; + return tv_nsec == that.tv_nsec; + } - @Override - public int hashCode() { - int result = (int) (tv_sec ^ (tv_sec >>> 32)); - result = 31 * result + (int) (tv_nsec ^ (tv_nsec >>> 32)); - return result; - } + @Override + public int hashCode() { + int result = (int) (tv_sec ^ (tv_sec >>> 32)); + result = 31 * result + (int) (tv_nsec ^ (tv_nsec >>> 32)); + return result; + } - @Override - public String toString() { - return Objects.toString(this); - } + @Override + public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructTimeval.java b/android/system/StructTimeval.java index 8a155b4f..91a6f2ae 100644 --- a/android/system/StructTimeval.java +++ b/android/system/StructTimeval.java @@ -25,28 +25,28 @@ import libcore.util.Objects; * @hide */ public final class StructTimeval { - /** Seconds. */ - public final long tv_sec; - - /** Microseconds. */ - public final long tv_usec; - - private StructTimeval(long tv_sec, long tv_usec) { - this.tv_sec = tv_sec; - this.tv_usec = tv_usec; - } - - public static StructTimeval fromMillis(long millis) { - long tv_sec = millis / 1000; - long tv_usec = (millis - (tv_sec * 1000)) * 1000; - return new StructTimeval(tv_sec, tv_usec); - } - - public long toMillis() { - return (tv_sec * 1000) + (tv_usec / 1000); - } - - @Override public String toString() { - return Objects.toString(this); - } + /** Seconds. */ + public final long tv_sec; + + /** Microseconds. */ + public final long tv_usec; + + private StructTimeval(long tv_sec, long tv_usec) { + this.tv_sec = tv_sec; + this.tv_usec = tv_usec; + } + + public static StructTimeval fromMillis(long millis) { + long tv_sec = millis / 1000; + long tv_usec = (millis - (tv_sec * 1000)) * 1000; + return new StructTimeval(tv_sec, tv_usec); + } + + public long toMillis() { + return (tv_sec * 1000) + (tv_usec / 1000); + } + + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructUcred.java b/android/system/StructUcred.java index a1e3cd6e..e6e1479d 100644 --- a/android/system/StructUcred.java +++ b/android/system/StructUcred.java @@ -24,22 +24,22 @@ import libcore.util.Objects; * @hide */ public final class StructUcred { - /** The peer's process id. */ - public final int pid; + /** The peer's process id. */ + public final int pid; - /** The peer process' uid. */ - public final int uid; + /** The peer process' uid. */ + public final int uid; - /** The peer process' gid. */ - public final int gid; + /** The peer process' gid. */ + public final int gid; - public StructUcred(int pid, int uid, int gid) { - this.pid = pid; - this.uid = uid; - this.gid = gid; - } + public StructUcred(int pid, int uid, int gid) { + this.pid = pid; + this.uid = uid; + this.gid = gid; + } - @Override public String toString() { - return Objects.toString(this); - } + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/system/StructUtsname.java b/android/system/StructUtsname.java index 37778380..9cfe21b0 100644 --- a/android/system/StructUtsname.java +++ b/android/system/StructUtsname.java @@ -23,33 +23,33 @@ import libcore.util.Objects; * Corresponds to C's {@code struct utsname} from {@code <sys/utsname.h>}. */ public final class StructUtsname { - /** The OS name, such as "Linux". */ - public final String sysname; - - /** The machine's unqualified name on some implementation-defined network. */ - public final String nodename; - - /** The OS release, such as "2.6.35-27-generic". */ - public final String release; - - /** The OS version, such as "#48-Ubuntu SMP Tue Feb 22 20:25:29 UTC 2011". */ - public final String version; - - /** The machine architecture, such as "armv7l" or "x86_64". */ - public final String machine; - - /** - * Constructs an instance with the given field values. - */ - public StructUtsname(String sysname, String nodename, String release, String version, String machine) { - this.sysname = sysname; - this.nodename = nodename; - this.release = release; - this.version = version; - this.machine = machine; - } - - @Override public String toString() { - return Objects.toString(this); - } + /** The OS name, such as "Linux". */ + public final String sysname; + + /** The machine's unqualified name on some implementation-defined network. */ + public final String nodename; + + /** The OS release, such as "2.6.35-27-generic". */ + public final String release; + + /** The OS version, such as "#48-Ubuntu SMP Tue Feb 22 20:25:29 UTC 2011". */ + public final String version; + + /** The machine architecture, such as "armv7l" or "x86_64". */ + public final String machine; + + /** + * Constructs an instance with the given field values. + */ + public StructUtsname(String sysname, String nodename, String release, String version, String machine) { + this.sysname = sysname; + this.nodename = nodename; + this.release = release; + this.version = version; + this.machine = machine; + } + + @Override public String toString() { + return Objects.toString(this); + } } diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java index 689ce954..99f8cfbf 100644 --- a/android/telephony/CarrierConfigManager.java +++ b/android/telephony/CarrierConfigManager.java @@ -763,6 +763,18 @@ public class CarrierConfigManager { public static final String KEY_CDMA_DTMF_TONE_DELAY_INT = "cdma_dtmf_tone_delay_int"; /** + * Some carriers will send call forwarding responses for voicemail in a format that is not 3gpp + * compliant, which causes issues during parsing. This causes the + * {@link com.android.internal.telephony.CallForwardInfo#number} to contain non-numerical + * characters instead of a number. + * + * If true, we will detect the non-numerical characters and replace them with "Voicemail". + * @hide + */ + public static final String KEY_CALL_FORWARDING_MAP_NON_NUMBER_TO_VOICEMAIL_BOOL = + "call_forwarding_map_non_number_to_voicemail_bool"; + + /** * Determines whether conference calls are supported by a carrier. When {@code true}, * conference calling is supported, {@code false otherwise}. */ @@ -1407,6 +1419,17 @@ public class CarrierConfigManager { "support_3gpp_call_forwarding_while_roaming_bool"; /** + * Boolean indicating whether to display voicemail number as default call forwarding number in + * call forwarding settings. + * If true, display vm number when cf number is null. + * If false, display the cf number from network. + * By default this value is false. + * @hide + */ + public static final String KEY_DISPLAY_VOICEMAIL_NUMBER_AS_DEFAULT_CALL_FORWARDING_NUMBER_BOOL = + "display_voicemail_number_as_default_call_forwarding_number"; + + /** * When {@code true}, the user will be notified when they attempt to place an international call * when the call is placed using wifi calling. * @hide @@ -1573,6 +1596,25 @@ public class CarrierConfigManager { public static final String KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL = "show_ims_registration_status_bool"; + /** + * The flag to disable the popup dialog which warns the user of data charges. + * @hide + */ + public static final String KEY_DISABLE_CHARGE_INDICATION_BOOL = + "disable_charge_indication_bool"; + + /** + * Boolean indicating whether to skip the call forwarding (CF) fail-to-disable dialog. + * The logic used to determine whether we succeeded in disabling is carrier specific, + * so the dialog may not always be accurate. + * {@code false} - show CF fail-to-disable dialog. + * {@code true} - skip showing CF fail-to-disable dialog. + * + * @hide + */ + public static final String KEY_SKIP_CF_FAIL_TO_DISABLE_DIALOG_BOOL = + "skip_cf_fail_to_disable_dialog_bool"; + /** The default value for every variable. */ private final static PersistableBundle sDefaults; @@ -1703,6 +1745,7 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_GSM_DTMF_TONE_DELAY_INT, 0); sDefaults.putInt(KEY_IMS_DTMF_TONE_DELAY_INT, 0); sDefaults.putInt(KEY_CDMA_DTMF_TONE_DELAY_INT, 100); + sDefaults.putBoolean(KEY_CALL_FORWARDING_MAP_NON_NUMBER_TO_VOICEMAIL_BOOL, false); sDefaults.putInt(KEY_CDMA_3WAYCALL_FLASH_DELAY_INT , 0); sDefaults.putBoolean(KEY_SUPPORT_CONFERENCE_CALL_BOOL, true); sDefaults.putBoolean(KEY_SUPPORT_IMS_CONFERENCE_CALL_BOOL, true); @@ -1726,6 +1769,7 @@ public class CarrierConfigManager { sDefaults.putString(KEY_CARRIER_NAME_STRING, ""); sDefaults.putBoolean(KEY_SUPPORT_DIRECT_FDN_DIALING_BOOL, false); sDefaults.putBoolean(KEY_CARRIER_DEFAULT_DATA_ROAMING_ENABLED_BOOL, false); + sDefaults.putBoolean(KEY_SKIP_CF_FAIL_TO_DISABLE_DIALOG_BOOL, false); // MMS defaults sDefaults.putBoolean(KEY_MMS_ALIAS_ENABLED_BOOL, false); @@ -1827,6 +1871,8 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_EMERGENCY_NOTIFICATION_DELAY_INT, -1); sDefaults.putBoolean(KEY_ALLOW_USSD_REQUESTS_VIA_TELEPHONY_MANAGER_BOOL, true); sDefaults.putBoolean(KEY_SUPPORT_3GPP_CALL_FORWARDING_WHILE_ROAMING_BOOL, true); + sDefaults.putBoolean(KEY_DISPLAY_VOICEMAIL_NUMBER_AS_DEFAULT_CALL_FORWARDING_NUMBER_BOOL, + false); sDefaults.putBoolean(KEY_NOTIFY_INTERNATIONAL_CALL_ON_WFC_BOOL, false); sDefaults.putStringArray(KEY_CALL_FORWARDING_BLOCKS_WHILE_ROAMING_STRING_ARRAY, null); @@ -1840,6 +1886,7 @@ public class CarrierConfigManager { sDefaults.putStringArray(KEY_NON_ROAMING_OPERATOR_STRING_ARRAY, null); sDefaults.putStringArray(KEY_ROAMING_OPERATOR_STRING_ARRAY, null); sDefaults.putBoolean(KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false); + sDefaults.putBoolean(KEY_DISABLE_CHARGE_INDICATION_BOOL, false); } /** diff --git a/android/telephony/MbmsDownloadSession.java b/android/telephony/MbmsDownloadSession.java index 764b7b22..9a9877a8 100644 --- a/android/telephony/MbmsDownloadSession.java +++ b/android/telephony/MbmsDownloadSession.java @@ -77,8 +77,9 @@ public class MbmsDownloadSession implements AutoCloseable { * Integer extra that Android will attach to the intent supplied via * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} * Indicates the result code of the download. One of - * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, {@link #RESULT_CANCELLED}, or - * {@link #RESULT_IO_ERROR}. + * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, {@link #RESULT_CANCELLED}, + * {@link #RESULT_IO_ERROR}, {@link #RESULT_DOWNLOAD_FAILURE}, {@link #RESULT_OUT_OF_STORAGE}, + * {@link #RESULT_SERVICE_ID_NOT_DEFINED}, or {@link #RESULT_FILE_ROOT_UNREACHABLE}. * * This extra may also be used by the middleware when it is sending intents to the app. */ @@ -142,11 +143,41 @@ public class MbmsDownloadSession implements AutoCloseable { /** * Indicates that the download will not be completed due to an I/O error incurred while - * writing to temp files. This commonly indicates that the device is out of storage space, - * but may indicate other conditions as well (such as an SD card being removed). + * writing to temp files. + * + * This is likely a transient error and another {@link DownloadRequest} should be sent to try + * the download again. */ public static final int RESULT_IO_ERROR = 4; - // TODO - more results! + + /** + * Indicates that the Service ID specified in the {@link DownloadRequest} is incorrect due to + * the Id being incorrect, stale, expired, or similar. + */ + public static final int RESULT_SERVICE_ID_NOT_DEFINED = 5; + + /** + * Indicates that there was an error while processing downloaded files, such as a file repair or + * file decoding error and is not due to a file I/O error. + * + * This is likely a transient error and another {@link DownloadRequest} should be sent to try + * the download again. + */ + public static final int RESULT_DOWNLOAD_FAILURE = 6; + + /** + * Indicates that the file system is full and the {@link DownloadRequest} can not complete. + * Either space must be made on the current file system or the temp file root location must be + * changed to a location that is not full to download the temp files. + */ + public static final int RESULT_OUT_OF_STORAGE = 7; + + /** + * Indicates that the file root that was set is currently unreachable. This can happen if the + * temp files are set to be stored on external storage and the SD card was removed, for example. + * The temp file root should be changed before sending another DownloadRequest. + */ + public static final int RESULT_FILE_ROOT_UNREACHABLE = 8; /** @hide */ @Retention(RetentionPolicy.SOURCE) diff --git a/android/telephony/NetworkScanRequest.java b/android/telephony/NetworkScanRequest.java index d2aef200..9674c930 100644 --- a/android/telephony/NetworkScanRequest.java +++ b/android/telephony/NetworkScanRequest.java @@ -19,6 +19,7 @@ package android.telephony; import android.os.Parcel; import android.os.Parcelable; +import java.util.ArrayList; import java.util.Arrays; /** @@ -38,6 +39,20 @@ public final class NetworkScanRequest implements Parcelable { public static final int MAX_BANDS = 8; /** @hide */ public static final int MAX_CHANNELS = 32; + /** @hide */ + public static final int MAX_MCC_MNC_LIST_SIZE = 20; + /** @hide */ + public static final int MIN_SEARCH_PERIODICITY_SEC = 5; + /** @hide */ + public static final int MAX_SEARCH_PERIODICITY_SEC = 300; + /** @hide */ + public static final int MIN_SEARCH_MAX_SEC = 60; + /** @hide */ + public static final int MAX_SEARCH_MAX_SEC = 3600; + /** @hide */ + public static final int MIN_INCREMENTAL_PERIODICITY_SEC = 1; + /** @hide */ + public static final int MAX_INCREMENTAL_PERIODICITY_SEC = 10; /** Performs the scan only once */ public static final int SCAN_TYPE_ONE_SHOT = 0; @@ -46,24 +61,84 @@ public final class NetworkScanRequest implements Parcelable { * * The modem will start new scans periodically, and the interval between two scans is usually * multiple minutes. - * */ + */ public static final int SCAN_TYPE_PERIODIC = 1; /** Defines the type of the scan. */ public int scanType; + /** + * Search periodicity (in seconds). + * Expected range for the input is [5s - 300s] + * This value must be less than or equal to maxSearchTime + */ + public int searchPeriodicity; + + /** + * Maximum duration of the periodic search (in seconds). + * Expected range for the input is [60s - 3600s] + * If the search lasts this long, it will be terminated. + */ + public int maxSearchTime; + + /** + * Indicates whether the modem should report incremental + * results of the network scan to the client. + * FALSE – Incremental results are not reported. + * TRUE (default) – Incremental results are reported + */ + public boolean incrementalResults; + + /** + * Indicates the periodicity with which the modem should + * report incremental results to the client (in seconds). + * Expected range for the input is [1s - 10s] + * This value must be less than or equal to maxSearchTime + */ + public int incrementalResultsPeriodicity; + /** Describes the radio access technologies with bands or channels that need to be scanned. */ public RadioAccessSpecifier[] specifiers; /** + * Describes the List of PLMN ids (MCC-MNC) + * If any PLMN of this list is found, search should end at that point and + * results with all PLMN found till that point should be sent as response. + * If list not sent, search to be completed till end and all PLMNs found to be reported. + * Max size of array is MAX_MCC_MNC_LIST_SIZE + */ + public ArrayList<String> mccMncs; + + /** * Creates a new NetworkScanRequest with scanType and network specifiers * * @param scanType The type of the scan * @param specifiers the radio network with bands / channels to be scanned + * @param searchPeriodicity Search periodicity (in seconds) + * @param maxSearchTime Maximum duration of the periodic search (in seconds) + * @param incrementalResults Indicates whether the modem should report incremental + * results of the network scan to the client + * @param incrementalResultsPeriodicity Indicates the periodicity with which the modem should + * report incremental results to the client (in seconds) + * @param mccMncs Describes the List of PLMN ids (MCC-MNC) */ - public NetworkScanRequest(int scanType, RadioAccessSpecifier[] specifiers) { + public NetworkScanRequest(int scanType, RadioAccessSpecifier[] specifiers, + int searchPeriodicity, + int maxSearchTime, + boolean incrementalResults, + int incrementalResultsPeriodicity, + ArrayList<String> mccMncs) { this.scanType = scanType; this.specifiers = specifiers; + this.searchPeriodicity = searchPeriodicity; + this.maxSearchTime = maxSearchTime; + this.incrementalResults = incrementalResults; + this.incrementalResultsPeriodicity = incrementalResultsPeriodicity; + if (mccMncs != null) { + this.mccMncs = mccMncs; + } else { + this.mccMncs = new ArrayList<>(); + } } @Override @@ -75,6 +150,11 @@ public final class NetworkScanRequest implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(scanType); dest.writeParcelableArray(specifiers, flags); + dest.writeInt(searchPeriodicity); + dest.writeInt(maxSearchTime); + dest.writeBoolean(incrementalResults); + dest.writeInt(incrementalResultsPeriodicity); + dest.writeStringList(mccMncs); } private NetworkScanRequest(Parcel in) { @@ -82,6 +162,12 @@ public final class NetworkScanRequest implements Parcelable { specifiers = (RadioAccessSpecifier[]) in.readParcelableArray( Object.class.getClassLoader(), RadioAccessSpecifier.class); + searchPeriodicity = in.readInt(); + maxSearchTime = in.readInt(); + incrementalResults = in.readBoolean(); + incrementalResultsPeriodicity = in.readInt(); + mccMncs = new ArrayList<>(); + in.readStringList(mccMncs); } @Override @@ -99,13 +185,24 @@ public final class NetworkScanRequest implements Parcelable { } return (scanType == nsr.scanType - && Arrays.equals(specifiers, nsr.specifiers)); + && Arrays.equals(specifiers, nsr.specifiers) + && searchPeriodicity == nsr.searchPeriodicity + && maxSearchTime == nsr.maxSearchTime + && incrementalResults == nsr.incrementalResults + && incrementalResultsPeriodicity == nsr.incrementalResultsPeriodicity + && (((mccMncs != null) + && mccMncs.equals(nsr.mccMncs)))); } @Override public int hashCode () { return ((scanType * 31) - + (Arrays.hashCode(specifiers)) * 37); + + (Arrays.hashCode(specifiers)) * 37 + + (searchPeriodicity * 41) + + (maxSearchTime * 43) + + ((incrementalResults == true? 1 : 0) * 47) + + (incrementalResultsPeriodicity * 53) + + (mccMncs.hashCode() * 59)); } public static final Creator<NetworkScanRequest> CREATOR = diff --git a/android/telephony/ServiceState.java b/android/telephony/ServiceState.java index e448fb2a..116e711e 100644 --- a/android/telephony/ServiceState.java +++ b/android/telephony/ServiceState.java @@ -1197,15 +1197,6 @@ public class ServiceState implements Parcelable { } } - /** - * @Deprecated to be removed Q3 2013 use {@link #getVoiceNetworkType} - * @hide - */ - public int getNetworkType() { - Rlog.e(LOG_TAG, "ServiceState.getNetworkType() DEPRECATED will be removed *******"); - return rilRadioTechnologyToNetworkType(mRilVoiceRadioTechnology); - } - /** @hide */ public int getDataNetworkType() { return rilRadioTechnologyToNetworkType(mRilDataRadioTechnology); diff --git a/android/telephony/SignalStrength.java b/android/telephony/SignalStrength.java index 9e023993..c8b47765 100644 --- a/android/telephony/SignalStrength.java +++ b/android/telephony/SignalStrength.java @@ -19,7 +19,6 @@ package android.telephony; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.telephony.Rlog; import android.util.Log; import android.content.res.Resources; @@ -429,6 +428,15 @@ public class SignalStrength implements Parcelable { } /** + * Fix {@link #isGsm} based on the signal strength data. + * + * @hide + */ + public void fixType() { + isGsm = getCdmaRelatedSignalStrength() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + } + + /** * @param true - Gsm, Lte phones * false - Cdma phones * @@ -541,30 +549,7 @@ public class SignalStrength implements Parcelable { * while 4 represents a very strong signal strength. */ public int getLevel() { - int level = 0; - - if (isGsm) { - level = getLteLevel(); - if (level == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { - level = getTdScdmaLevel(); - if (level == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { - level = getGsmLevel(); - } - } - } else { - int cdmaLevel = getCdmaLevel(); - int evdoLevel = getEvdoLevel(); - if (evdoLevel == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { - /* We don't know evdo, use cdma */ - level = cdmaLevel; - } else if (cdmaLevel == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { - /* We don't know cdma, use evdo */ - level = evdoLevel; - } else { - /* We know both, use the lowest level */ - level = cdmaLevel < evdoLevel ? cdmaLevel : evdoLevel; - } - } + int level = isGsm ? getGsmRelatedSignalStrength() : getCdmaRelatedSignalStrength(); if (DBG) log("getLevel=" + level); return level; } @@ -1049,6 +1034,36 @@ public class SignalStrength implements Parcelable { + " " + (isGsm ? "gsm|lte" : "cdma")); } + /** Returns the signal strength related to GSM. */ + private int getGsmRelatedSignalStrength() { + int level = getLteLevel(); + if (level == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { + level = getTdScdmaLevel(); + if (level == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { + level = getGsmLevel(); + } + } + return level; + } + + /** Returns the signal strength related to CDMA. */ + private int getCdmaRelatedSignalStrength() { + int level; + int cdmaLevel = getCdmaLevel(); + int evdoLevel = getEvdoLevel(); + if (evdoLevel == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { + /* We don't know evdo, use cdma */ + level = cdmaLevel; + } else if (cdmaLevel == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { + /* We don't know cdma, use evdo */ + level = evdoLevel; + } else { + /* We know both, use the lowest level */ + level = cdmaLevel < evdoLevel ? cdmaLevel : evdoLevel; + } + return level; + } + /** * Set SignalStrength based on intent notifier map * diff --git a/android/telephony/mbms/DownloadRequest.java b/android/telephony/mbms/DownloadRequest.java index 5a57f322..f0d60b68 100644 --- a/android/telephony/mbms/DownloadRequest.java +++ b/android/telephony/mbms/DownloadRequest.java @@ -16,6 +16,7 @@ package android.telephony.mbms; +import android.annotation.NonNull; import android.annotation.SystemApi; import android.content.Intent; import android.net.Uri; @@ -26,7 +27,6 @@ import android.util.Log; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @@ -71,6 +71,19 @@ public final class DownloadRequest implements Parcelable { private String appIntent; private int version = CURRENT_VERSION; + + /** + * Builds a new DownloadRequest. + * @param sourceUri the source URI for the DownloadRequest to be built. This URI should + * never be null. + */ + public Builder(@NonNull Uri sourceUri) { + if (sourceUri == null) { + throw new IllegalArgumentException("Source URI must be non-null."); + } + source = sourceUri; + } + /** * Sets the service from which the download request to be built will download from. * @param serviceInfo @@ -92,15 +105,6 @@ public final class DownloadRequest implements Parcelable { } /** - * Sets the source URI for the download request to be built. - * @param source - */ - public Builder setSource(Uri source) { - this.source = source; - return this; - } - - /** * Set the subscription ID on which the file(s) should be downloaded. * @param subscriptionId */ @@ -316,9 +320,11 @@ public final class DownloadRequest implements Parcelable { throw new RuntimeException("Could not get sha256 hash object"); } if (version >= 1) { - // Hash the source URI, destination URI, and the app intent + // Hash the source URI and the app intent digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8)); - digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8)); + if (serializedResultIntentForApp != null) { + digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8)); + } } // Add updates for future versions here return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP); diff --git a/android/telephony/mbms/MbmsDownloadReceiver.java b/android/telephony/mbms/MbmsDownloadReceiver.java index fe275372..9af1eb9e 100644 --- a/android/telephony/mbms/MbmsDownloadReceiver.java +++ b/android/telephony/mbms/MbmsDownloadReceiver.java @@ -287,7 +287,7 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { return; } - List<Uri> tempFiles = intent.getParcelableExtra(VendorUtils.EXTRA_TEMP_LIST); + List<Uri> tempFiles = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_LIST); if (tempFiles == null) { return; } @@ -309,7 +309,7 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { return; } int fdCount = intent.getIntExtra(VendorUtils.EXTRA_FD_COUNT, 0); - List<Uri> pausedList = intent.getParcelableExtra(VendorUtils.EXTRA_PAUSED_LIST); + List<Uri> pausedList = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_PAUSED_LIST); if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) { Log.i(LOG_TAG, "No temp files actually requested. Ending."); @@ -492,9 +492,14 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException("Package manager couldn't find " + context.getPackageName()); } + if (appInfo.metaData == null) { + throw new RuntimeException("App must declare the file provider authority as metadata " + + "in the manifest."); + } String authority = appInfo.metaData.getString(MBMS_FILE_PROVIDER_META_DATA_KEY); if (authority == null) { - throw new RuntimeException("Must declare the file provider authority as meta data"); + throw new RuntimeException("App must declare the file provider authority as metadata " + + "in the manifest."); } return authority; } diff --git a/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java b/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java index 2f85a1df..9ccdd56f 100644 --- a/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java +++ b/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java @@ -113,15 +113,13 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { @Override public final int initialize(final int subscriptionId, final IMbmsDownloadSessionCallback callback) throws RemoteException { + if (callback == null) { + throw new NullPointerException("Callback must not be null"); + } + final int uid = Binder.getCallingUid(); - callback.asBinder().linkToDeath(new DeathRecipient() { - @Override - public void binderDied() { - onAppCallbackDied(uid, subscriptionId); - } - }, 0); - return initialize(subscriptionId, new MbmsDownloadSessionCallback() { + int result = initialize(subscriptionId, new MbmsDownloadSessionCallback() { @Override public void onError(int errorCode, String message) { try { @@ -149,6 +147,17 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { } } }); + + if (result == MbmsErrors.SUCCESS) { + callback.asBinder().linkToDeath(new DeathRecipient() { + @Override + public void binderDied() { + onAppCallbackDied(uid, subscriptionId); + } + }, 0); + } + + return result; } /** @@ -240,16 +249,12 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { public final int registerStateCallback(final DownloadRequest downloadRequest, final IDownloadStateCallback callback, int flags) throws RemoteException { final int uid = Binder.getCallingUid(); - DeathRecipient deathRecipient = new DeathRecipient() { - @Override - public void binderDied() { - onAppCallbackDied(uid, downloadRequest.getSubscriptionId()); - mDownloadCallbackBinderMap.remove(callback.asBinder()); - mDownloadCallbackDeathRecipients.remove(callback.asBinder()); - } - }; - mDownloadCallbackDeathRecipients.put(callback.asBinder(), deathRecipient); - callback.asBinder().linkToDeath(deathRecipient, 0); + if (downloadRequest == null) { + throw new NullPointerException("Download request must not be null"); + } + if (callback == null) { + throw new NullPointerException("Callback must not be null"); + } DownloadStateCallback exposedCallback = new FilteredDownloadStateCallback(callback, flags) { @Override @@ -258,9 +263,23 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { } }; - mDownloadCallbackBinderMap.put(callback.asBinder(), exposedCallback); + int result = registerStateCallback(downloadRequest, exposedCallback); - return registerStateCallback(downloadRequest, exposedCallback); + if (result == MbmsErrors.SUCCESS) { + DeathRecipient deathRecipient = new DeathRecipient() { + @Override + public void binderDied() { + onAppCallbackDied(uid, downloadRequest.getSubscriptionId()); + mDownloadCallbackBinderMap.remove(callback.asBinder()); + mDownloadCallbackDeathRecipients.remove(callback.asBinder()); + } + }; + mDownloadCallbackDeathRecipients.put(callback.asBinder(), deathRecipient); + callback.asBinder().linkToDeath(deathRecipient, 0); + mDownloadCallbackBinderMap.put(callback.asBinder(), exposedCallback); + } + + return result; } /** @@ -292,6 +311,13 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { public final int unregisterStateCallback( final DownloadRequest downloadRequest, final IDownloadStateCallback callback) throws RemoteException { + if (downloadRequest == null) { + throw new NullPointerException("Download request must not be null"); + } + if (callback == null) { + throw new NullPointerException("Callback must not be null"); + } + DeathRecipient deathRecipient = mDownloadCallbackDeathRecipients.remove(callback.asBinder()); if (deathRecipient == null) { diff --git a/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java b/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java index f8f370a5..a2381536 100644 --- a/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java +++ b/android/telephony/mbms/vendor/MbmsStreamingServiceBase.java @@ -65,15 +65,13 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { @Override public final int initialize(final IMbmsStreamingSessionCallback callback, final int subscriptionId) throws RemoteException { + if (callback == null) { + throw new NullPointerException("Callback must not be null"); + } + final int uid = Binder.getCallingUid(); - callback.asBinder().linkToDeath(new DeathRecipient() { - @Override - public void binderDied() { - onAppCallbackDied(uid, subscriptionId); - } - }, 0); - return initialize(new MbmsStreamingSessionCallback() { + int result = initialize(new MbmsStreamingSessionCallback() { @Override public void onError(final int errorCode, final String message) { try { @@ -101,6 +99,17 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { } } }, subscriptionId); + + if (result == MbmsErrors.SUCCESS) { + callback.asBinder().linkToDeath(new DeathRecipient() { + @Override + public void binderDied() { + onAppCallbackDied(uid, subscriptionId); + } + }, 0); + } + + return result; } @@ -152,15 +161,13 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { @Override public int startStreaming(final int subscriptionId, String serviceId, final IStreamingServiceCallback callback) throws RemoteException { + if (callback == null) { + throw new NullPointerException("Callback must not be null"); + } + final int uid = Binder.getCallingUid(); - callback.asBinder().linkToDeath(new DeathRecipient() { - @Override - public void binderDied() { - onAppCallbackDied(uid, subscriptionId); - } - }, 0); - return startStreaming(subscriptionId, serviceId, new StreamingServiceCallback() { + int result = startStreaming(subscriptionId, serviceId, new StreamingServiceCallback() { @Override public void onError(final int errorCode, final String message) { try { @@ -207,6 +214,17 @@ public class MbmsStreamingServiceBase extends IMbmsStreamingService.Stub { } } }); + + if (result == MbmsErrors.SUCCESS) { + callback.asBinder().linkToDeath(new DeathRecipient() { + @Override + public void binderDied() { + onAppCallbackDied(uid, subscriptionId); + } + }, 0); + } + + return result; } /** diff --git a/android/text/AndroidBidi.java b/android/text/AndroidBidi.java index bbe15232..179d545f 100644 --- a/android/text/AndroidBidi.java +++ b/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/android/text/AndroidBidi_Delegate.java b/android/text/AndroidBidi_Delegate.java deleted file mode 100644 index 38171dc0..00000000 --- a/android/text/AndroidBidi_Delegate.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.text; - -import com.android.ide.common.rendering.api.LayoutLog; -import com.android.layoutlib.bridge.Bridge; -import com.android.tools.layoutlib.annotations.LayoutlibDelegate; - -import android.icu.text.Bidi; - -/** - * Delegate used to provide new implementation for the native methods of {@link AndroidBidi} - * - * Through the layoutlib_create tool, the original methods of AndroidBidi have been replaced - * by calls to methods of the same name in this delegate class. - * - */ -public class AndroidBidi_Delegate { - - @LayoutlibDelegate - /*package*/ static int runBidi(int dir, char[] chars, byte[] charInfo, int count, - boolean haveInfo) { - - switch (dir) { - case 0: // Layout.DIR_REQUEST_LTR - dir = Bidi.LTR; - break; - case 1: // Layout.DIR_REQUEST_RTL - dir = Bidi.RTL; - break; - case -1: // Layout.DIR_REQUEST_DEFAULT_RTL - dir = Bidi.LEVEL_DEFAULT_RTL; - break; - case -2: // Layout.DIR_REQUEST_DEFAULT_LTR - dir = Bidi.LEVEL_DEFAULT_LTR; - break; - default: - // Invalid code. Log error, assume LEVEL_DEFAULT_LTR and continue. - Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Invalid direction flag", null); - dir = Bidi.LEVEL_DEFAULT_LTR; - } - Bidi bidi = new Bidi(chars, 0, null, 0, count, dir); - if (charInfo != null) { - for (int i = 0; i < count; ++i) - charInfo[i] = bidi.getLevelAt(i); - } - return bidi.getParaLevel(); - } -} diff --git a/android/text/BoringLayoutCreateDrawPerfTest.java b/android/text/BoringLayoutCreateDrawPerfTest.java index 47dd257b..586c3852 100644 --- a/android/text/BoringLayoutCreateDrawPerfTest.java +++ b/android/text/BoringLayoutCreateDrawPerfTest.java @@ -46,7 +46,7 @@ public class BoringLayoutCreateDrawPerfTest { private static final float SPACING_ADD = 10f; private static final float SPACING_MULT = 1.5f; - @Parameterized.Parameters(name = "cached={3},{1} chars,{0}") + @Parameterized.Parameters(name = "cached={3},{1}chars,{0}") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/android/text/BoringLayoutIsBoringPerfTest.java b/android/text/BoringLayoutIsBoringPerfTest.java index 34de65de..9d11f295 100644 --- a/android/text/BoringLayoutIsBoringPerfTest.java +++ b/android/text/BoringLayoutIsBoringPerfTest.java @@ -40,7 +40,7 @@ public class BoringLayoutIsBoringPerfTest { private static final boolean[] BOOLEANS = new boolean[]{false, true}; - @Parameterized.Parameters(name = "cached={4},{1} chars,{0}") + @Parameterized.Parameters(name = "cached={4},{1}chars,{0}") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/android/text/DynamicLayout.java b/android/text/DynamicLayout.java index 24260c4f..fba358cf 100644 --- a/android/text/DynamicLayout.java +++ b/android/text/DynamicLayout.java @@ -299,7 +299,7 @@ public class DynamicLayout extends Layout private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); - private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); + private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3); } /** @@ -440,7 +440,7 @@ public class DynamicLayout extends Layout mEllipsizeAt = null; } - mObjects = new PackedObjectVector<Directions>(1); + 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. @@ -1050,7 +1050,7 @@ 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) { diff --git a/android/text/Hyphenator.java b/android/text/Hyphenator.java index ad26f23a..4f1488e1 100644 --- a/android/text/Hyphenator.java +++ b/android/text/Hyphenator.java @@ -16,258 +16,15 @@ package android.text; -import android.annotation.IntRange; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; -import android.util.Log; - -import com.android.internal.annotations.GuardedBy; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -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 { - private static String TAG = "Hyphenator"; - - private final static Object sLock = new Object(); - - @GuardedBy("sLock") - final static HashMap<Locale, Hyphenator> sMap = new HashMap<Locale, Hyphenator>(); - - private final long mNativePtr; - private final HyphenationData mData; - - private Hyphenator(long nativePtr, HyphenationData data) { - mNativePtr = nativePtr; - mData = data; - } - - 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) { - return putAlias(locale, result); - } - } - - // Fall back to language-only, if available - final Locale languageOnlyLocale = new Locale(locale.getLanguage()); - result = sMap.get(languageOnlyLocale); - if (result != null) { - return putAlias(locale, 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) { - return putAlias(locale, result); - } - } - - return putEmptyAlias(locale); - } - } - - private static class HyphenationData { - private static final String SYSTEM_HYPHENATOR_LOCATION = "/system/usr/hyphen-data"; - - public final int mMinPrefix, mMinSuffix; - public final long mDataAddress; - - // 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; - - public static final HyphenationData sEmptyData = - new HyphenationData(DEFAULT_MIN_PREFIX, DEFAULT_MIN_SUFFIX); - - // Create empty HyphenationData. - private HyphenationData(int minPrefix, int minSuffix) { - mMinPrefix = minPrefix; - mMinSuffix = minSuffix; - mDataAddress = 0; - } - - HyphenationData(String languageTag, int minPrefix, int minSuffix) { - mMinPrefix = minPrefix; - mMinSuffix = minSuffix; - - final String patternFilename = "hyph-" + languageTag.toLowerCase(Locale.US) + ".hyb"; - final File patternFile = new File(SYSTEM_HYPHENATOR_LOCATION, patternFilename); - if (!patternFile.canRead()) { - Log.e(TAG, "hyphenation patterns for " + patternFile + " not found or unreadable"); - mDataAddress = 0; - } else { - long address; - try (RandomAccessFile f = new RandomAccessFile(patternFile, "r")) { - address = Os.mmap(0, f.length(), OsConstants.PROT_READ, - OsConstants.MAP_SHARED, f.getFD(), 0 /* offset */); - } catch (IOException | ErrnoException e) { - Log.e(TAG, "error loading hyphenation " + patternFile, e); - address = 0; - } - mDataAddress = address; - } - } - } - - // Do not call this method outside of init method. - private static Hyphenator putNewHyphenator(Locale loc, HyphenationData data) { - final Hyphenator hyphenator = new Hyphenator(nBuildHyphenator( - data.mDataAddress, loc.getLanguage(), data.mMinPrefix, data.mMinSuffix), data); - sMap.put(loc, hyphenator); - return hyphenator; - } - - // Do not call this method outside of init method. - private static void loadData(String langTag, int minPrefix, int maxPrefix) { - final HyphenationData data = new HyphenationData(langTag, minPrefix, maxPrefix); - putNewHyphenator(Locale.forLanguageTag(langTag), data); - } - - // Caller must acquire sLock before calling this method. - // The Hyphenator for the baseLangTag must exists. - private static Hyphenator addAliasByTag(String langTag, String baseLangTag) { - return putAlias(Locale.forLanguageTag(langTag), - sMap.get(Locale.forLanguageTag(baseLangTag))); - } - - // Caller must acquire sLock before calling this method. - private static Hyphenator putAlias(Locale locale, Hyphenator base) { - return putNewHyphenator(locale, base.mData); - } - - // Caller must acquire sLock before calling this method. - private static Hyphenator putEmptyAlias(Locale locale) { - return putNewHyphenator(locale, HyphenationData.sEmptyData); - } - - // 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; - - /** - * 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() { - synchronized (sLock) { - sMap.put(null, null); - - loadData("as", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Assamese - loadData("bg", 2, 2); // Bulgarian - loadData("bn", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Bengali - loadData("cu", 1, 2); // Church Slavonic - loadData("cy", 2, 3); // Welsh - loadData("da", 2, 2); // Danish - loadData("de-1901", 2, 2); // German 1901 orthography - loadData("de-1996", 2, 2); // German 1996 orthography - loadData("de-CH-1901", 2, 2); // Swiss High German 1901 orthography - loadData("en-GB", 2, 3); // British English - loadData("en-US", 2, 3); // American English - loadData("es", 2, 2); // Spanish - loadData("et", 2, 3); // Estonian - loadData("eu", 2, 2); // Basque - loadData("fr", 2, 3); // French - loadData("ga", 2, 3); // Irish - loadData("gu", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Gujarati - loadData("hi", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Hindi - loadData("hr", 2, 2); // Croatian - loadData("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. - loadData("hy", 2, 2); // Armenian - loadData("kn", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Kannada - loadData("ml", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Malayalam - loadData("mn-Cyrl", 2, 2); // Mongolian in Cyrillic script - loadData("mr", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Marathi - loadData("nb", 2, 2); // Norwegian BokmÃ¥l - loadData("nn", 2, 2); // Norwegian Nynorsk - loadData("or", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Oriya - loadData("pa", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Punjabi - loadData("pt", 2, 3); // Portuguese - loadData("sl", 2, 2); // Slovenian - loadData("ta", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Tamil - loadData("te", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX); // Telugu - loadData("tk", 2, 2); // Turkmen - loadData("und-Ethi", 1, 1); // Any language in Ethiopic script - - // 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. - addAliasByTag("en-AS", "en-US"); // English (American Samoa) - addAliasByTag("en-GU", "en-US"); // English (Guam) - addAliasByTag("en-MH", "en-US"); // English (Marshall Islands) - addAliasByTag("en-MP", "en-US"); // English (Northern Mariana Islands) - addAliasByTag("en-PR", "en-US"); // English (Puerto Rico) - addAliasByTag("en-UM", "en-US"); // English (United States Minor Outlying Islands) - addAliasByTag("en-VI", "en-US"); // English (Virgin Islands) - - // All English locales other than those falling back to en-US are mapped to en-GB. - addAliasByTag("en", "en-GB"); - - // For German, we're assuming the 1996 (and later) orthography by default. - addAliasByTag("de", "de-1996"); - // Liechtenstein uses the Swiss hyphenation rules for the 1901 orthography. - addAliasByTag("de-LI-1901", "de-CH-1901"); - - // Norwegian is very probably Norwegian BokmÃ¥l. - addAliasByTag("no", "nb"); - - // Use mn-Cyrl. According to CLDR's likelySubtags.xml, mn is most likely to be mn-Cyrl. - addAliasByTag("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(). - addAliasByTag("am", "und-Ethi"); // Amharic - addAliasByTag("byn", "und-Ethi"); // Blin - addAliasByTag("gez", "und-Ethi"); // GeÊ»ez - addAliasByTag("ti", "und-Ethi"); // Tigrinya - addAliasByTag("wal", "und-Ethi"); // Wolaytta - } - }; - - private static native long nBuildHyphenator(/* non-zero */ long dataAddress, - @NonNull String langTag, @IntRange(from = 1) int minPrefix, - @IntRange(from = 1) int minSuffix); + nInit(); + } + private static native void nInit(); } diff --git a/android/text/Layout.java b/android/text/Layout.java index 60fff738..ac5c2e92 100644 --- a/android/text/Layout.java +++ b/android/text/Layout.java @@ -319,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; @@ -371,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; @@ -1423,7 +1417,6 @@ public abstract class Layout { float dist = Math.abs(getHorizontal(max, primary) - horiz); if (dist <= bestdist) { - bestdist = dist; best = max; } @@ -1570,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; } @@ -1894,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]); diff --git a/android/text/MeasuredText.java b/android/text/MeasuredText.java index b09ccc29..ffc44a72 100644 --- a/android/text/MeasuredText.java +++ b/android/text/MeasuredText.java @@ -106,8 +106,8 @@ class MeasuredText { if (mWidths == null || mWidths.length < len) { mWidths = ArrayUtils.newUnpaddedFloatArray(len); } - if (mChars == null || mChars.length < len) { - mChars = ArrayUtils.newUnpaddedCharArray(len); + if (mChars == null || mChars.length != len) { + mChars = new char[len]; } TextUtils.getChars(text, start, end, mChars, 0); @@ -151,7 +151,7 @@ class MeasuredText { boolean isRtl = textDir.isRtl(mChars, 0, len); bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR; } - mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels, len, false); + mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels); mEasy = false; } } diff --git a/android/text/PaintMeasureDrawPerfTest.java b/android/text/PaintMeasureDrawPerfTest.java index 00b60add..67687985 100644 --- a/android/text/PaintMeasureDrawPerfTest.java +++ b/android/text/PaintMeasureDrawPerfTest.java @@ -42,7 +42,7 @@ public class PaintMeasureDrawPerfTest { private static final boolean[] BOOLEANS = new boolean[]{false, true}; - @Parameterized.Parameters(name = "cached={1},{0} chars") + @Parameterized.Parameters(name = "cached={1},{0}chars") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/android/text/StaticLayout.java b/android/text/StaticLayout.java index 961cd8ee..5c60188d 100644 --- a/android/text/StaticLayout.java +++ b/android/text/StaticLayout.java @@ -21,21 +21,18 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Paint; -import android.os.LocaleList; 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.Pair; import android.util.Pools.SynchronizedPool; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import java.util.Arrays; -import java.util.Locale; /** * StaticLayout is a Layout for text that will not be edited after it @@ -101,7 +98,6 @@ public class StaticLayout extends Layout { b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; - b.mLocales = null; b.mMeasuredText = MeasuredText.obtain(); return b; @@ -118,7 +114,6 @@ public class StaticLayout extends Layout { b.mMeasuredText = null; b.mLeftIndents = null; b.mRightIndents = null; - b.mLocales = null; b.mLeftPaddings = null; b.mRightPaddings = null; nFinishBuilder(b.mNativePtr); @@ -409,17 +404,6 @@ public class StaticLayout extends Layout { return this; } - @NonNull - private long[] getHyphenators(@NonNull 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. @@ -433,35 +417,17 @@ public class StaticLayout extends Layout { * + addStyleRun (a text run, to be measured in 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. */ - private Pair<String, long[]> getLocaleAndHyphenatorIfChanged(TextPaint paint) { - final LocaleList locales = paint.getTextLocales(); - final String languageTags; - long[] hyphenators; - if (!locales.equals(mLocales)) { - mLocales = locales; - return new Pair(locales.toLanguageTags(), getHyphenators(locales)); - } else { - // passing null means keep current locale. - // TODO: move locale change detection to native. - return new Pair(null, null); - } - } - /* package */ void addStyleRun(TextPaint paint, int start, int end, boolean isRtl) { - Pair<String, long[]> locHyph = getLocaleAndHyphenatorIfChanged(paint); - nAddStyleRun(mNativePtr, paint.getNativeInstance(), start, end, isRtl, locHyph.first, - locHyph.second); + nAddStyleRun(mNativePtr, paint.getNativeInstance(), start, end, isRtl); } /* package */ void addReplacementRun(TextPaint paint, int start, int end, float width) { - Pair<String, long[]> locHyph = getLocaleAndHyphenatorIfChanged(paint); - nAddReplacementRun(mNativePtr, start, end, width, locHyph.first, locHyph.second); + nAddReplacementRun(mNativePtr, paint.getNativeInstance(), start, end, width); } /** @@ -519,9 +485,7 @@ public class StaticLayout extends Layout { // This will go away and be subsumed by native builder code private MeasuredText mMeasuredText; - private LocaleList mLocales; - - private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); + private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3); } public StaticLayout(CharSequence source, TextPaint paint, @@ -810,9 +774,6 @@ public class StaticLayout extends Layout { } } - // TODO: Move locale tracking code to native. - b.mLocales = null; // Reset the locale tracking. - nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart, firstWidth, firstWidthLineCount, restWidth, variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency, @@ -866,10 +827,9 @@ public class StaticLayout extends Layout { spanEndCacheCount++; } - nGetWidths(b.mNativePtr, widths); int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks, lineBreaks.widths, lineBreaks.ascents, lineBreaks.descents, lineBreaks.flags, - lineBreaks.breaks.length); + lineBreaks.breaks.length, widths); final int[] breaks = lineBreaks.breaks; final float[] lineWidths = lineBreaks.widths; @@ -947,10 +907,10 @@ public class StaticLayout extends Layout { boolean moreChars = (endPos < bufEnd); final int ascent = fallbackLineSpacing - ? Math.min(fmAscent, (int) Math.round(ascents[breakIndex])) + ? Math.min(fmAscent, Math.round(ascents[breakIndex])) : fmAscent; final int descent = fallbackLineSpacing - ? Math.max(fmDescent, (int) Math.round(descents[breakIndex])) + ? Math.max(fmDescent, Math.round(descents[breakIndex])) : fmDescent; v = out(source, here, endPos, ascent, descent, fmTop, fmBottom, @@ -1177,7 +1137,7 @@ public class StaticLayout extends Layout { mWorkPaint.set(paint); do { final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths, - widthStart, tempAvail, where, line, textWidth, mWorkPaint, forceEllipsis, dir); + widthStart, tempAvail, where, line, mWorkPaint, forceEllipsis, dir); if (ellipsizedWidth <= avail) { lineFits = true; } else { @@ -1207,7 +1167,7 @@ public class StaticLayout extends Layout { // 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, float textWidth, + int widthStart, float avail, TextUtils.TruncateAt where, int line, TextPaint paint, boolean forceEllipsis, int dir) { final int savedHyphenEdit = paint.getHyphenEdit(); paint.setHyphenEdit(0); @@ -1541,26 +1501,28 @@ public class StaticLayout extends Layout { @Nullable int[] indents, @Nullable int[] leftPaddings, @Nullable int[] rightPaddings, @IntRange(from = 0) int indentsOffset); + // TODO: Make this method CriticalNative once native code defers doing layouts. private static native void nAddStyleRun( /* non-zero */ long nativePtr, /* non-zero */ long nativePaint, - @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl, - @Nullable String languageTags, @Nullable long[] hyphenators); + @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl); - private static native void nAddReplacementRun(/* non-zero */ long nativePtr, + // TODO: Make this method CriticalNative once native code defers doing layouts. + private static native void nAddReplacementRun( + /* non-zero */ long nativePtr, /* non-zero */ long nativePaint, @IntRange(from = 0) int start, @IntRange(from = 0) int end, - @FloatRange(from = 0.0f) float width, @Nullable String languageTags, - @Nullable long[] hyphenators); - - private static native void nGetWidths(long nativePtr, float[] widths); + @FloatRange(from = 0.0f) float width); // 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 + // 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(long nativePtr, LineBreaks recycle, int[] recycleBreaks, float[] recycleWidths, float[] recycleAscents, - float[] recycleDescents, int[] recycleFlags, int recycleLength); + float[] recycleDescents, int[] recycleFlags, int recycleLength, + float[] charWidths); private int mLineCount; private int mTopPadding, mBottomPadding; diff --git a/android/text/StaticLayoutCreateDrawPerfTest.java b/android/text/StaticLayoutCreateDrawPerfTest.java index 356e2e0d..bfdb7589 100644 --- a/android/text/StaticLayoutCreateDrawPerfTest.java +++ b/android/text/StaticLayoutCreateDrawPerfTest.java @@ -50,7 +50,7 @@ public class StaticLayoutCreateDrawPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameterized.Parameters(name = "cached={3},{1} chars,{0}") + @Parameterized.Parameters(name = "cached={3},{1}chars,{0}") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/android/text/StaticLayout_Delegate.java b/android/text/StaticLayout_Delegate.java index 63337f08..def3c91c 100644 --- a/android/text/StaticLayout_Delegate.java +++ b/android/text/StaticLayout_Delegate.java @@ -13,7 +13,6 @@ import android.icu.util.ULocale; import android.text.Primitive.PrimitiveType; import android.text.StaticLayout.LineBreaks; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -72,13 +71,11 @@ public class StaticLayout_Delegate { @LayoutlibDelegate /*package*/ static void nAddStyleRun(long nativeBuilder, long nativePaint, int start, - int end, boolean isRtl, String languageTags, long[] hyphenators) { + int end, boolean isRtl) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder == null) { return; } - builder.mLocales = languageTags; - builder.mNativeHyphenators = hyphenators; int bidiFlags = isRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR; measureText(nativePaint, builder.mText, start, end - start, builder.mWidths, @@ -86,30 +83,20 @@ public class StaticLayout_Delegate { } @LayoutlibDelegate - /*package*/ static void nAddReplacementRun(long nativeBuilder, int start, int end, float width, - String languageTags, long[] hyphenators) { + /*package*/ static void nAddReplacementRun(long nativeBuilder, long nativePaint, int start, + int end, float width) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder == null) { return; } - builder.mLocales = languageTags; - builder.mNativeHyphenators = hyphenators; builder.mWidths[start] = width; Arrays.fill(builder.mWidths, start + 1, end, 0.0f); } @LayoutlibDelegate - /*package*/ static void nGetWidths(long nativeBuilder, float[] floatsArray) { - Builder builder = sBuilderManager.getDelegate(nativeBuilder); - if (builder != null) { - System.arraycopy(builder.mWidths, 0, floatsArray, 0, builder.mWidths.length); - } - } - - @LayoutlibDelegate /*package*/ static int nComputeLineBreaks(long nativeBuilder, LineBreaks recycle, int[] recycleBreaks, float[] recycleWidths, float[] recycleAscents, - float[] recycleDescents, int[] recycleFlags, int recycleLength) { + float[] recycleDescents, int[] recycleFlags, int recycleLength, float[] charWidths) { Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder == null) { @@ -118,7 +105,7 @@ public class StaticLayout_Delegate { // compute all possible breakpoints. int length = builder.mWidths.length; - BreakIterator it = BreakIterator.getLineInstance(new ULocale(builder.mLocales)); + BreakIterator it = BreakIterator.getLineInstance(); it.setText(new Segment(builder.mText, 0, length)); // average word length in english is 5. So, initialize the possible breaks with a guess. @@ -149,6 +136,7 @@ public class StaticLayout_Delegate { builder.mTabStopCalculator); } builder.mLineBreaker.computeBreaks(recycle); + System.arraycopy(builder.mWidths, 0, charWidths, 0, builder.mWidths.length); return recycle.breaks.length; } @@ -206,11 +194,9 @@ public class StaticLayout_Delegate { * Java representation of the native Builder class. */ private static class Builder { - String mLocales; char[] mText; float[] mWidths; LineBreaker mLineBreaker; - long[] mNativeHyphenators; int mBreakStrategy; LineWidth mLineWidth; TabStops mTabStopCalculator; diff --git a/android/text/TextLine.java b/android/text/TextLine.java index 2dbff100..20c0ed87 100644 --- a/android/text/TextLine.java +++ b/android/text/TextLine.java @@ -73,7 +73,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]; @@ -340,14 +340,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 +828,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); } diff --git a/android/text/TextViewSetTextMeasurePerfTest.java b/android/text/TextViewSetTextMeasurePerfTest.java index a2bf33e1..ff2d57ed 100644 --- a/android/text/TextViewSetTextMeasurePerfTest.java +++ b/android/text/TextViewSetTextMeasurePerfTest.java @@ -40,7 +40,7 @@ import java.util.Locale; import java.util.Random; /** - * Performance test for multi line, single style {@link StaticLayout} creation/draw. + * Performance test for {@link TextView} measure/draw. */ @LargeTest @RunWith(Parameterized.class) @@ -51,7 +51,7 @@ public class TextViewSetTextMeasurePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameterized.Parameters(name = "cached={3},{1} chars,{0}") + @Parameterized.Parameters(name = "cached={3},{1}chars,{0}") public static Collection cases() { final List<Object[]> params = new ArrayList<>(); for (int length : new int[]{128}) { diff --git a/android/util/Log.java b/android/util/Log.java index 02998653..b94e48b3 100644 --- a/android/util/Log.java +++ b/android/util/Log.java @@ -16,45 +16,12 @@ package android.util; -import android.os.DeadSystemException; - -import com.android.internal.os.RuntimeInit; -import com.android.internal.util.FastPrintWriter; -import com.android.internal.util.LineBreakBufferedWriter; - import java.io.PrintWriter; import java.io.StringWriter; -import java.io.Writer; import java.net.UnknownHostException; /** - * API for sending log output. - * - * <p>Generally, you should use the {@link #v Log.v()}, {@link #d Log.d()}, - * {@link #i Log.i()}, {@link #w Log.w()}, and {@link #e Log.e()} methods to write logs. - * You can then <a href="{@docRoot}studio/debug/am-logcat.html">view the logs in logcat</a>. - * - * <p>The order in terms of verbosity, from least to most is - * ERROR, WARN, INFO, DEBUG, VERBOSE. Verbose should never be compiled - * into an application except during development. Debug logs are compiled - * in but stripped at runtime. Error, warning and info logs are always kept. - * - * <p><b>Tip:</b> A good convention is to declare a <code>TAG</code> constant - * in your class: - * - * <pre>private static final String TAG = "MyActivity";</pre> - * - * and use that in subsequent calls to the log methods. - * </p> - * - * <p><b>Tip:</b> Don't forget that when you make a call like - * <pre>Log.v(TAG, "index=" + i);</pre> - * that when you're building the string to pass into Log.d, the compiler uses a - * StringBuilder and at least three allocations occur: the StringBuilder - * itself, the buffer, and the String object. Realistically, there is also - * another buffer allocation and copy, and even more pressure on the gc. - * That means that if your log message is filtered out, you might be doing - * significant work and incurring significant overhead. + * Mock Log implementation for testing on non android host. */ public final class Log { @@ -88,29 +55,6 @@ public final class Log { */ public static final int ASSERT = 7; - /** - * Exception class used to capture a stack trace in {@link #wtf}. - * @hide - */ - public static class TerribleFailure extends Exception { - TerribleFailure(String msg, Throwable cause) { super(msg, cause); } - } - - /** - * Interface to handle terrible failures from {@link #wtf}. - * - * @hide - */ - public interface TerribleFailureHandler { - void onTerribleFailure(String tag, TerribleFailure what, boolean system); - } - - private static TerribleFailureHandler sWtfHandler = new TerribleFailureHandler() { - public void onTerribleFailure(String tag, TerribleFailure what, boolean system) { - RuntimeInit.wtf(tag, what, system); - } - }; - private Log() { } @@ -121,7 +65,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int v(String tag, String msg) { - return println_native(LOG_ID_MAIN, VERBOSE, tag, msg); + return println(LOG_ID_MAIN, VERBOSE, tag, msg); } /** @@ -132,7 +76,7 @@ public final class Log { * @param tr An exception to log */ public static int v(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr); + return println(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -142,7 +86,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int d(String tag, String msg) { - return println_native(LOG_ID_MAIN, DEBUG, tag, msg); + return println(LOG_ID_MAIN, DEBUG, tag, msg); } /** @@ -153,7 +97,7 @@ public final class Log { * @param tr An exception to log */ public static int d(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, DEBUG, tag, msg, tr); + return println(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -163,7 +107,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int i(String tag, String msg) { - return println_native(LOG_ID_MAIN, INFO, tag, msg); + return println(LOG_ID_MAIN, INFO, tag, msg); } /** @@ -174,7 +118,7 @@ public final class Log { * @param tr An exception to log */ public static int i(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, INFO, tag, msg, tr); + return println(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -184,7 +128,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int w(String tag, String msg) { - return println_native(LOG_ID_MAIN, WARN, tag, msg); + return println(LOG_ID_MAIN, WARN, tag, msg); } /** @@ -195,31 +139,9 @@ public final class Log { * @param tr An exception to log */ public static int w(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, WARN, tag, msg, tr); + return println(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr)); } - /** - * Checks to see whether or not a log for the specified tag is loggable at the specified level. - * - * The default level of any tag is set to INFO. This means that any level above and including - * INFO will be logged. Before you make any calls to a logging method you should check to see - * if your tag should be logged. You can change the default level by setting a system property: - * 'setprop log.tag.<YOUR_LOG_TAG> <LEVEL>' - * Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPPRESS will - * turn off all logging for your tag. You can also create a local.prop file that with the - * following in it: - * 'log.tag.<YOUR_LOG_TAG>=<LEVEL>' - * and place that in /data/local.prop. - * - * @param tag The tag to check. - * @param level The level to check. - * @return Whether or not that this is allowed to be logged. - * @throws IllegalArgumentException is thrown if the tag.length() > 23 - * for Nougat (7.0) releases (API <= 23) and prior, there is no - * tag limit of concern after this API level. - */ - public static native boolean isLoggable(String tag, int level); - /* * Send a {@link #WARN} log message and log the exception. * @param tag Used to identify the source of a log message. It usually identifies @@ -227,7 +149,7 @@ public final class Log { * @param tr An exception to log */ public static int w(String tag, Throwable tr) { - return printlns(LOG_ID_MAIN, WARN, tag, "", tr); + return println(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr)); } /** @@ -237,7 +159,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int e(String tag, String msg) { - return println_native(LOG_ID_MAIN, ERROR, tag, msg); + return println(LOG_ID_MAIN, ERROR, tag, msg); } /** @@ -248,82 +170,7 @@ public final class Log { * @param tr An exception to log */ public static int e(String tag, String msg, Throwable tr) { - return printlns(LOG_ID_MAIN, ERROR, tag, msg, tr); - } - - /** - * What a Terrible Failure: Report a condition that should never happen. - * The error will always be logged at level ASSERT with the call stack. - * Depending on system configuration, a report may be added to the - * {@link android.os.DropBoxManager} and/or the process may be terminated - * immediately with an error dialog. - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - */ - public static int wtf(String tag, String msg) { - return wtf(LOG_ID_MAIN, tag, msg, null, false, false); - } - - /** - * Like {@link #wtf(String, String)}, but also writes to the log the full - * call stack. - * @hide - */ - public static int wtfStack(String tag, String msg) { - return wtf(LOG_ID_MAIN, tag, msg, null, true, false); - } - - /** - * What a Terrible Failure: Report an exception that should never happen. - * Similar to {@link #wtf(String, String)}, with an exception to log. - * @param tag Used to identify the source of a log message. - * @param tr An exception to log. - */ - public static int wtf(String tag, Throwable tr) { - return wtf(LOG_ID_MAIN, tag, tr.getMessage(), tr, false, false); - } - - /** - * What a Terrible Failure: Report an exception that should never happen. - * Similar to {@link #wtf(String, Throwable)}, with a message as well. - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - * @param tr An exception to log. May be null. - */ - public static int wtf(String tag, String msg, Throwable tr) { - return wtf(LOG_ID_MAIN, tag, msg, tr, false, false); - } - - static int wtf(int logId, String tag, String msg, Throwable tr, boolean localStack, - boolean system) { - TerribleFailure what = new TerribleFailure(msg, tr); - // Only mark this as ERROR, do not use ASSERT since that should be - // reserved for cases where the system is guaranteed to abort. - // The onTerribleFailure call does not always cause a crash. - int bytes = printlns(logId, ERROR, tag, msg, localStack ? what : tr); - sWtfHandler.onTerribleFailure(tag, what, system); - return bytes; - } - - static void wtfQuiet(int logId, String tag, String msg, boolean system) { - TerribleFailure what = new TerribleFailure(msg, null); - sWtfHandler.onTerribleFailure(tag, what, system); - } - - /** - * Sets the terrible failure handler, for testing. - * - * @return the old handler - * - * @hide - */ - public static TerribleFailureHandler setWtfHandler(TerribleFailureHandler handler) { - if (handler == null) { - throw new NullPointerException("handler == null"); - } - TerribleFailureHandler oldHandler = sWtfHandler; - sWtfHandler = handler; - return oldHandler; + return println(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr)); } /** @@ -346,7 +193,7 @@ public final class Log { } StringWriter sw = new StringWriter(); - PrintWriter pw = new FastPrintWriter(sw, false, 256); + PrintWriter pw = new PrintWriter(sw); tr.printStackTrace(pw); pw.flush(); return sw.toString(); @@ -361,7 +208,7 @@ public final class Log { * @return The number of bytes written. */ public static int println(int priority, String tag, String msg) { - return println_native(LOG_ID_MAIN, priority, tag, msg); + return println(LOG_ID_MAIN, priority, tag, msg); } /** @hide */ public static final int LOG_ID_MAIN = 0; @@ -370,115 +217,9 @@ public final class Log { /** @hide */ public static final int LOG_ID_SYSTEM = 3; /** @hide */ public static final int LOG_ID_CRASH = 4; - /** @hide */ public static native int println_native(int bufID, - int priority, String tag, String msg); - - /** - * Return the maximum payload the log daemon accepts without truncation. - * @return LOGGER_ENTRY_MAX_PAYLOAD. - */ - private static native int logger_entry_max_payload_native(); - - /** - * Helper function for long messages. Uses the LineBreakBufferedWriter to break - * up long messages and stacktraces along newlines, but tries to write in large - * chunks. This is to avoid truncation. - * @hide - */ - public static int printlns(int bufID, int priority, String tag, String msg, - Throwable tr) { - ImmediateLogWriter logWriter = new ImmediateLogWriter(bufID, priority, tag); - // Acceptable buffer size. Get the native buffer size, subtract two zero terminators, - // and the length of the tag. - // Note: we implicitly accept possible truncation for Modified-UTF8 differences. It - // is too expensive to compute that ahead of time. - int bufferSize = PreloadHolder.LOGGER_ENTRY_MAX_PAYLOAD // Base. - - 2 // Two terminators. - - (tag != null ? tag.length() : 0) // Tag length. - - 32; // Some slack. - // At least assume you can print *some* characters (tag is not too large). - bufferSize = Math.max(bufferSize, 100); - - LineBreakBufferedWriter lbbw = new LineBreakBufferedWriter(logWriter, bufferSize); - - lbbw.println(msg); - - if (tr != null) { - // This is to reduce the amount of log spew that apps do in the non-error - // condition of the network being unavailable. - Throwable t = tr; - while (t != null) { - if (t instanceof UnknownHostException) { - break; - } - if (t instanceof DeadSystemException) { - lbbw.println("DeadSystemException: The system died; " - + "earlier logs will point to the root cause"); - break; - } - t = t.getCause(); - } - if (t == null) { - tr.printStackTrace(lbbw); - } - } - - lbbw.flush(); - - return logWriter.getWritten(); - } - - /** - * PreloadHelper class. Caches the LOGGER_ENTRY_MAX_PAYLOAD value to avoid - * a JNI call during logging. - */ - static class PreloadHolder { - public final static int LOGGER_ENTRY_MAX_PAYLOAD = - logger_entry_max_payload_native(); - } - - /** - * Helper class to write to the logcat. Different from LogWriter, this writes - * the whole given buffer and does not break along newlines. - */ - private static class ImmediateLogWriter extends Writer { - - private int bufID; - private int priority; - private String tag; - - private int written = 0; - - /** - * Create a writer that immediately writes to the log, using the given - * parameters. - */ - public ImmediateLogWriter(int bufID, int priority, String tag) { - this.bufID = bufID; - this.priority = priority; - this.tag = tag; - } - - public int getWritten() { - return written; - } - - @Override - public void write(char[] cbuf, int off, int len) { - // Note: using String here has a bit of overhead as a Java object is created, - // but using the char[] directly is not easier, as it needs to be translated - // to a C char[] for logging. - written += println_native(bufID, priority, tag, new String(cbuf, off, len)); - } - - @Override - public void flush() { - // Ignored. - } - - @Override - public void close() { - // Ignored. - } + /** @hide */ @SuppressWarnings("unused") + public static int println(int bufID, + int priority, String tag, String msg) { + return 0; } } diff --git a/android/util/LruCache.java b/android/util/LruCache.java index 40154880..52086065 100644 --- a/android/util/LruCache.java +++ b/android/util/LruCache.java @@ -20,6 +20,10 @@ import java.util.LinkedHashMap; import java.util.Map; /** + * BEGIN LAYOUTLIB CHANGE + * This is a custom version that doesn't use the non standard LinkedHashMap#eldest. + * END LAYOUTLIB CHANGE + * * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may @@ -87,8 +91,9 @@ public class LruCache<K, V> { /** * Sets the size of the cache. - * * @param maxSize The new maximum size. + * + * @hide */ public void resize(int maxSize) { if (maxSize <= 0) { @@ -185,13 +190,10 @@ public class LruCache<K, V> { } /** - * Remove the eldest entries until the total of remaining entries is at or - * below the requested size. - * * @param maxSize the maximum size of the cache before returning. May be -1 - * to evict even 0-sized elements. + * to evict even 0-sized elements. */ - public void trimToSize(int maxSize) { + private void trimToSize(int maxSize) { while (true) { K key; V value; @@ -205,7 +207,16 @@ public class LruCache<K, V> { break; } - Map.Entry<K, V> toEvict = map.eldest(); + // BEGIN LAYOUTLIB CHANGE + // get the last item in the linked list. + // This is not efficient, the goal here is to minimize the changes + // compared to the platform version. + Map.Entry<K, V> toEvict = null; + for (Map.Entry<K, V> entry : map.entrySet()) { + toEvict = entry; + } + // END LAYOUTLIB CHANGE + if (toEvict == null) { break; } diff --git a/android/util/MutableBoolean.java b/android/util/MutableBoolean.java index 5a8a200d..ed837ab6 100644 --- a/android/util/MutableBoolean.java +++ b/android/util/MutableBoolean.java @@ -19,9 +19,9 @@ package android.util; /** */ public final class MutableBoolean { - public boolean value; + public boolean value; - public MutableBoolean(boolean value) { - this.value = value; - } + public MutableBoolean(boolean value) { + this.value = value; + } } diff --git a/android/util/MutableByte.java b/android/util/MutableByte.java index 7397ba47..cc6b00a8 100644 --- a/android/util/MutableByte.java +++ b/android/util/MutableByte.java @@ -19,9 +19,9 @@ package android.util; /** */ public final class MutableByte { - public byte value; + public byte value; - public MutableByte(byte value) { - this.value = value; - } + public MutableByte(byte value) { + this.value = value; + } } diff --git a/android/util/MutableChar.java b/android/util/MutableChar.java index f435331b..9a2e2bce 100644 --- a/android/util/MutableChar.java +++ b/android/util/MutableChar.java @@ -19,9 +19,9 @@ package android.util; /** */ public final class MutableChar { - public char value; + public char value; - public MutableChar(char value) { - this.value = value; - } + public MutableChar(char value) { + this.value = value; + } } diff --git a/android/util/MutableDouble.java b/android/util/MutableDouble.java index f62f47e2..bd7329a3 100644 --- a/android/util/MutableDouble.java +++ b/android/util/MutableDouble.java @@ -19,9 +19,9 @@ package android.util; /** */ public final class MutableDouble { - public double value; + public double value; - public MutableDouble(double value) { - this.value = value; - } + public MutableDouble(double value) { + this.value = value; + } } diff --git a/android/util/MutableFloat.java b/android/util/MutableFloat.java index 6b5441c5..e6f2d7dc 100644 --- a/android/util/MutableFloat.java +++ b/android/util/MutableFloat.java @@ -19,9 +19,9 @@ package android.util; /** */ public final class MutableFloat { - public float value; + public float value; - public MutableFloat(float value) { - this.value = value; - } + public MutableFloat(float value) { + this.value = value; + } } diff --git a/android/util/MutableInt.java b/android/util/MutableInt.java index 2f930302..a3d8606d 100644 --- a/android/util/MutableInt.java +++ b/android/util/MutableInt.java @@ -19,9 +19,9 @@ package android.util; /** */ public final class MutableInt { - public int value; + public int value; - public MutableInt(int value) { - this.value = value; - } + public MutableInt(int value) { + this.value = value; + } } diff --git a/android/util/MutableLong.java b/android/util/MutableLong.java index 94beab57..575068ea 100644 --- a/android/util/MutableLong.java +++ b/android/util/MutableLong.java @@ -19,9 +19,9 @@ package android.util; /** */ public final class MutableLong { - public long value; + public long value; - public MutableLong(long value) { - this.value = value; - } + public MutableLong(long value) { + this.value = value; + } } diff --git a/android/util/MutableShort.java b/android/util/MutableShort.java index cdd99230..48fb232b 100644 --- a/android/util/MutableShort.java +++ b/android/util/MutableShort.java @@ -19,9 +19,9 @@ package android.util; /** */ public final class MutableShort { - public short value; + public short value; - public MutableShort(short value) { - this.value = value; - } + public MutableShort(short value) { + this.value = value; + } } diff --git a/android/util/StatsLog.java b/android/util/StatsLog.java deleted file mode 100644 index 0be1a8cf..00000000 --- a/android/util/StatsLog.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2007 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; - -/** - * Logging access for platform metrics. - * - * <p>This is <b>not</b> the main "logcat" debugging log ({@link android.util.Log})! - * These diagnostic stats are for system integrators, not application authors. - * - * <p>Stats use integer tag codes. - * They carry a payload of one or more int, long, or String values. - * @hide - */ -public class StatsLog { - /** @hide */ public StatsLog() {} - - private static final String TAG = "StatsLog"; - - // We assume that the native methods deal with any concurrency issues. - - /** - * Records an stats log message. - * @param tag The stats type tag code - * @param value A value to log - * @return The number of bytes written - */ - public static native int writeInt(int tag, int value); - - /** - * Records an stats log message. - * @param tag The stats type tag code - * @param value A value to log - * @return The number of bytes written - */ - public static native int writeLong(int tag, long value); - - /** - * Records an stats log message. - * @param tag The stats type tag code - * @param value A value to log - * @return The number of bytes written - */ - public static native int writeFloat(int tag, float value); - - /** - * Records an stats log message. - * @param tag The stats type tag code - * @param str A value to log - * @return The number of bytes written - */ - public static native int writeString(int tag, String str); - - /** - * Records an stats log message. - * @param tag The stats type tag code - * @param list A list of values to log. All values should - * be of type int, long, float or String. - * @return The number of bytes written - */ - public static native int writeArray(int tag, Object... list); -} diff --git a/android/util/StatsLogKey.java b/android/util/StatsLogKey.java deleted file mode 100644 index 9ad0a23d..00000000 --- a/android/util/StatsLogKey.java +++ /dev/null @@ -1,48 +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. - */ - -// THIS FILE IS AUTO-GENERATED. -// DO NOT MODIFY. - -package android.util; - -/** @hide */ -public class StatsLogKey { - private StatsLogKey() {} - - /** Constants for android.os.statsd.ScreenStateChange. */ - - /** display_state */ - public static final int SCREEN_STATE_CHANGE__DISPLAY_STATE = 1; - - /** Constants for android.os.statsd.ProcessStateChange. */ - - /** state */ - public static final int PROCESS_STATE_CHANGE__STATE = 1; - - /** uid */ - public static final int PROCESS_STATE_CHANGE__UID = 2; - - /** package_name */ - public static final int PROCESS_STATE_CHANGE__PACKAGE_NAME = 1002; - - /** package_version */ - public static final int PROCESS_STATE_CHANGE__PACKAGE_VERSION = 3; - - /** package_version_string */ - public static final int PROCESS_STATE_CHANGE__PACKAGE_VERSION_STRING = 4; - -} diff --git a/android/util/StatsLogTag.java b/android/util/StatsLogTag.java deleted file mode 100644 index 5e5a8287..00000000 --- a/android/util/StatsLogTag.java +++ /dev/null @@ -1,32 +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. - */ - -// THIS FILE IS AUTO-GENERATED. -// DO NOT MODIFY. - -package android.util; - -/** @hide */ -public class StatsLogTag { - private StatsLogTag() {} - - /** android.os.statsd.ScreenStateChange. */ - public static final int SCREEN_STATE_CHANGE = 2; - - /** android.os.statsd.ProcessStateChange. */ - public static final int PROCESS_STATE_CHANGE = 1112; - -} diff --git a/android/util/StatsLogValue.java b/android/util/StatsLogValue.java deleted file mode 100644 index 05b9d933..00000000 --- a/android/util/StatsLogValue.java +++ /dev/null @@ -1,54 +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. - */ - -// THIS FILE IS AUTO-GENERATED. -// DO NOT MODIFY. - -package android.util; - -/** @hide */ -public class StatsLogValue { - private StatsLogValue() {} - - /** Constants for android.os.statsd.ScreenStateChange. */ - - /** display_state: STATE_UNKNOWN */ - public static final int SCREEN_STATE_CHANGE__DISPLAY_STATE__STATE_UNKNOWN = 0; - - /** display_state: STATE_OFF */ - public static final int SCREEN_STATE_CHANGE__DISPLAY_STATE__STATE_OFF = 1; - - /** display_state: STATE_ON */ - public static final int SCREEN_STATE_CHANGE__DISPLAY_STATE__STATE_ON = 2; - - /** display_state: STATE_DOZE */ - public static final int SCREEN_STATE_CHANGE__DISPLAY_STATE__STATE_DOZE = 3; - - /** display_state: STATE_DOZE_SUSPEND */ - public static final int SCREEN_STATE_CHANGE__DISPLAY_STATE__STATE_DOZE_SUSPEND = 4; - - /** display_state: STATE_VR */ - public static final int SCREEN_STATE_CHANGE__DISPLAY_STATE__STATE_VR = 5; - - /** Constants for android.os.statsd.ProcessStateChange. */ - - /** state: START */ - public static final int PROCESS_STATE_CHANGE__STATE__START = 1; - - /** state: CRASH */ - public static final int PROCESS_STATE_CHANGE__STATE__CRASH = 2; - -} diff --git a/android/util/apk/ApkSignatureSchemeV2Verifier.java b/android/util/apk/ApkSignatureSchemeV2Verifier.java index 0216a075..a9ccae11 100644 --- a/android/util/apk/ApkSignatureSchemeV2Verifier.java +++ b/android/util/apk/ApkSignatureSchemeV2Verifier.java @@ -17,6 +17,7 @@ package android.util.apk; import android.system.ErrnoException; +import android.system.Os; import android.system.OsConstants; import android.util.ArrayMap; import android.util.Pair; @@ -59,9 +60,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import libcore.io.Libcore; -import libcore.io.Os; - /** * APK Signature Scheme v2 verifier. * @@ -994,8 +992,7 @@ public class ApkSignatureSchemeV2Verifier { * {@link DataSource#feedIntoMessageDigests(MessageDigest[], long, int) feedIntoMessageDigests}. */ private static final class MemoryMappedFileDataSource implements DataSource { - private static final Os OS = Libcore.os; - private static final long MEMORY_PAGE_SIZE_BYTES = OS.sysconf(OsConstants._SC_PAGESIZE); + private static final long MEMORY_PAGE_SIZE_BYTES = Os.sysconf(OsConstants._SC_PAGESIZE); private final FileDescriptor mFd; private final long mFilePosition; @@ -1041,7 +1038,7 @@ public class ApkSignatureSchemeV2Verifier { long mmapRegionSize = size + dataStartOffsetInMmapRegion; long mmapPtr = 0; try { - mmapPtr = OS.mmap( + mmapPtr = Os.mmap( 0, // let the OS choose the start address of the region in memory mmapRegionSize, OsConstants.PROT_READ, @@ -1066,7 +1063,7 @@ public class ApkSignatureSchemeV2Verifier { } finally { if (mmapPtr != 0) { try { - OS.munmap(mmapPtr, mmapRegionSize); + Os.munmap(mmapPtr, mmapRegionSize); } catch (ErrnoException ignored) {} } } diff --git a/android/view/MenuItem.java b/android/view/MenuItem.java index 88b9c0d3..ad160cbf 100644 --- a/android/view/MenuItem.java +++ b/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/android/view/SurfaceControl.java b/android/view/SurfaceControl.java index 31daefff..ff027a94 100644 --- a/android/view/SurfaceControl.java +++ b/android/view/SurfaceControl.java @@ -21,15 +21,22 @@ import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import android.annotation.Size; import android.graphics.Bitmap; import android.graphics.GraphicBuffer; +import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.os.Binder; +import android.os.Debug; import android.os.IBinder; import android.util.Log; import android.view.Surface.OutOfResourcesException; import dalvik.system.CloseGuard; +import java.io.Closeable; + +import libcore.util.NativeAllocationRegistry; + /** * SurfaceControl * @hide @@ -54,25 +61,34 @@ public class SurfaceControl { 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 nativeSetColor(long nativeObject, float[] color); - private static native void nativeSetMatrix(long nativeObject, float dsdx, float dtdx, + private static native long nativeCreateTransaction(); + private static native long nativeGetNativeTransactionFinalizer(); + private static native void nativeApplyTransaction(long transactionObj, boolean sync); + 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); @@ -82,15 +98,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); @@ -101,16 +118,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 nativeReparent(long nativeObject, + private static native void nativeReparent(long transactionObj, long nativeObject, IBinder parentHandle); - private static native void nativeSeverChildren(long nativeObject); - private static native void nativeSetOverrideScalingMode(long nativeObject, + 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); @@ -122,6 +140,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) */ /** @@ -377,11 +398,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. @@ -429,102 +445,141 @@ 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); + } } /** 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); + } } - /** Re-parents this layer to a new parent. */ public void reparent(IBinder newParentHandle) { - nativeReparent(mNativeObject, 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) { 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() { @@ -545,80 +600,70 @@ 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); + } } - /** - * Sets a color for the Surface. - * @param color A float array with three values to represent r, g, b in range [0..1] - */ public void setColor(@Size(3) float[] color) { checkNotReleased(); - nativeSetColor(mNativeObject, color); + 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); + } } 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 @@ -741,50 +786,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"); - } - if (layerStackRect == null) { - throw new IllegalArgumentException("layerStackRect must not be null"); + synchronized (SurfaceControl.class) { + sGlobalTransaction.setDisplayProjection(displayToken, orientation, + layerStackRect, displayRect); } - 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) { @@ -946,4 +969,261 @@ public class SurfaceControl { nativeScreenshot(display, consumer, sourceCrop, width, height, minLayer, maxLayer, allLayers, useIdentityTransform); } + + 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) { + nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SURFACE_HIDDEN); + return this; + } + + public Transaction hide(SurfaceControl sc) { + nativeSetFlags(mNativeObject, sc.mNativeObject, SURFACE_HIDDEN, SURFACE_HIDDEN); + return this; + } + + public Transaction setPosition(SurfaceControl sc, float x, float y) { + nativeSetPosition(mNativeObject, sc.mNativeObject, x, y); + return this; + } + + public Transaction setSize(SurfaceControl sc, int w, int h) { + nativeSetSize(mNativeObject, sc.mNativeObject, + w, h); + return this; + } + + public Transaction setLayer(SurfaceControl sc, int z) { + nativeSetLayer(mNativeObject, sc.mNativeObject, z); + return this; + } + + public Transaction setRelativeLayer(SurfaceControl sc, IBinder relativeTo, int z) { + nativeSetRelativeLayer(mNativeObject, sc.mNativeObject, + relativeTo, z); + return this; + } + + public Transaction setTransparentRegionHint(SurfaceControl sc, Region transparentRegion) { + nativeSetTransparentRegionHint(mNativeObject, + sc.mNativeObject, transparentRegion); + return this; + } + + public Transaction setAlpha(SurfaceControl sc, float alpha) { + nativeSetAlpha(mNativeObject, sc.mNativeObject, alpha); + return this; + } + + public Transaction setMatrix(SurfaceControl sc, + float dsdx, float dtdx, float dtdy, float dsdy) { + nativeSetMatrix(mNativeObject, sc.mNativeObject, + dsdx, dtdx, dtdy, dsdy); + return this; + } + + public Transaction setWindowCrop(SurfaceControl sc, Rect crop) { + 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) { + 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) { + nativeSetLayerStack(mNativeObject, sc.mNativeObject, layerStack); + return this; + } + + public Transaction deferTransactionUntil(SurfaceControl sc, IBinder handle, long frameNumber) { + nativeDeferTransactionUntil(mNativeObject, sc.mNativeObject, handle, frameNumber); + return this; + } + + public Transaction deferTransactionUntilSurface(SurfaceControl sc, Surface barrierSurface, + long frameNumber) { + nativeDeferTransactionUntilSurface(mNativeObject, sc.mNativeObject, + barrierSurface.mNativeObject, frameNumber); + return this; + } + + public Transaction reparentChildren(SurfaceControl sc, IBinder newParentHandle) { + nativeReparentChildren(mNativeObject, sc.mNativeObject, newParentHandle); + return this; + } + + /** Re-parents a specific child layer to a new parent */ + public Transaction reparent(SurfaceControl sc, IBinder newParentHandle) { + nativeReparent(mNativeObject, sc.mNativeObject, + newParentHandle); + return this; + } + + public Transaction detachChildren(SurfaceControl sc) { + nativeSeverChildren(mNativeObject, sc.mNativeObject); + return this; + } + + public Transaction setOverrideScalingMode(SurfaceControl sc, int overrideScalingMode) { + 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) { + 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) { + 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. + */ + Transaction setSecure(SurfaceControl sc, boolean isSecure) { + 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) { + 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; + } + } } diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java index 462dad3f..ebb2af45 100644 --- a/android/view/SurfaceView.java +++ b/android/view/SurfaceView.java @@ -16,1208 +16,115 @@ 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 com.android.layoutlib.bridge.MockView; import android.content.Context; -import android.content.res.CompatibilityInfo.Translator; -import android.content.res.Configuration; import android.graphics.Canvas; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; -import android.os.Build; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.SystemClock; import android.util.AttributeSet; -import android.util.Log; - -import com.android.internal.view.SurfaceCallbackHelper; - -import java.util.ArrayList; -import java.util.concurrent.locks.ReentrantLock; /** - * Provides a dedicated drawing surface embedded inside of a view hierarchy. - * You can control the format of this surface and, if you like, its size; the - * SurfaceView takes care of placing the surface at the correct location on the - * screen - * - * <p>The surface is Z ordered so that it is behind the window holding its - * SurfaceView; the SurfaceView punches a hole in its window to allow its - * surface to be displayed. The view hierarchy will take care of correctly - * compositing with the Surface any siblings of the SurfaceView that would - * normally appear on top of it. This can be used to place overlays such as - * buttons on top of the Surface, though note however that it can have an - * impact on performance since a full alpha-blended composite will be performed - * each time the Surface changes. - * - * <p> The transparent region that makes the surface visible is based on the - * layout positions in the view hierarchy. If the post-layout transform - * properties are used to draw a sibling view on top of the SurfaceView, the - * view may not be properly composited with the surface. + * Mock version of the SurfaceView. + * Only non override public methods from the real SurfaceView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. * - * <p>Access to the underlying surface is provided via the SurfaceHolder interface, - * which can be retrieved by calling {@link #getHolder}. + * TODO: generate automatically. * - * <p>The Surface will be created for you while the SurfaceView's window is - * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated} - * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the - * Surface is created and destroyed as the window is shown and hidden. - * - * <p>One of the purposes of this class is to provide a surface in which a - * secondary thread can render into the screen. If you are going to use it - * this way, you need to be aware of some threading semantics: - * - * <ul> - * <li> All SurfaceView and - * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called - * from the thread running the SurfaceView's window (typically the main thread - * of the application). They thus need to correctly synchronize with any - * state that is also touched by the drawing thread. - * <li> You must ensure that the drawing thread only touches the underlying - * Surface while it is valid -- between - * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()} - * and - * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}. - * </ul> - * - * <p class="note"><strong>Note:</strong> Starting in platform version - * {@link android.os.Build.VERSION_CODES#N}, SurfaceView's window position is - * updated synchronously with other View rendering. This means that translating - * and scaling a SurfaceView on screen will not cause rendering artifacts. Such - * artifacts may occur on previous versions of the platform when its window is - * positioned asynchronously.</p> */ -public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback { - private static final String TAG = "SurfaceView"; - private static final boolean DEBUG = false; - - final ArrayList<SurfaceHolder.Callback> mCallbacks - = new ArrayList<SurfaceHolder.Callback>(); - - final int[] mLocation = new int[2]; - - final ReentrantLock mSurfaceLock = new ReentrantLock(); - final Surface mSurface = new Surface(); // Current surface in use - boolean mDrawingStopped = true; - // We use this to track if the application has produced a frame - // in to the Surface. Up until that point, we should be careful not to punch - // holes. - boolean mDrawFinished = false; - - final Rect mScreenRect = new Rect(); - SurfaceSession mSurfaceSession; - - SurfaceControl mSurfaceControl; - // In the case of format changes we switch out the surface in-place - // we need to preserve the old one until the new one has drawn. - SurfaceControl mDeferredDestroySurfaceControl; - final Rect mTmpRect = new Rect(); - final Configuration mConfiguration = new Configuration(); - - int mSubLayer = APPLICATION_MEDIA_SUBLAYER; - - boolean mIsCreating = false; - private volatile boolean mRtHandlingPositionUpdates = false; - - private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener - = new ViewTreeObserver.OnScrollChangedListener() { - @Override - public void onScrollChanged() { - updateSurface(); - } - }; - - private final ViewTreeObserver.OnPreDrawListener mDrawListener = - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - // reposition ourselves where the surface is - mHaveFrame = getWidth() > 0 && getHeight() > 0; - updateSurface(); - return true; - } - }; - - boolean mRequestedVisible = false; - boolean mWindowVisibility = false; - boolean mLastWindowVisibility = false; - boolean mViewVisibility = false; - boolean mWindowStopped = false; - - int mRequestedWidth = -1; - int mRequestedHeight = -1; - /* Set SurfaceView's format to 565 by default to maintain backward - * compatibility with applications assuming this format. - */ - int mRequestedFormat = PixelFormat.RGB_565; - - boolean mHaveFrame = false; - boolean mSurfaceCreated = false; - long mLastLockTime = 0; - - boolean mVisible = false; - int mWindowSpaceLeft = -1; - int mWindowSpaceTop = -1; - int mSurfaceWidth = -1; - int mSurfaceHeight = -1; - int mFormat = -1; - final Rect mSurfaceFrame = new Rect(); - int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1; - private Translator mTranslator; - - private boolean mGlobalListenersAdded; - private boolean mAttachedToWindow; - - private int mSurfaceFlags = SurfaceControl.HIDDEN; - - private int mPendingReportDraws; +public class SurfaceView extends MockView { public SurfaceView(Context context) { this(context, null); } public SurfaceView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs , 0); } - public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); + public SurfaceView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); } public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mRenderNode.requestPositionUpdates(this); - - setWillNotDraw(true); - } - - /** - * Return the SurfaceHolder providing access and control over this - * SurfaceView's underlying surface. - * - * @return SurfaceHolder The holder of the surface. - */ - public SurfaceHolder getHolder() { - return mSurfaceHolder; - } - - private void updateRequestedVisibility() { - mRequestedVisible = mViewVisibility && mWindowVisibility && !mWindowStopped; - } - - /** @hide */ - @Override - public void windowStopped(boolean stopped) { - mWindowStopped = stopped; - updateRequestedVisibility(); - updateSurface(); } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - getViewRootImpl().addWindowStoppedCallback(this); - mWindowStopped = false; - - mViewVisibility = getVisibility() == VISIBLE; - updateRequestedVisibility(); - - mAttachedToWindow = true; - mParent.requestTransparentRegion(SurfaceView.this); - if (!mGlobalListenersAdded) { - ViewTreeObserver observer = getViewTreeObserver(); - observer.addOnScrollChangedListener(mScrollChangedListener); - observer.addOnPreDrawListener(mDrawListener); - mGlobalListenersAdded = true; - } - } - - @Override - protected void onWindowVisibilityChanged(int visibility) { - super.onWindowVisibilityChanged(visibility); - mWindowVisibility = visibility == VISIBLE; - updateRequestedVisibility(); - updateSurface(); - } - - @Override - public void setVisibility(int visibility) { - super.setVisibility(visibility); - mViewVisibility = visibility == VISIBLE; - boolean newRequestedVisible = mWindowVisibility && mViewVisibility && !mWindowStopped; - if (newRequestedVisible != mRequestedVisible) { - // our base class (View) invalidates the layout only when - // we go from/to the GONE state. However, SurfaceView needs - // to request a re-layout when the visibility changes at all. - // This is needed because the transparent region is computed - // as part of the layout phase, and it changes (obviously) when - // the visibility changes. - requestLayout(); - } - mRequestedVisible = newRequestedVisible; - updateSurface(); - } - - private void performDrawFinished() { - if (mPendingReportDraws > 0) { - mDrawFinished = true; - if (mAttachedToWindow) { - notifyDrawFinished(); - invalidate(); - } - } else { - Log.e(TAG, System.identityHashCode(this) + "finished drawing" - + " but no pending report draw (extra call" - + " to draw completion runnable?)"); - } - } - - void notifyDrawFinished() { - ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot != null) { - viewRoot.pendingDrawFinished(); - } - mPendingReportDraws--; - } - - @Override - protected void onDetachedFromWindow() { - ViewRootImpl viewRoot = getViewRootImpl(); - // It's possible to create a SurfaceView using the default constructor and never - // attach it to a view hierarchy, this is a common use case when dealing with - // OpenGL. A developer will probably create a new GLSurfaceView, and let it manage - // the lifecycle. Instead of attaching it to a view, he/she can just pass - // the SurfaceHolder forward, most live wallpapers do it. - if (viewRoot != null) { - viewRoot.removeWindowStoppedCallback(this); - } - - mAttachedToWindow = false; - if (mGlobalListenersAdded) { - ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnScrollChangedListener(mScrollChangedListener); - observer.removeOnPreDrawListener(mDrawListener); - mGlobalListenersAdded = false; - } - - while (mPendingReportDraws > 0) { - notifyDrawFinished(); - } - - mRequestedVisible = false; - - updateSurface(); - if (mSurfaceControl != null) { - mSurfaceControl.destroy(); - } - mSurfaceControl = null; - - mHaveFrame = false; - - super.onDetachedFromWindow(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = mRequestedWidth >= 0 - ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0) - : getDefaultSize(0, widthMeasureSpec); - int height = mRequestedHeight >= 0 - ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0) - : getDefaultSize(0, heightMeasureSpec); - setMeasuredDimension(width, height); - } - - /** @hide */ - @Override - protected boolean setFrame(int left, int top, int right, int bottom) { - boolean result = super.setFrame(left, top, right, bottom); - updateSurface(); - return result; - } - - @Override public boolean gatherTransparentRegion(Region region) { - if (isAboveParent() || !mDrawFinished) { - return super.gatherTransparentRegion(region); - } - - boolean opaque = true; - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { - // this view draws, remove it from the transparent region - opaque = super.gatherTransparentRegion(region); - } else if (region != null) { - int w = getWidth(); - int h = getHeight(); - if (w>0 && h>0) { - getLocationInWindow(mLocation); - // otherwise, punch a hole in the whole hierarchy - int l = mLocation[0]; - int t = mLocation[1]; - region.op(l, t, l+w, t+h, Region.Op.UNION); - } - } - if (PixelFormat.formatHasAlpha(mRequestedFormat)) { - opaque = false; - } - return opaque; + return false; } - @Override - public void draw(Canvas canvas) { - if (mDrawFinished && !isAboveParent()) { - // draw() is not called when SKIP_DRAW is set - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { - // punch a whole in the view-hierarchy below us - canvas.drawColor(0, PorterDuff.Mode.CLEAR); - } - } - super.draw(canvas); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - if (mDrawFinished && !isAboveParent()) { - // draw() is not called when SKIP_DRAW is set - if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { - // punch a whole in the view-hierarchy below us - canvas.drawColor(0, PorterDuff.Mode.CLEAR); - } - } - super.dispatchDraw(canvas); - } - - /** - * Control whether the surface view's surface is placed on top of another - * regular surface view in the window (but still behind the window itself). - * This is typically used to place overlays on top of an underlying media - * surface view. - * - * <p>Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. - */ public void setZOrderMediaOverlay(boolean isMediaOverlay) { - mSubLayer = isMediaOverlay - ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER; } - /** - * Control whether the surface view's surface is placed on top of its - * window. Normally it is placed behind the window, to allow it to - * (for the most part) appear to composite with the views in the - * hierarchy. By setting this, you cause it to be placed above the - * window. This means that none of the contents of the window this - * SurfaceView is in will be visible on top of its surface. - * - * <p>Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. - */ public void setZOrderOnTop(boolean onTop) { - if (onTop) { - mSubLayer = APPLICATION_PANEL_SUBLAYER; - } else { - mSubLayer = APPLICATION_MEDIA_SUBLAYER; - } } - /** - * Control whether the surface view's content should be treated as secure, - * preventing it from appearing in screenshots or from being viewed on - * non-secure displays. - * - * <p>Note that this must be set before the surface view's containing - * window is attached to the window manager. - * - * <p>See {@link android.view.Display#FLAG_SECURE} for details. - * - * @param isSecure True if the surface view is secure. - */ public void setSecure(boolean isSecure) { - if (isSecure) { - mSurfaceFlags |= SurfaceControl.SECURE; - } else { - mSurfaceFlags &= ~SurfaceControl.SECURE; - } - } - - private void updateOpaqueFlag() { - if (!PixelFormat.formatHasAlpha(mRequestedFormat)) { - mSurfaceFlags |= SurfaceControl.OPAQUE; - } else { - mSurfaceFlags &= ~SurfaceControl.OPAQUE; - } } - private Rect getParentSurfaceInsets() { - final ViewRootImpl root = getViewRootImpl(); - if (root == null) { - return null; - } else { - return root.mWindowAttributes.surfaceInsets; - } - } - - /** @hide */ - protected void updateSurface() { - if (!mHaveFrame) { - return; - } - ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) { - return; - } - - mTranslator = viewRoot.mTranslator; - if (mTranslator != null) { - mSurface.setCompatibilityTranslator(mTranslator); - } - - int myWidth = mRequestedWidth; - if (myWidth <= 0) myWidth = getWidth(); - int myHeight = mRequestedHeight; - if (myHeight <= 0) myHeight = getHeight(); - - final boolean formatChanged = mFormat != mRequestedFormat; - final boolean visibleChanged = mVisible != mRequestedVisible; - final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged) - && mRequestedVisible; - final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight; - final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility; - boolean redrawNeeded = false; - - if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) { - getLocationInWindow(mLocation); - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "Changes: creating=" + creating - + " format=" + formatChanged + " size=" + sizeChanged - + " visible=" + visibleChanged - + " left=" + (mWindowSpaceLeft != mLocation[0]) - + " top=" + (mWindowSpaceTop != mLocation[1])); - - try { - final boolean visible = mVisible = mRequestedVisible; - mWindowSpaceLeft = mLocation[0]; - mWindowSpaceTop = mLocation[1]; - mSurfaceWidth = myWidth; - mSurfaceHeight = myHeight; - mFormat = mRequestedFormat; - mLastWindowVisibility = mWindowVisibility; - - mScreenRect.left = mWindowSpaceLeft; - mScreenRect.top = mWindowSpaceTop; - mScreenRect.right = mWindowSpaceLeft + getWidth(); - mScreenRect.bottom = mWindowSpaceTop + getHeight(); - if (mTranslator != null) { - mTranslator.translateRectInAppWindowToScreen(mScreenRect); - } - - final Rect surfaceInsets = getParentSurfaceInsets(); - mScreenRect.offset(surfaceInsets.left, surfaceInsets.top); - - if (creating) { - mSurfaceSession = new SurfaceSession(viewRoot.mSurface); - mDeferredDestroySurfaceControl = mSurfaceControl; - - updateOpaqueFlag(); - mSurfaceControl = new SurfaceControlWithBackground(mSurfaceSession, - "SurfaceView - " + viewRoot.getTitle().toString(), - mSurfaceWidth, mSurfaceHeight, mFormat, - mSurfaceFlags); - } else if (mSurfaceControl == null) { - return; - } - - boolean realSizeChanged = false; - - mSurfaceLock.lock(); - try { - mDrawingStopped = !visible; - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "Cur surface: " + mSurface); - - SurfaceControl.openTransaction(); - try { - mSurfaceControl.setLayer(mSubLayer); - if (mViewVisibility) { - mSurfaceControl.show(); - } else { - mSurfaceControl.hide(); - } - - // While creating the surface, we will set it's initial - // geometry. Outside of that though, we should generally - // leave it to the RenderThread. - // - // There is one more case when the buffer size changes we aren't yet - // prepared to sync (as even following the transaction applying - // we still need to latch a buffer). - // b/28866173 - if (sizeChanged || creating || !mRtHandlingPositionUpdates) { - mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top); - mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth, - 0.0f, 0.0f, - mScreenRect.height() / (float) mSurfaceHeight); - } - if (sizeChanged) { - mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight); - } - } finally { - SurfaceControl.closeTransaction(); - } - - if (sizeChanged || creating) { - redrawNeeded = true; - } - - mSurfaceFrame.left = 0; - mSurfaceFrame.top = 0; - if (mTranslator == null) { - mSurfaceFrame.right = mSurfaceWidth; - mSurfaceFrame.bottom = mSurfaceHeight; - } else { - float appInvertedScale = mTranslator.applicationInvertedScale; - mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f); - mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f); - } - - final int surfaceWidth = mSurfaceFrame.right; - final int surfaceHeight = mSurfaceFrame.bottom; - realSizeChanged = mLastSurfaceWidth != surfaceWidth - || mLastSurfaceHeight != surfaceHeight; - mLastSurfaceWidth = surfaceWidth; - mLastSurfaceHeight = surfaceHeight; - } finally { - mSurfaceLock.unlock(); - } - - try { - redrawNeeded |= visible && !mDrawFinished; - - SurfaceHolder.Callback callbacks[] = null; - - final boolean surfaceChanged = creating; - if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) { - mSurfaceCreated = false; - if (mSurface.isValid()) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "visibleChanged -- surfaceDestroyed"); - callbacks = getSurfaceCallbacks(); - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceDestroyed(mSurfaceHolder); - } - // Since Android N the same surface may be reused and given to us - // again by the system server at a later point. However - // as we didn't do this in previous releases, clients weren't - // necessarily required to clean up properly in - // surfaceDestroyed. This leads to problems for example when - // clients don't destroy their EGL context, and try - // and create a new one on the same surface following reuse. - // Since there is no valid use of the surface in-between - // surfaceDestroyed and surfaceCreated, we force a disconnect, - // so the next connect will always work if we end up reusing - // the surface. - if (mSurface.isValid()) { - mSurface.forceScopedDisconnect(); - } - } - } - - if (creating) { - mSurface.copyFrom(mSurfaceControl); - } - - if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion - < Build.VERSION_CODES.O) { - // Some legacy applications use the underlying native {@link Surface} object - // as a key to whether anything has changed. In these cases, updates to the - // existing {@link Surface} will be ignored when the size changes. - // Therefore, we must explicitly recreate the {@link Surface} in these - // cases. - mSurface.createFrom(mSurfaceControl); - } - - if (visible && mSurface.isValid()) { - if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) { - mSurfaceCreated = true; - mIsCreating = true; - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "visibleChanged -- surfaceCreated"); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceCreated(mSurfaceHolder); - } - } - if (creating || formatChanged || sizeChanged - || visibleChanged || realSizeChanged) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "surfaceChanged -- format=" + mFormat - + " w=" + myWidth + " h=" + myHeight); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - for (SurfaceHolder.Callback c : callbacks) { - c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight); - } - } - if (redrawNeeded) { - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " - + "surfaceRedrawNeeded"); - if (callbacks == null) { - callbacks = getSurfaceCallbacks(); - } - - mPendingReportDraws++; - viewRoot.drawPending(); - SurfaceCallbackHelper sch = - new SurfaceCallbackHelper(this::onDrawFinished); - sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks); - } - } - } finally { - mIsCreating = false; - if (mSurfaceControl != null && !mSurfaceCreated) { - mSurface.release(); - // If we are not in the stopped state, then the destruction of the Surface - // represents a visual change we need to display, and we should go ahead - // and destroy the SurfaceControl. However if we are in the stopped state, - // we can just leave the Surface around so it can be a part of animations, - // and we let the life-time be tied to the parent surface. - if (!mWindowStopped) { - mSurfaceControl.destroy(); - mSurfaceControl = null; - } - } - } - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - if (DEBUG) Log.v( - TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top - + " w=" + mScreenRect.width() + " h=" + mScreenRect.height() - + ", frame=" + mSurfaceFrame); - } else { - // Calculate the window position in case RT loses the window - // and we need to fallback to a UI-thread driven position update - getLocationInSurface(mLocation); - final boolean positionChanged = mWindowSpaceLeft != mLocation[0] - || mWindowSpaceTop != mLocation[1]; - final boolean layoutSizeChanged = getWidth() != mScreenRect.width() - || getHeight() != mScreenRect.height(); - if (positionChanged || layoutSizeChanged) { // Only the position has changed - mWindowSpaceLeft = mLocation[0]; - mWindowSpaceTop = mLocation[1]; - // For our size changed check, we keep mScreenRect.width() and mScreenRect.height() - // in view local space. - mLocation[0] = getWidth(); - mLocation[1] = getHeight(); - - mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop, - mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]); - - if (mTranslator != null) { - mTranslator.translateRectInAppWindowToScreen(mScreenRect); - } - - if (mSurfaceControl == null) { - return; - } - - if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) { - try { - if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - mScreenRect.left, mScreenRect.top, - mScreenRect.right, mScreenRect.bottom)); - setParentSpaceRectangle(mScreenRect, -1); - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - } - } - } - } - - private void onDrawFinished() { - if (DEBUG) { - Log.i(TAG, System.identityHashCode(this) + " " - + "finishedDrawing"); - } - - if (mDeferredDestroySurfaceControl != null) { - mDeferredDestroySurfaceControl.destroy(); - mDeferredDestroySurfaceControl = null; - } - - runOnUiThread(() -> { - performDrawFinished(); - }); - } - - private void setParentSpaceRectangle(Rect position, long frameNumber) { - ViewRootImpl viewRoot = getViewRootImpl(); - - SurfaceControl.openTransaction(); - try { - if (frameNumber > 0) { - mSurfaceControl.deferTransactionUntil(viewRoot.mSurface, frameNumber); - } - mSurfaceControl.setPosition(position.left, position.top); - mSurfaceControl.setMatrix(position.width() / (float) mSurfaceWidth, - 0.0f, 0.0f, - position.height() / (float) mSurfaceHeight); - } finally { - SurfaceControl.closeTransaction(); - } - } - - private Rect mRTLastReportedPosition = new Rect(); - - /** - * Called by native by a Rendering Worker thread to update the window position - * @hide - */ - public final void updateSurfacePosition_renderWorker(long frameNumber, - int left, int top, int right, int bottom) { - if (mSurfaceControl == null) { - return; - } - - // TODO: This is teensy bit racey in that a brand new SurfaceView moving on - // its 2nd frame if RenderThread is running slowly could potentially see - // this as false, enter the branch, get pre-empted, then this comes along - // and reports a new position, then the UI thread resumes and reports - // its position. This could therefore be de-sync'd in that interval, but - // the synchronization would violate the rule that RT must never block - // on the UI thread which would open up potential deadlocks. The risk of - // a single-frame desync is therefore preferable for now. - mRtHandlingPositionUpdates = true; - if (mRTLastReportedPosition.left == left - && mRTLastReportedPosition.top == top - && mRTLastReportedPosition.right == right - && mRTLastReportedPosition.bottom == bottom) { - return; - } - try { - if (DEBUG) { - Log.d(TAG, String.format("%d updateSurfacePosition RenderWorker, frameNr = %d, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - frameNumber, left, top, right, bottom)); - } - mRTLastReportedPosition.set(left, top, right, bottom); - setParentSpaceRectangle(mRTLastReportedPosition, frameNumber); - // Now overwrite mRTLastReportedPosition with our values - } catch (Exception ex) { - Log.e(TAG, "Exception from repositionChild", ex); - } - } - - /** - * Called by native on RenderThread to notify that the view is no longer in the - * draw tree. UI thread is blocked at this point. - * @hide - */ - public final void surfacePositionLost_uiRtSync(long frameNumber) { - if (DEBUG) { - Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d", - System.identityHashCode(this), frameNumber)); - } - mRTLastReportedPosition.setEmpty(); - - if (mSurfaceControl == null) { - return; - } - if (mRtHandlingPositionUpdates) { - mRtHandlingPositionUpdates = false; - // This callback will happen while the UI thread is blocked, so we can - // safely access other member variables at this time. - // So do what the UI thread would have done if RT wasn't handling position - // updates. - if (!mScreenRect.isEmpty() && !mScreenRect.equals(mRTLastReportedPosition)) { - try { - if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition, " + - "postion = [%d, %d, %d, %d]", System.identityHashCode(this), - mScreenRect.left, mScreenRect.top, - mScreenRect.right, mScreenRect.bottom)); - setParentSpaceRectangle(mScreenRect, frameNumber); - } catch (Exception ex) { - Log.e(TAG, "Exception configuring surface", ex); - } - } - } - } - - private SurfaceHolder.Callback[] getSurfaceCallbacks() { - SurfaceHolder.Callback callbacks[]; - synchronized (mCallbacks) { - callbacks = new SurfaceHolder.Callback[mCallbacks.size()]; - mCallbacks.toArray(callbacks); - } - return callbacks; - } - - /** - * This method still exists only for compatibility reasons because some applications have relied - * on this method via reflection. See Issue 36345857 for details. - * - * @deprecated No platform code is using this method anymore. - * @hide - */ - @Deprecated - public void setWindowType(int type) { - if (getContext().getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O) { - throw new UnsupportedOperationException( - "SurfaceView#setWindowType() has never been a public API."); - } - - if (type == TYPE_APPLICATION_PANEL) { - Log.e(TAG, "If you are calling SurfaceView#setWindowType(TYPE_APPLICATION_PANEL) " - + "just to make the SurfaceView to be placed on top of its window, you must " - + "call setZOrderOnTop(true) instead.", new Throwable()); - setZOrderOnTop(true); - return; - } - Log.e(TAG, "SurfaceView#setWindowType(int) is deprecated and now does nothing. " - + "type=" + type, new Throwable()); - } - - private void runOnUiThread(Runnable runnable) { - Handler handler = getHandler(); - if (handler != null && handler.getLooper() != Looper.myLooper()) { - handler.post(runnable); - } else { - runnable.run(); - } - } - - /** - * Check to see if the surface has fixed size dimensions or if the surface's - * dimensions are dimensions are dependent on its current layout. - * - * @return true if the surface has dimensions that are fixed in size - * @hide - */ - public boolean isFixedSize() { - return (mRequestedWidth != -1 || mRequestedHeight != -1); - } - - private boolean isAboveParent() { - return mSubLayer >= 0; + public SurfaceHolder getHolder() { + return mSurfaceHolder; } - private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() { - private static final String LOG_TAG = "SurfaceHolder"; + private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { @Override public boolean isCreating() { - return mIsCreating; + return false; } @Override public void addCallback(Callback callback) { - synchronized (mCallbacks) { - // This is a linear search, but in practice we'll - // have only a couple callbacks, so it doesn't matter. - if (mCallbacks.contains(callback) == false) { - mCallbacks.add(callback); - } - } } @Override public void removeCallback(Callback callback) { - synchronized (mCallbacks) { - mCallbacks.remove(callback); - } } @Override public void setFixedSize(int width, int height) { - if (mRequestedWidth != width || mRequestedHeight != height) { - mRequestedWidth = width; - mRequestedHeight = height; - requestLayout(); - } } @Override public void setSizeFromLayout() { - if (mRequestedWidth != -1 || mRequestedHeight != -1) { - mRequestedWidth = mRequestedHeight = -1; - requestLayout(); - } } @Override public void setFormat(int format) { - // for backward compatibility reason, OPAQUE always - // means 565 for SurfaceView - if (format == PixelFormat.OPAQUE) - format = PixelFormat.RGB_565; - - mRequestedFormat = format; - if (mSurfaceControl != null) { - updateSurface(); - } } - /** - * @deprecated setType is now ignored. - */ @Override - @Deprecated - public void setType(int type) { } + public void setType(int type) { + } @Override public void setKeepScreenOn(boolean screenOn) { - runOnUiThread(() -> SurfaceView.this.setKeepScreenOn(screenOn)); } - /** - * Gets a {@link Canvas} for drawing into the SurfaceView's Surface - * - * After drawing into the provided {@link Canvas}, the caller must - * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. - * - * The caller must redraw the entire surface. - * @return A canvas for drawing into the surface. - */ @Override public Canvas lockCanvas() { - return internalLockCanvas(null, false); - } - - /** - * Gets a {@link Canvas} for drawing into the SurfaceView's Surface - * - * After drawing into the provided {@link Canvas}, the caller must - * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. - * - * @param inOutDirty A rectangle that represents the dirty region that the caller wants - * to redraw. This function may choose to expand the dirty rectangle if for example - * the surface has been resized or if the previous contents of the surface were - * not available. The caller must redraw the entire dirty region as represented - * by the contents of the inOutDirty rectangle upon return from this function. - * The caller may also pass <code>null</code> instead, in the case where the - * entire surface should be redrawn. - * @return A canvas for drawing into the surface. - */ - @Override - public Canvas lockCanvas(Rect inOutDirty) { - return internalLockCanvas(inOutDirty, false); + return null; } @Override - public Canvas lockHardwareCanvas() { - return internalLockCanvas(null, true); - } - - private Canvas internalLockCanvas(Rect dirty, boolean hardware) { - mSurfaceLock.lock(); - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped=" - + mDrawingStopped + ", surfaceControl=" + mSurfaceControl); - - Canvas c = null; - if (!mDrawingStopped && mSurfaceControl != null) { - try { - if (hardware) { - c = mSurface.lockHardwareCanvas(); - } else { - c = mSurface.lockCanvas(dirty); - } - } catch (Exception e) { - Log.e(LOG_TAG, "Exception locking surface", e); - } - } - - if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c); - if (c != null) { - mLastLockTime = SystemClock.uptimeMillis(); - return c; - } - - // If the Surface is not ready to be drawn, then return null, - // but throttle calls to this function so it isn't called more - // than every 100ms. - long now = SystemClock.uptimeMillis(); - long nextTime = mLastLockTime + 100; - if (nextTime > now) { - try { - Thread.sleep(nextTime-now); - } catch (InterruptedException e) { - } - now = SystemClock.uptimeMillis(); - } - mLastLockTime = now; - mSurfaceLock.unlock(); - + public Canvas lockCanvas(Rect dirty) { return null; } - /** - * Posts the new contents of the {@link Canvas} to the surface and - * releases the {@link Canvas}. - * - * @param canvas The canvas previously obtained from {@link #lockCanvas}. - */ @Override public void unlockCanvasAndPost(Canvas canvas) { - mSurface.unlockCanvasAndPost(canvas); - mSurfaceLock.unlock(); } @Override public Surface getSurface() { - return mSurface; + return null; } @Override public Rect getSurfaceFrame() { - return mSurfaceFrame; + return null; } }; - - class SurfaceControlWithBackground extends SurfaceControl { - private SurfaceControl mBackgroundControl; - private boolean mOpaque = true; - public boolean mVisible = false; - - public SurfaceControlWithBackground(SurfaceSession s, - String name, int w, int h, int format, int flags) - 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; - } - - @Override - public void setAlpha(float alpha) { - super.setAlpha(alpha); - mBackgroundControl.setAlpha(alpha); - } - - @Override - public void setLayer(int zorder) { - super.setLayer(zorder); - // -3 is below all other child layers as SurfaceView never goes below -2 - mBackgroundControl.setLayer(-3); - } - - @Override - public void setPosition(float x, float y) { - super.setPosition(x, y); - mBackgroundControl.setPosition(x, y); - } - - @Override - public void setSize(int w, int h) { - super.setSize(w, h); - mBackgroundControl.setSize(w, h); - } - - @Override - public void setWindowCrop(Rect crop) { - super.setWindowCrop(crop); - mBackgroundControl.setWindowCrop(crop); - } - - @Override - public void setFinalCrop(Rect crop) { - super.setFinalCrop(crop); - mBackgroundControl.setFinalCrop(crop); - } - - @Override - public void setLayerStack(int layerStack) { - super.setLayerStack(layerStack); - mBackgroundControl.setLayerStack(layerStack); - } - - @Override - public void setOpaque(boolean isOpaque) { - super.setOpaque(isOpaque); - mOpaque = isOpaque; - updateBackgroundVisibility(); - } - - @Override - public void setSecure(boolean isSecure) { - super.setSecure(isSecure); - } - - @Override - public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) { - super.setMatrix(dsdx, dtdx, dsdy, dtdy); - mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy); - } - - @Override - public void hide() { - super.hide(); - mVisible = false; - updateBackgroundVisibility(); - } - - @Override - public void show() { - super.show(); - mVisible = true; - updateBackgroundVisibility(); - } - - @Override - public void destroy() { - super.destroy(); - mBackgroundControl.destroy(); - } - - @Override - public void release() { - super.release(); - mBackgroundControl.release(); - } - - @Override - public void setTransparentRegionHint(Region region) { - super.setTransparentRegionHint(region); - mBackgroundControl.setTransparentRegionHint(region); - } - - @Override - public void deferTransactionUntil(IBinder handle, long frame) { - super.deferTransactionUntil(handle, frame); - mBackgroundControl.deferTransactionUntil(handle, frame); - } - - @Override - public void deferTransactionUntil(Surface barrier, long frame) { - super.deferTransactionUntil(barrier, frame); - mBackgroundControl.deferTransactionUntil(barrier, frame); - } - - void updateBackgroundVisibility() { - if (mOpaque && mVisible) { - mBackgroundControl.show(); - } else { - mBackgroundControl.hide(); - } - } - } } + diff --git a/android/view/View.java b/android/view/View.java index b6be2961..c043dcac 100644 --- a/android/view/View.java +++ b/android/view/View.java @@ -1448,17 +1448,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 +2342,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; @@ -6355,6 +6397,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 @@ -6363,7 +6441,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) { @@ -8907,7 +8992,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; @@ -8925,7 +9024,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); } @@ -11433,7 +11546,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: { if (isClickable()) { - performClick(); + performClickInternal(); return true; } } break; @@ -12545,7 +12658,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(); } } } @@ -13117,7 +13230,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { - performClick(); + performClickInternal(); } } } @@ -18103,7 +18216,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); @@ -18116,7 +18243,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; @@ -18130,10 +18271,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; } @@ -18257,7 +18399,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); } @@ -18288,7 +18444,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; @@ -18308,7 +18478,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(); @@ -18330,7 +18514,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; @@ -18342,7 +18540,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; @@ -18352,7 +18564,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); } @@ -18379,7 +18605,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)) { @@ -19812,7 +20052,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 + ")"); } @@ -24858,7 +25098,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private final class PerformClick implements Runnable { @Override public void run() { - performClick(); + performClickInternal(); } } diff --git a/android/view/ViewConfiguration.java b/android/view/ViewConfiguration.java index 574137b3..45008627 100644 --- a/android/view/ViewConfiguration.java +++ b/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 = 1500; + + /** * 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/android/view/ViewDebug.java b/android/view/ViewDebug.java index 3426485e..afa94131 100644 --- a/android/view/ViewDebug.java +++ b/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 - }; - } + ? profileViewDraw(view, node) : 0; - public void run(Object... data) { - if (data[1] != null) { - view.draw((Canvas) data[1]); - } - } - - 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); + } + } + } + + 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 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<T> { - T[] pre(); - void run(T... data); - void post(T... data); + interface ViewOperation { + default void pre() {} + + void run(); } - private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) { + 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(); } }); diff --git a/android/view/ViewGroup.java b/android/view/ViewGroup.java index b2e5a163..929beaea 100644 --- a/android/view/ViewGroup.java +++ b/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 @@ -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; @@ -6331,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"), @@ -6352,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; } diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java index 71106ada..99438d87 100644 --- a/android/view/ViewRootImpl.java +++ b/android/view/ViewRootImpl.java @@ -72,6 +72,7 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.MergedConfiguration; import android.util.Slog; +import android.util.SparseArray; import android.util.TimeUtils; import android.util.TypedValue; import android.view.Surface.OutOfResourcesException; @@ -1668,8 +1669,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(); @@ -2827,7 +2826,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 +2896,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. @@ -3469,6 +3466,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 +3729,273 @@ 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) + && 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) + || !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(); + + 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); - - 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); + mLastWasImTarget = WindowManager.LayoutParams + .mayUseInputMethod(mWindowAttributes.flags); - 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 +4208,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); @@ -4956,9 +4973,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 +4984,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 +5108,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 +5292,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 +5307,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 +5322,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 +6212,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) { @@ -6129,7 +6232,6 @@ public final class ViewRootImpl implements ViewParent, mPendingAlwaysConsumeNavBar = (relayoutResult & WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_NAV_BAR) != 0; - //Log.d(mTag, "<<<<<< BACK FROM relayout"); if (restore) { params.restore(); } diff --git a/android/view/ViewStructure.java b/android/view/ViewStructure.java index f671c349..d665dde3 100644 --- a/android/view/ViewStructure.java +++ b/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}. */ diff --git a/android/view/WindowManagerInternal.java b/android/view/WindowManagerInternal.java index 98f8dc8e..69cc1002 100644 --- a/android/view/WindowManagerInternal.java +++ b/android/view/WindowManagerInternal.java @@ -335,8 +335,8 @@ public abstract class WindowManagerInternal { public abstract void setOnHardKeyboardStatusChangeListener( OnHardKeyboardStatusChangeListener listener); - /** Returns true if the stack with the input Id is currently visible. */ - public abstract boolean isStackVisible(int stackId); + /** Returns true if a stack in the windowing mode is currently visible. */ + public abstract boolean isStackVisible(int windowingMode); /** * @return True if and only if the docked divider is currently in resize mode. diff --git a/android/view/WindowManagerPolicy.java b/android/view/WindowManagerPolicy.java index da72535d..137e551d 100644 --- a/android/view/WindowManagerPolicy.java +++ b/android/view/WindowManagerPolicy.java @@ -467,11 +467,8 @@ public interface WindowManagerPolicy { */ 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(); + /** @return the current windowing mode of this window. */ + int getWindowingMode(); /** * Returns true if the window is current in multi-windowing mode. i.e. it shares the diff --git a/android/view/accessibility/AccessibilityCache.java b/android/view/accessibility/AccessibilityCache.java index 0f21c5c8..d7851171 100644 --- a/android/view/accessibility/AccessibilityCache.java +++ b/android/view/accessibility/AccessibilityCache.java @@ -329,8 +329,6 @@ public final class AccessibilityCache { final long oldParentId = oldInfo.getParentNodeId(); if (info.getParentNodeId() != oldParentId) { clearSubTreeLocked(windowId, oldParentId); - } else { - oldInfo.recycle(); } } diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 0b9bc576..11cb046a 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -16,152 +16,46 @@ package android.view.accessibility; -import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME; - -import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SdkConstant; -import android.annotation.SystemService; -import android.content.ComponentName; import android.content.Context; -import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.content.res.Resources; -import android.os.Binder; import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.util.ArrayMap; -import android.util.Log; -import android.util.SparseArray; import android.view.IWindow; import android.view.View; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.IntPair; - -import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * System level service that serves as an event dispatch for {@link AccessibilityEvent}s, - * and provides facilities for querying the accessibility state of the system. - * Accessibility events are generated when something notable happens in the user interface, + * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. + * Such events are generated when something notable happens in the user interface, * for example an {@link android.app.Activity} starts, the focus or selection of a * {@link android.view.View} changes etc. Parties interested in handling accessibility * events implement and register an accessibility service which extends - * {@link android.accessibilityservice.AccessibilityService}. + * {@code android.accessibilityservice.AccessibilityService}. * * @see AccessibilityEvent - * @see AccessibilityNodeInfo - * @see android.accessibilityservice.AccessibilityService - * @see Context#getSystemService - * @see Context#ACCESSIBILITY_SERVICE + * @see android.content.Context#getSystemService */ -@SystemService(Context.ACCESSIBILITY_SERVICE) +@SuppressWarnings("UnusedDeclaration") public final class AccessibilityManager { - private static final boolean DEBUG = false; - - private static final String LOG_TAG = "AccessibilityManager"; - - /** @hide */ - public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001; - - /** @hide */ - public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; - - /** @hide */ - public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004; - - /** @hide */ - public static final int DALTONIZER_DISABLED = -1; - - /** @hide */ - public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; - - /** @hide */ - public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; - - /** @hide */ - public static final int AUTOCLICK_DELAY_DEFAULT = 600; - - /** - * Activity action: Launch UI to manage which accessibility service or feature is assigned - * to the navigation bar Accessibility button. - * <p> - * Input: Nothing. - * </p> - * <p> - * Output: Nothing. - * </p> - * - * @hide - */ - @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) - public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON = - "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON"; - - static final Object sInstanceSync = new Object(); - - private static AccessibilityManager sInstance; - - private final Object mLock = new Object(); - - private IAccessibilityManager mService; - - final int mUserId; - - final Handler mHandler; - - final Handler.Callback mCallback; - - boolean mIsEnabled; - int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK; + private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0); - boolean mIsTouchExplorationEnabled; - - boolean mIsHighTextContrastEnabled; - - private final ArrayMap<AccessibilityStateChangeListener, Handler> - mAccessibilityStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap<TouchExplorationStateChangeListener, Handler> - mTouchExplorationStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap<HighTextContrastChangeListener, Handler> - mHighTextContrastStateChangeListeners = new ArrayMap<>(); - - private final ArrayMap<AccessibilityServicesStateChangeListener, Handler> - mServicesStateChangeListeners = new ArrayMap<>(); - - /** - * Map from a view's accessibility id to the list of request preparers set for that view - */ - private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists; /** - * Listener for the system accessibility state. To listen for changes to the - * accessibility state on the device, implement this interface and register - * it with the system by calling {@link #addAccessibilityStateChangeListener}. + * Listener for the accessibility state. */ public interface AccessibilityStateChangeListener { /** - * Called when the accessibility enabled state changes. + * Called back on change in the accessibility state. * * @param enabled Whether accessibility is enabled. */ - void onAccessibilityStateChanged(boolean enabled); + public void onAccessibilityStateChanged(boolean enabled); } /** @@ -177,24 +71,7 @@ public final class AccessibilityManager { * * @param enabled Whether touch exploration is enabled. */ - void onTouchExplorationStateChanged(boolean enabled); - } - - /** - * Listener for changes to the state of accessibility services. Changes include services being - * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service. - * {@see #addAccessibilityServicesStateChangeListener}. - * - * @hide - */ - public interface AccessibilityServicesStateChangeListener { - - /** - * Called when the state of accessibility services changes. - * - * @param manager The manager that is calling back - */ - void onAccessibilityServicesStateChanged(AccessibilityManager manager); + public void onTouchExplorationStateChanged(boolean enabled); } /** @@ -202,8 +79,6 @@ public final class AccessibilityManager { * the high text contrast state on the device, implement this interface and * register it with the system by calling * {@link #addHighTextContrastStateChangeListener}. - * - * @hide */ public interface HighTextContrastChangeListener { @@ -212,72 +87,26 @@ public final class AccessibilityManager { * * @param enabled Whether high text contrast is enabled. */ - void onHighTextContrastStateChanged(boolean enabled); + public void onHighTextContrastStateChanged(boolean enabled); } private final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { - @Override - public void setState(int state) { - // We do not want to change this immediately as the application may - // have already checked that accessibility is on and fired an event, - // that is now propagating up the view tree, Hence, if accessibility - // is now off an exception will be thrown. We want to have the exception - // enforcement to guard against apps that fire unnecessary accessibility - // events when accessibility is off. - mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget(); - } - - @Override - public void notifyServicesStateChanged() { - final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners; - synchronized (mLock) { - if (mServicesStateChangeListeners.isEmpty()) { - return; + public void setState(int state) { } - listeners = new ArrayMap<>(mServicesStateChangeListeners); - } - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final AccessibilityServicesStateChangeListener listener = - mServicesStateChangeListeners.keyAt(i); - mServicesStateChangeListeners.valueAt(i).post(() -> listener - .onAccessibilityServicesStateChanged(AccessibilityManager.this)); - } - } + public void notifyServicesStateChanged() { + } - @Override - public void setRelevantEventTypes(int eventTypes) { - mRelevantEventTypes = eventTypes; - } - }; + public void setRelevantEventTypes(int eventTypes) { + } + }; /** * Get an AccessibilityManager instance (create one if necessary). * - * @param context Context in which this manager operates. - * - * @hide */ public static AccessibilityManager getInstance(Context context) { - synchronized (sInstanceSync) { - if (sInstance == null) { - final int userId; - if (Binder.getCallingUid() == Process.SYSTEM_UID - || context.checkCallingOrSelfPermission( - Manifest.permission.INTERACT_ACROSS_USERS) - == PackageManager.PERMISSION_GRANTED - || context.checkCallingOrSelfPermission( - Manifest.permission.INTERACT_ACROSS_USERS_FULL) - == PackageManager.PERMISSION_GRANTED) { - userId = UserHandle.USER_CURRENT; - } else { - userId = UserHandle.myUserId(); - } - sInstance = new AccessibilityManager(context, null, userId); - } - } return sInstance; } @@ -285,68 +114,21 @@ public final class AccessibilityManager { * Create an instance. * * @param context A {@link Context}. - * @param service An interface to the backing service. - * @param userId User id under which to run. - * - * @hide */ public AccessibilityManager(Context context, IAccessibilityManager service, int userId) { - // Constructor can't be chained because we can't create an instance of an inner class - // before calling another constructor. - mCallback = new MyCallback(); - mHandler = new Handler(context.getMainLooper(), mCallback); - mUserId = userId; - synchronized (mLock) { - tryConnectToServiceLocked(service); - } - } - - /** - * Create an instance. - * - * @param handler The handler to use - * @param service An interface to the backing service. - * @param userId User id under which to run. - * - * @hide - */ - public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) { - mCallback = new MyCallback(); - mHandler = handler; - mUserId = userId; - synchronized (mLock) { - tryConnectToServiceLocked(service); - } } - /** - * @hide - */ public IAccessibilityManagerClient getClient() { return mClient; } /** - * @hide - */ - @VisibleForTesting - public Handler.Callback getCallback() { - return mCallback; - } - - /** - * Returns if the accessibility in the system is enabled. + * Returns if the {@link AccessibilityManager} is enabled. * - * @return True if accessibility is enabled, false otherwise. + * @return True if this {@link AccessibilityManager} is enabled, false otherwise. */ public boolean isEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsEnabled; - } + return false; } /** @@ -355,13 +137,7 @@ public final class AccessibilityManager { * @return True if touch exploration is enabled, false otherwise. */ public boolean isTouchExplorationEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsTouchExplorationEnabled; - } + return true; } /** @@ -371,169 +147,35 @@ public final class AccessibilityManager { * doing its own rendering and does not rely on the platform rendering pipeline. * </p> * - * @return True if high text contrast is enabled, false otherwise. - * - * @hide */ public boolean isHighTextContrastEnabled() { - synchronized (mLock) { - IAccessibilityManager service = getServiceLocked(); - if (service == null) { - return false; - } - return mIsHighTextContrastEnabled; - } + return false; } /** * Sends an {@link AccessibilityEvent}. - * - * @param event The event to send. - * - * @throws IllegalStateException if accessibility is not enabled. - * - * <strong>Note:</strong> The preferred mechanism for sending custom accessibility - * events is through calling - * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} - * instead of this method to allow predecessors to augment/filter events sent by - * their descendants. */ public void sendAccessibilityEvent(AccessibilityEvent event) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - if (!mIsEnabled) { - Looper myLooper = Looper.myLooper(); - if (myLooper == Looper.getMainLooper()) { - throw new IllegalStateException( - "Accessibility off. Did you forget to check that?"); - } else { - // If we're not running on the thread with the main looper, it's possible for - // the state of accessibility to change between checking isEnabled and - // calling this method. So just log the error rather than throwing the - // exception. - Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); - return; - } - } - if ((event.getEventType() & mRelevantEventTypes) == 0) { - if (DEBUG) { - Log.i(LOG_TAG, "Not dispatching irrelevant event: " + event - + " that is not among " - + AccessibilityEvent.eventTypeToString(mRelevantEventTypes)); - } - return; - } - userId = mUserId; - } - try { - event.setEventTime(SystemClock.uptimeMillis()); - // it is possible that this manager is in the same process as the service but - // 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); - if (DEBUG) { - Log.i(LOG_TAG, event + " sent"); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error during sending " + event + " ", re); - } finally { - event.recycle(); - } } /** - * Requests feedback interruption from all accessibility services. + * Requests interruption of the accessibility feedback from all accessibility services. */ public void interrupt() { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - if (!mIsEnabled) { - Looper myLooper = Looper.myLooper(); - if (myLooper == Looper.getMainLooper()) { - throw new IllegalStateException( - "Accessibility off. Did you forget to check that?"); - } else { - // If we're not running on the thread with the main looper, it's possible for - // the state of accessibility to change between checking isEnabled and - // calling this method. So just log the error rather than throwing the - // exception. - Log.e(LOG_TAG, "Interrupt called with accessibility disabled"); - return; - } - } - userId = mUserId; - } - try { - service.interrupt(userId); - if (DEBUG) { - Log.i(LOG_TAG, "Requested interrupt from all services"); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re); - } } /** * Returns the {@link ServiceInfo}s of the installed accessibility services. * * @return An unmodifiable list with {@link ServiceInfo}s. - * - * @deprecated Use {@link #getInstalledAccessibilityServiceList()} */ @Deprecated public List<ServiceInfo> getAccessibilityServiceList() { - List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList(); - List<ServiceInfo> services = new ArrayList<>(); - final int infoCount = infos.size(); - for (int i = 0; i < infoCount; i++) { - AccessibilityServiceInfo info = infos.get(i); - services.add(info.getResolveInfo().serviceInfo); - } - return Collections.unmodifiableList(services); + return Collections.emptyList(); } - /** - * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. - * - * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. - */ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return Collections.emptyList(); - } - userId = mUserId; - } - - List<AccessibilityServiceInfo> services = null; - try { - services = service.getInstalledAccessibilityServiceList(userId); - if (DEBUG) { - Log.i(LOG_TAG, "Installed AccessibilityServices " + services); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); - } - if (services != null) { - return Collections.unmodifiableList(services); - } else { - return Collections.emptyList(); - } + return Collections.emptyList(); } /** @@ -548,48 +190,21 @@ public final class AccessibilityManager { * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN * @see AccessibilityServiceInfo#FEEDBACK_VISUAL - * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE */ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList( int feedbackTypeFlags) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return Collections.emptyList(); - } - userId = mUserId; - } - - List<AccessibilityServiceInfo> services = null; - try { - services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId); - if (DEBUG) { - Log.i(LOG_TAG, "Installed AccessibilityServices " + services); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); - } - if (services != null) { - return Collections.unmodifiableList(services); - } else { - return Collections.emptyList(); - } + return Collections.emptyList(); } /** * Registers an {@link AccessibilityStateChangeListener} for changes in - * the global accessibility state of the system. Equivalent to calling - * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)} - * with a null handler. + * the global accessibility state of the system. * * @param listener The listener. - * @return Always returns {@code true}. + * @return True if successfully registered. */ public boolean addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener) { - addAccessibilityStateChangeListener(listener, null); + AccessibilityStateChangeListener listener) { return true; } @@ -603,40 +218,22 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mAccessibilityStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {} - /** - * Unregisters an {@link AccessibilityStateChangeListener}. - * - * @param listener The listener. - * @return True if the listener was previously registered. - */ public boolean removeAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener) { - synchronized (mLock) { - int index = mAccessibilityStateChangeListeners.indexOfKey(listener); - mAccessibilityStateChangeListeners.remove(listener); - return (index >= 0); - } + AccessibilityStateChangeListener listener) { + return true; } /** * Registers a {@link TouchExplorationStateChangeListener} for changes in - * the global touch exploration state of the system. Equivalent to calling - * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)} - * with a null handler. + * the global touch exploration state of the system. * * @param listener The listener. - * @return Always returns {@code true}. + * @return True if successfully registered. */ public boolean addTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - addTouchExplorationStateChangeListener(listener, null); return true; } @@ -650,103 +247,17 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addTouchExplorationStateChangeListener( - @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mTouchExplorationStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {} /** * Unregisters a {@link TouchExplorationStateChangeListener}. * * @param listener The listener. - * @return True if listener was previously registered. + * @return True if successfully unregistered. */ public boolean removeTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - synchronized (mLock) { - int index = mTouchExplorationStateChangeListeners.indexOfKey(listener); - mTouchExplorationStateChangeListeners.remove(listener); - return (index >= 0); - } - } - - /** - * Registers a {@link AccessibilityServicesStateChangeListener}. - * - * @param listener The listener. - * @param handler The handler on which the listener should be called back, or {@code null} - * for a callback on the process's main handler. - * @hide - */ - public void addAccessibilityServicesStateChangeListener( - @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mServicesStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } - - /** - * Unregisters a {@link AccessibilityServicesStateChangeListener}. - * - * @param listener The listener. - * - * @hide - */ - public void removeAccessibilityServicesStateChangeListener( - @NonNull AccessibilityServicesStateChangeListener listener) { - // Final CopyOnWriteArrayList - no lock needed. - mServicesStateChangeListeners.remove(listener); - } - - /** - * Registers a {@link AccessibilityRequestPreparer}. - */ - public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { - if (mRequestPreparerLists == null) { - mRequestPreparerLists = new SparseArray<>(1); - } - int id = preparer.getView().getAccessibilityViewId(); - List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id); - if (requestPreparerList == null) { - requestPreparerList = new ArrayList<>(1); - mRequestPreparerLists.put(id, requestPreparerList); - } - requestPreparerList.add(preparer); - } - - /** - * Unregisters a {@link AccessibilityRequestPreparer}. - */ - public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { - if (mRequestPreparerLists == null) { - return; - } - int viewId = preparer.getView().getAccessibilityViewId(); - List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId); - if (requestPreparerList != null) { - requestPreparerList.remove(preparer); - if (requestPreparerList.isEmpty()) { - mRequestPreparerLists.remove(viewId); - } - } - } - - /** - * Get the preparers that are registered for an accessibility ID - * - * @param id The ID of interest - * @return The list of preparers, or {@code null} if there are none. - * - * @hide - */ - public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) { - if (mRequestPreparerLists == null) { - return null; - } - return mRequestPreparerLists.get(id); + return true; } /** @@ -758,12 +269,7 @@ public final class AccessibilityManager { * @hide */ public void addHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) { - synchronized (mLock) { - mHighTextContrastStateChangeListeners - .put(listener, (handler == null) ? mHandler : handler); - } - } + @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {} /** * Unregisters a {@link HighTextContrastChangeListener}. @@ -773,51 +279,7 @@ public final class AccessibilityManager { * @hide */ public void removeHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener) { - synchronized (mLock) { - mHighTextContrastStateChangeListeners.remove(listener); - } - } - - /** - * Check if the accessibility volume stream is active. - * - * @return True if accessibility volume is active (i.e. some service has requested it). False - * otherwise. - * @hide - */ - public boolean isAccessibilityVolumeStreamActive() { - List<AccessibilityServiceInfo> serviceInfos = - getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); - for (int i = 0; i < serviceInfos.size(); i++) { - if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) { - return true; - } - } - return false; - } - - /** - * Report a fingerprint gesture to accessibility. Only available for the system process. - * - * @param keyCode The key code of the gesture - * @return {@code true} if accessibility consumes the event. {@code false} if not. - * @hide - */ - public boolean sendFingerprintGesture(int keyCode) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return false; - } - } - try { - return service.sendFingerprintGesture(keyCode); - } catch (RemoteException e) { - return false; - } - } + @NonNull HighTextContrastChangeListener listener) {} /** * Sets the current state and notifies listeners, if necessary. @@ -825,314 +287,14 @@ public final class AccessibilityManager { * @param stateFlags The state flags. */ private void setStateLocked(int stateFlags) { - final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0; - final boolean touchExplorationEnabled = - (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0; - final boolean highTextContrastEnabled = - (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0; - - final boolean wasEnabled = mIsEnabled; - final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; - final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled; - - // Ensure listeners get current state from isZzzEnabled() calls. - mIsEnabled = enabled; - mIsTouchExplorationEnabled = touchExplorationEnabled; - mIsHighTextContrastEnabled = highTextContrastEnabled; - - if (wasEnabled != enabled) { - notifyAccessibilityStateChanged(); - } - - if (wasTouchExplorationEnabled != touchExplorationEnabled) { - notifyTouchExplorationStateChanged(); - } - - if (wasHighTextContrastEnabled != highTextContrastEnabled) { - notifyHighTextContrastStateChanged(); - } } - /** - * Find an installed service with the specified {@link ComponentName}. - * - * @param componentName The name to match to the service. - * - * @return The info corresponding to the installed service, or {@code null} if no such service - * is installed. - * @hide - */ - public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName( - ComponentName componentName) { - final List<AccessibilityServiceInfo> installedServiceInfos = - getInstalledAccessibilityServiceList(); - if ((installedServiceInfos == null) || (componentName == null)) { - return null; - } - for (int i = 0; i < installedServiceInfos.size(); i++) { - if (componentName.equals(installedServiceInfos.get(i).getComponentName())) { - return installedServiceInfos.get(i); - } - } - return null; - } - - /** - * Adds an accessibility interaction connection interface for a given window. - * @param windowToken The window token to which a connection is added. - * @param connection The connection. - * - * @hide - */ public int addAccessibilityInteractionConnection(IWindow windowToken, IAccessibilityInteractionConnection connection) { - final IAccessibilityManager service; - final int userId; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return View.NO_ID; - } - userId = mUserId; - } - try { - return service.addAccessibilityInteractionConnection(windowToken, connection, userId); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); - } return View.NO_ID; } - /** - * Removed an accessibility interaction connection interface for a given window. - * @param windowToken The window token to which a connection is removed. - * - * @hide - */ public void removeAccessibilityInteractionConnection(IWindow windowToken) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.removeAccessibilityInteractionConnection(windowToken); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); - } - } - - /** - * Perform the accessibility shortcut if the caller has permission. - * - * @hide - */ - public void performAccessibilityShortcut() { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.performAccessibilityShortcut(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re); - } - } - - /** - * Notifies that the accessibility button in the system's navigation area has been clicked - * - * @hide - */ - public void notifyAccessibilityButtonClicked() { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.notifyAccessibilityButtonClicked(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while dispatching accessibility button click", re); - } - } - - /** - * Notifies that the visibility of the accessibility button in the system's navigation area - * has changed. - * - * @param shown {@code true} if the accessibility button is visible within the system - * navigation area, {@code false} otherwise - * @hide - */ - public void notifyAccessibilityButtonVisibilityChanged(boolean shown) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.notifyAccessibilityButtonVisibilityChanged(shown); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re); - } - } - - /** - * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture - * window. Intended for use by the System UI only. - * - * @param connection The connection to handle the actions. Set to {@code null} to avoid - * affecting the actions. - * - * @hide - */ - public void setPictureInPictureActionReplacingConnection( - @Nullable IAccessibilityInteractionConnection connection) { - final IAccessibilityManager service; - synchronized (mLock) { - service = getServiceLocked(); - if (service == null) { - return; - } - } - try { - service.setPictureInPictureActionReplacingConnection(connection); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error setting picture in picture action replacement", re); - } } - private IAccessibilityManager getServiceLocked() { - if (mService == null) { - tryConnectToServiceLocked(null); - } - return mService; - } - - private void tryConnectToServiceLocked(IAccessibilityManager service) { - if (service == null) { - IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); - if (iBinder == null) { - return; - } - service = IAccessibilityManager.Stub.asInterface(iBinder); - } - - try { - final long userStateAndRelevantEvents = service.addClient(mClient, mUserId); - setStateLocked(IntPair.first(userStateAndRelevantEvents)); - mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents); - mService = service; - } catch (RemoteException re) { - Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); - } - } - - /** - * Notifies the registered {@link AccessibilityStateChangeListener}s. - */ - private void notifyAccessibilityStateChanged() { - final boolean isEnabled; - final ArrayMap<AccessibilityStateChangeListener, Handler> listeners; - synchronized (mLock) { - if (mAccessibilityStateChangeListeners.isEmpty()) { - return; - } - isEnabled = mIsEnabled; - listeners = new ArrayMap<>(mAccessibilityStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final AccessibilityStateChangeListener listener = - mAccessibilityStateChangeListeners.keyAt(i); - mAccessibilityStateChangeListeners.valueAt(i) - .post(() -> listener.onAccessibilityStateChanged(isEnabled)); - } - } - - /** - * Notifies the registered {@link TouchExplorationStateChangeListener}s. - */ - private void notifyTouchExplorationStateChanged() { - final boolean isTouchExplorationEnabled; - final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners; - synchronized (mLock) { - if (mTouchExplorationStateChangeListeners.isEmpty()) { - return; - } - isTouchExplorationEnabled = mIsTouchExplorationEnabled; - listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final TouchExplorationStateChangeListener listener = - mTouchExplorationStateChangeListeners.keyAt(i); - mTouchExplorationStateChangeListeners.valueAt(i) - .post(() -> listener.onTouchExplorationStateChanged(isTouchExplorationEnabled)); - } - } - - /** - * Notifies the registered {@link HighTextContrastChangeListener}s. - */ - private void notifyHighTextContrastStateChanged() { - final boolean isHighTextContrastEnabled; - final ArrayMap<HighTextContrastChangeListener, Handler> listeners; - synchronized (mLock) { - if (mHighTextContrastStateChangeListeners.isEmpty()) { - return; - } - isHighTextContrastEnabled = mIsHighTextContrastEnabled; - listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners); - } - - int numListeners = listeners.size(); - for (int i = 0; i < numListeners; i++) { - final HighTextContrastChangeListener listener = - mHighTextContrastStateChangeListeners.keyAt(i); - mHighTextContrastStateChangeListeners.valueAt(i) - .post(() -> listener.onHighTextContrastStateChanged(isHighTextContrastEnabled)); - } - } - - /** - * Determines if the accessibility button within the system navigation area is supported. - * - * @return {@code true} if the accessibility button is supported on this device, - * {@code false} otherwise - */ - public static boolean isAccessibilityButtonSupported() { - final Resources res = Resources.getSystem(); - return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar); - } - - private final class MyCallback implements Handler.Callback { - public static final int MSG_SET_STATE = 1; - - @Override - public boolean handleMessage(Message message) { - switch (message.what) { - case MSG_SET_STATE: { - // See comment at mClient - final int state = message.arg1; - synchronized (mLock) { - setStateLocked(state); - } - } break; - } - return true; - } - } } diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java index 4fb2a99a..e564fa34 100644 --- a/android/view/autofill/AutofillManager.java +++ b/android/view/autofill/AutofillManager.java @@ -91,10 +91,10 @@ import java.util.Objects; * </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 @@ -108,7 +108,7 @@ import java.util.Objects; * </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. @@ -150,6 +150,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} */ @@ -311,6 +317,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 { /** @@ -834,6 +848,46 @@ 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) { + 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. * @@ -850,12 +904,15 @@ public final class AutofillManager { return; } synchronized (mLock) { - if (!mEnabled && !isActiveLocked()) { - return; - } + commitLocked(); + } + } - finishSessionLocked(); + private void commitLocked() { + if (!mEnabled && !isActiveLocked()) { + return; } + finishSessionLocked(); } /** @@ -874,12 +931,15 @@ public final class AutofillManager { return; } synchronized (mLock) { - if (!mEnabled && !isActiveLocked()) { - return; - } + cancelLocked(); + } + } - cancelSessionLocked(); + private void cancelLocked() { + if (!mEnabled && !isActiveLocked()) { + return; } + cancelSessionLocked(); } /** @hide */ @@ -937,7 +997,12 @@ public final class AutofillManager { } private AutofillClient getClientLocked() { - return mContext.getAutofillClient(); + final AutofillClient client = mContext.getAutofillClient(); + if (client == null && sDebug) { + Log.d(TAG, "No AutofillClient for " + mContext.getPackageName() + " on context " + + mContext); + } + return client; } /** @hide */ @@ -959,6 +1024,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()); @@ -1038,6 +1107,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, @@ -1289,12 +1359,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) { @@ -1302,6 +1375,7 @@ public final class AutofillManager { } else { mTrackedViews = null; } + mSaveOnFinish = saveOnFinish; if (fillableIds != null) { if (mFillableIds == null) { mFillableIds = new ArraySet<>(fillableIds.length); @@ -1314,10 +1388,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) { @@ -1490,6 +1584,7 @@ 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("enabled: "); pw.println(mEnabled); pw.print(pfx); pw.print("hasService: "); pw.println(mService != null); pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null); @@ -1504,6 +1599,8 @@ 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); } private String getStateAsStringLocked() { @@ -1752,7 +1849,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 { @@ -1762,26 +1859,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; @@ -1883,12 +1980,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/android/view/inputmethod/InputMethod.java b/android/view/inputmethod/InputMethod.java index 0922422c..ab8886bb 100644 --- a/android/view/inputmethod/InputMethod.java +++ b/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/android/view/textclassifier/Log.java b/android/view/textclassifier/Log.java new file mode 100644 index 00000000..83ca15df --- /dev/null +++ b/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/android/view/textclassifier/SmartSelection.java b/android/view/textclassifier/SmartSelection.java index f0e83d1f..2c93a19b 100644 --- a/android/view/textclassifier/SmartSelection.java +++ b/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/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java index bb1e693f..c3601d9d 100644 --- a/android/view/textclassifier/TextClassifier.java +++ b/android/view/textclassifier/TextClassifier.java @@ -152,4 +152,12 @@ public interface TextClassifier { */ @WorkerThread default void logEvent(String source, String event) {} + + /** + * Returns this TextClassifier's settings. + * @hide + */ + default TextClassifierConstants getSettings() { + return TextClassifierConstants.DEFAULT; + } } diff --git a/android/view/textclassifier/TextClassifierConstants.java b/android/view/textclassifier/TextClassifierConstants.java new file mode 100644 index 00000000..51e6168e --- /dev/null +++ b/android/view/textclassifier/TextClassifierConstants.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.textclassifier; + +import android.annotation.Nullable; +import android.util.KeyValueListParser; +import android.util.Slog; + +/** + * TextClassifier specific settings. + * This is encoded as a key=value list, separated by commas. Ex: + * + * <pre> + * smart_selection_dark_launch (boolean) + * smart_selection_enabled_for_edit_text (boolean) + * </pre> + * + * <p> + * Type: string + * see also android.provider.Settings.Global.TEXT_CLASSIFIER_CONSTANTS + * + * Example of setting the values for testing. + * adb shell settings put global text_classifier_constants smart_selection_dark_launch=true,smart_selection_enabled_for_edit_text=true + * @hide + */ +public final class TextClassifierConstants { + + private static final String LOG_TAG = "TextClassifierConstants"; + + private static final String SMART_SELECTION_DARK_LAUNCH = + "smart_selection_dark_launch"; + private static final String SMART_SELECTION_ENABLED_FOR_EDIT_TEXT = + "smart_selection_enabled_for_edit_text"; + + private static final boolean SMART_SELECTION_DARK_LAUNCH_DEFAULT = false; + private static final boolean SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT = true; + + /** Default settings. */ + static final TextClassifierConstants DEFAULT = new TextClassifierConstants(); + + private final boolean mDarkLaunch; + private final boolean mSuggestSelectionEnabledForEditableText; + + private TextClassifierConstants() { + mDarkLaunch = SMART_SELECTION_DARK_LAUNCH_DEFAULT; + mSuggestSelectionEnabledForEditableText = SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT; + } + + private TextClassifierConstants(@Nullable String settings) { + final KeyValueListParser parser = new KeyValueListParser(','); + try { + parser.setString(settings); + } catch (IllegalArgumentException e) { + // Failed to parse the settings string, log this and move on with defaults. + Slog.e(LOG_TAG, "Bad TextClassifier settings: " + settings); + } + mDarkLaunch = parser.getBoolean( + SMART_SELECTION_DARK_LAUNCH, + SMART_SELECTION_DARK_LAUNCH_DEFAULT); + mSuggestSelectionEnabledForEditableText = parser.getBoolean( + SMART_SELECTION_ENABLED_FOR_EDIT_TEXT, + SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT); + } + + static TextClassifierConstants loadFromString(String settings) { + return new TextClassifierConstants(settings); + } + + public boolean isDarkLaunch() { + return mDarkLaunch; + } + + public boolean isSuggestSelectionEnabledForEditableText() { + return mSuggestSelectionEnabledForEditableText; + } +} diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java index 2aa81a2c..1c07be4b 100644 --- a/android/view/textclassifier/TextClassifierImpl.java +++ b/android/view/textclassifier/TextClassifierImpl.java @@ -24,18 +24,17 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; -import android.icu.text.BreakIterator; 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; @@ -47,6 +46,7 @@ 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; @@ -91,6 +91,8 @@ final class TextClassifierImpl implements TextClassifier { @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. private SmartSelection mSmartSelection; + private TextClassifierConstants mSettings; + TextClassifierImpl(Context context) { mContext = Preconditions.checkNotNull(context); } @@ -160,7 +162,7 @@ final class TextClassifierImpl implements TextClassifier { } } 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( @@ -189,6 +191,15 @@ final class TextClassifierImpl implements TextClassifier { } } + @Override + public TextClassifierConstants getSettings() { + if (mSettings == null) { + mSettings = TextClassifierConstants.loadFromString(Settings.Global.getString( + mContext.getContentResolver(), Settings.Global.TEXT_CLASSIFIER_CONSTANTS)); + } + return mSettings; + } + private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException { synchronized (mSmartSelectionLock) { localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList; diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java index f368c74a..8e1f2183 100644 --- a/android/view/textservice/TextServicesManager.java +++ b/android/view/textservice/TextServicesManager.java @@ -1,213 +1,58 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2016 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 + * 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 + * 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. + * 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.textservice; -import android.annotation.SystemService; -import android.content.Context; import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.ServiceManager.ServiceNotFoundException; -import android.util.Log; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; -import com.android.internal.textservice.ITextServicesManager; - import java.util.Locale; /** - * System API to the overall text services, which arbitrates interaction between applications - * and text services. - * - * The user can change the current text services in Settings. And also applications can specify - * the target text services. - * - * <h3>Architecture Overview</h3> - * - * <p>There are three primary parties involved in the text services - * framework (TSF) architecture:</p> - * - * <ul> - * <li> The <strong>text services manager</strong> as expressed by this class - * is the central point of the system that manages interaction between all - * other parts. It is expressed as the client-side API here which exists - * in each application context and communicates with a global system service - * that manages the interaction across all processes. - * <li> A <strong>text service</strong> implements a particular - * interaction model allowing the client application to retrieve information of text. - * The system binds to the current text service that is in use, causing it to be created and run. - * <li> Multiple <strong>client applications</strong> arbitrate with the text service - * manager for connections to text services. - * </ul> - * - * <h3>Text services sessions</h3> - * <ul> - * <li>The <strong>spell checker session</strong> is one of the text services. - * {@link android.view.textservice.SpellCheckerSession}</li> - * </ul> - * + * A stub class of TextServicesManager for Layout-Lib. */ -@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) public final class TextServicesManager { - private static final String TAG = TextServicesManager.class.getSimpleName(); - private static final boolean DBG = false; - - private static TextServicesManager sInstance; - - private final ITextServicesManager mService; - - private TextServicesManager() throws ServiceNotFoundException { - mService = ITextServicesManager.Stub.asInterface( - ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE)); - } + private static final TextServicesManager sInstance = new TextServicesManager(); + private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0]; /** * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist. * @hide */ public static TextServicesManager getInstance() { - synchronized (TextServicesManager.class) { - if (sInstance == null) { - try { - sInstance = new TextServicesManager(); - } catch (ServiceNotFoundException e) { - throw new IllegalStateException(e); - } - } - return sInstance; - } - } - - /** - * Returns the language component of a given locale string. - */ - private static String parseLanguageFromLocaleString(String locale) { - final int idx = locale.indexOf('_'); - if (idx < 0) { - return locale; - } else { - return locale.substring(0, idx); - } + return sInstance; } - /** - * Get a spell checker session for the specified spell checker - * @param locale the locale for the spell checker. If {@code locale} is null and - * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be - * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true, - * the locale specified in Settings will be returned only when it is same as {@code locale}. - * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is - * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be - * selected. - * @param listener a spell checker session lister for getting results from a spell checker. - * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled - * languages in settings will be returned. - * @return the spell checker session of the spell checker - */ public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale, SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) { - if (listener == null) { - throw new NullPointerException(); - } - if (!referToSpellCheckerLanguageSettings && locale == null) { - throw new IllegalArgumentException("Locale should not be null if you don't refer" - + " settings."); - } - - if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) { - return null; - } - - final SpellCheckerInfo sci; - try { - sci = mService.getCurrentSpellChecker(null); - } catch (RemoteException e) { - return null; - } - if (sci == null) { - return null; - } - SpellCheckerSubtype subtypeInUse = null; - if (referToSpellCheckerLanguageSettings) { - subtypeInUse = getCurrentSpellCheckerSubtype(true); - if (subtypeInUse == null) { - return null; - } - if (locale != null) { - final String subtypeLocale = subtypeInUse.getLocale(); - final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale); - if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) { - return null; - } - } - } else { - final String localeStr = locale.toString(); - for (int i = 0; i < sci.getSubtypeCount(); ++i) { - final SpellCheckerSubtype subtype = sci.getSubtypeAt(i); - final String tempSubtypeLocale = subtype.getLocale(); - final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale); - if (tempSubtypeLocale.equals(localeStr)) { - subtypeInUse = subtype; - break; - } else if (tempSubtypeLanguage.length() >= 2 && - locale.getLanguage().equals(tempSubtypeLanguage)) { - subtypeInUse = subtype; - } - } - } - if (subtypeInUse == null) { - return null; - } - final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener); - try { - mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(), - session.getTextServicesSessionListener(), - session.getSpellCheckerSessionListener(), bundle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - return session; + return null; } /** * @hide */ public SpellCheckerInfo[] getEnabledSpellCheckers() { - try { - final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers(); - if (DBG) { - Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null")); - } - return retval; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return EMPTY_SPELL_CHECKER_INFO; } /** * @hide */ public SpellCheckerInfo getCurrentSpellChecker() { - try { - // Passing null as a locale for ICS - return mService.getCurrentSpellChecker(null); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return null; } /** @@ -215,22 +60,13 @@ public final class TextServicesManager { */ public SpellCheckerSubtype getCurrentSpellCheckerSubtype( boolean allowImplicitlySelectedSubtype) { - try { - // Passing null as a locale until we support multiple enabled spell checker subtypes. - return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return null; } /** * @hide */ public boolean isSpellCheckerEnabled() { - try { - return mService.isSpellCheckerEnabled(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return false; } } diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java index dfc81b2b..202f2046 100644 --- a/android/webkit/WebView.java +++ b/android/webkit/WebView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006 The Android Open Source Project + * Copyright (C) 2008 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. @@ -16,3001 +16,223 @@ package android.webkit; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SystemApi; -import android.annotation.Widget; +import com.android.layoutlib.bridge.MockView; + import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -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.Uri; -import android.net.http.SslCertificate; -import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.os.Message; -import android.os.RemoteException; -import android.os.StrictMode; -import android.print.PrintDocumentAdapter; -import android.security.KeyChain; import android.util.AttributeSet; -import android.util.Log; -import android.util.SparseArray; -import android.view.DragEvent; -import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewDebug; -import android.view.ViewGroup; -import android.view.ViewHierarchyEncoder; -import android.view.ViewStructure; -import android.view.ViewTreeObserver; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeProvider; -import android.view.autofill.AutofillValue; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.view.textclassifier.TextClassifier; -import android.widget.AbsoluteLayout; - -import java.io.BufferedWriter; -import java.io.File; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; -import java.util.Map; /** - * <p>A View that displays web pages. This class is the basis upon which you - * 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>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: - * - * <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>For more information, read - * <a href="{@docRoot}guide/webapps/webview.html">Building Web Apps in WebView</a>. - * - * <h3>Basic usage</h3> - * - * <p>By default, a WebView provides no browser-like widgets, does not - * enable JavaScript and web page errors are ignored. If your goal is only - * to display some HTML as a part of your UI, this is probably fine; - * the user won't need to interact with the web page beyond reading - * it, and the web page won't need to interact with the user. If you - * actually want a full-blown web browser, then you probably want to - * invoke the Browser application with a URL Intent rather than show it - * with a WebView. For example: - * <pre> - * Uri uri = Uri.parse("https://www.example.com"); - * Intent intent = new Intent(Intent.ACTION_VIEW, uri); - * startActivity(intent); - * </pre> - * <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()}: - * - * <pre class="prettyprint"> - * WebView webview = new WebView(this); - * setContentView(webview); - * </pre> - * - * <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). - * webview.loadUrl("https://example.com/"); - * - * // OR, you can also load from an HTML string: - * String summary = "<html><body>You scored <b>192</b> points.</body></html>"; - * webview.loadData(summary, "text/html", null); - * // ... although note that there are restrictions on what this HTML can do. - * // See the JavaDocs for {@link #loadData(String,String,String) loadData()} and {@link - * #loadDataWithBaseURL(String,String,String,String,String) loadDataWithBaseURL()} for more info. - * </pre> - * - * <p>A WebView has several customization points where you can add your - * own behavior. These are: - * - * <ul> - * <li>Creating and setting a {@link android.webkit.WebChromeClient} subclass. - * This class is called when something that might impact a - * browser UI happens, for instance, progress updates and - * JavaScript alerts are sent here (see <a - * href="{@docRoot}guide/developing/debug-tasks.html#DebuggingWebPages">Debugging Tasks</a>). - * </li> - * <li>Creating and setting a {@link android.webkit.WebViewClient} subclass. - * It will be called when things happen that impact the - * rendering of the content, eg, errors or form submissions. You - * can also intercept URL loading here (via {@link - * android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView,String) - * shouldOverrideUrlLoading()}).</li> - * <li>Modifying the {@link android.webkit.WebSettings}, such as - * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean) - * setJavaScriptEnabled()}. </li> - * <li>Injecting Java objects into the WebView using the - * {@link android.webkit.WebView#addJavascriptInterface} method. This - * method allows you to inject Java objects into a page's JavaScript - * context, so that they can be accessed by JavaScript in the page.</li> - * </ul> - * - * <p>Here's a more complicated example, showing error handling, - * settings, and progress notification: - * - * <pre class="prettyprint"> - * // Let's display the progress in the activity title bar, like the - * // browser app does. - * getWindow().requestFeature(Window.FEATURE_PROGRESS); - * - * webview.getSettings().setJavaScriptEnabled(true); - * - * final Activity activity = this; - * webview.setWebChromeClient(new WebChromeClient() { - * public void onProgressChanged(WebView view, int progress) { - * // Activities and WebViews measure progress with different scales. - * // The progress meter will automatically disappear when we reach 100% - * activity.setProgress(progress * 1000); - * } - * }); - * webview.setWebViewClient(new WebViewClient() { - * public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - * Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show(); - * } - * }); - * - * webview.loadUrl("https://developer.android.com/"); - * </pre> - * - * <h3>Zoom</h3> - * - * <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>NOTE: 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. - * - * <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>By default, requests by the HTML to open new windows are - * 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>The standard behavior for an Activity is to be destroyed and - * recreated when the device orientation or any other configuration changes. This will cause - * the WebView to reload the current page. If you don't want that, you - * can set your Activity to handle the {@code orientation} and {@code keyboardHidden} - * 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. - * - * - * <h3>Building web pages to support different screen densities</h3> - * - * <p>The screen density of a device is based on the screen resolution. A screen with low density - * has fewer available pixels per inch, where a screen with high density - * has more — sometimes significantly more — pixels per inch. The density of a - * screen is important because, other things being equal, a UI element (such as a button) whose - * 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>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>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 - * window.devicePixelRatio} is "1.0", then the device is considered a medium density (mdpi) device - * and default scaling is not applied to the web page; if the value is "1.5", then the device is - * considered a high density device (hdpi) and the page content is scaled 1.5x; if the - * value is "0.75", then the device is considered a low density device (ldpi) and the content is - * scaled 0.75x.</li> - * <li>The {@code -webkit-device-pixel-ratio} CSS media query. Use this to specify the screen - * densities for which this style sheet is to be used. The corresponding value should be either - * "0.75", "1", or "1.5", to indicate that the styles are for devices with low density, medium - * density, or high density screens, respectively. For example: - * <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. - * </li> - * </ul> - * - * <h3>HTML5 Video support</h3> - * - * <p>In order to support inline HTML5 video in your application you need to have hardware - * acceleration turned on. - * - * <h3>Full screen support</h3> - * - * <p>In order to support full screen — for video or other HTML content — you need to set a - * {@link android.webkit.WebChromeClient} and implement both - * {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)} - * and {@link WebChromeClient#onHideCustomView()}. If the implementation of either of these two methods is - * 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. - * - * <h3>HTML5 Geolocation API support</h3> - * - * <p>For applications targeting Android N and later releases - * (API level > {@link android.os.Build.VERSION_CODES#M}) the geolocation api is only supported on - * secure origins such as https. For such applications requests to geolocation api on non-secure - * origins are automatically denied without invoking the corresponding - * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback)} - * method. - * - * <h3>Layout size</h3> - * <p> - * It is recommended to set the WebView layout height to a fixed value or to - * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} instead of using - * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}. - * When using {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} - * 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>Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} - * enables the following behaviors: - * <ul> - * <li>The HTML body layout height is set to a fixed value. This means that elements with a height - * relative to the HTML body may not be sized correctly. </li> - * <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> - * 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. - * - * <h3>Metrics</h3> - * - * <p> - * 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: - * <pre> - * <meta-data android:name="android.webkit.WebView.MetricsOptOut" - * android:value="true" /> - * </pre> - * <p> - * Data will only be uploaded for a given app if the user has consented AND the app has not opted - * out. - * - * <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> - * The recommended way for apps to enable the feature is putting the following tag in the manifest: - * <p> - * <pre> - * <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" - * android:value="true" /> - * </pre> + * Mock version of the WebView. + * Only non override public methods from the real WebView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. * */ -// Implementation notes. -// The WebView is a thin API class that delegates its public API to a backend WebViewProvider -// class instance. WebView extends {@link AbsoluteLayout} for backward compatibility reasons. -// Methods are delegated to the provider implementation: all public API methods introduced in this -// file are fully delegated, whereas public and protected methods from the View base classes are -// only delegated where a specific need exists for them to do so. -@Widget -public class WebView extends AbsoluteLayout - implements ViewTreeObserver.OnGlobalFocusChangeListener, - ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler { - - private static final String LOGTAG = "WebView"; - - // Throwing an exception for incorrect thread usage if the - // build target is JB MR2 or newer. Defaults to false, and is - // set in the WebView constructor. - private static volatile boolean sEnforceThreadChecking = false; - - /** - * Transportation object for returning WebView across thread boundaries. - */ - public class WebViewTransport { - private WebView mWebview; +public class WebView extends MockView { - /** - * Sets the WebView to the transportation object. - * - * @param webview the WebView to transport - */ - public synchronized void setWebView(WebView webview) { - mWebview = webview; - } - - /** - * Gets the WebView object. - * - * @return the transported WebView object - */ - public synchronized WebView getWebView() { - return mWebview; - } - } - - /** - * URI scheme for telephone number. - */ - public static final String SCHEME_TEL = "tel:"; /** - * URI scheme for email address. - */ - public static final String SCHEME_MAILTO = "mailto:"; - /** - * URI scheme for map address. - */ - public static final String SCHEME_GEO = "geo:0,0?q="; - - /** - * Interface to listen for find results. - */ - public interface FindListener { - /** - * Notifies the listener about progress made by a find operation. - * - * @param activeMatchOrdinal the zero-based ordinal of the currently selected match - * @param numberOfMatches how many matches have been found - * @param isDoneCounting whether the find operation has actually completed. The listener - * may be notified multiple times while the - * operation is underway, and the numberOfMatches - * value should not be considered final unless - * isDoneCounting is {@code true}. - */ - public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, - boolean isDoneCounting); - } - - /** - * Callback interface supplied to {@link #postVisualStateCallback} for receiving - * notifications about the visual state. - */ - public static abstract class VisualStateCallback { - /** - * Invoked when the visual state is ready to be drawn in the next {@link #onDraw}. - * - * @param requestId The identifier passed to {@link #postVisualStateCallback} when this - * callback was posted. - */ - public abstract void onComplete(long requestId); - } - - /** - * Interface to listen for new pictures as they change. - * - * @deprecated This interface is now obsolete. - */ - @Deprecated - public interface PictureListener { - /** - * Used to provide notification that the WebView's picture has changed. - * See {@link WebView#capturePicture} for details of the picture. - * - * @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 {@code null} Picture. - * @deprecated Deprecated due to internal changes. - */ - @Deprecated - void onNewPicture(WebView view, @Nullable Picture picture); - } - - public static class HitTestResult { - /** - * Default HitTestResult, where the target is unknown. - */ - public static final int UNKNOWN_TYPE = 0; - /** - * @deprecated This type is no longer used. - */ - @Deprecated - public static final int ANCHOR_TYPE = 1; - /** - * HitTestResult for hitting a phone number. - */ - public static final int PHONE_TYPE = 2; - /** - * HitTestResult for hitting a map address. - */ - public static final int GEO_TYPE = 3; - /** - * HitTestResult for hitting an email address. - */ - public static final int EMAIL_TYPE = 4; - /** - * HitTestResult for hitting an HTML::img tag. - */ - public static final int IMAGE_TYPE = 5; - /** - * @deprecated This type is no longer used. - */ - @Deprecated - public static final int IMAGE_ANCHOR_TYPE = 6; - /** - * HitTestResult for hitting a HTML::a tag with src=http. - */ - public static final int SRC_ANCHOR_TYPE = 7; - /** - * HitTestResult for hitting a HTML::a tag with src=http + HTML::img. - */ - public static final int SRC_IMAGE_ANCHOR_TYPE = 8; - /** - * HitTestResult for hitting an edit text area. - */ - public static final int EDIT_TEXT_TYPE = 9; - - private int mType; - private String mExtra; - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public HitTestResult() { - mType = UNKNOWN_TYPE; - } - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public void setType(int type) { - mType = type; - } - - /** - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public void setExtra(String extra) { - mExtra = extra; - } - - /** - * Gets the type of the hit test result. See the XXX_TYPE constants - * defined in this class. - * - * @return the type of the hit test result - */ - public int getType() { - return mType; - } - - /** - * Gets additional type-dependant information about the result. See - * {@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. - * - * @param context a Context object used to access application assets + * Construct a new WebView with a Context object. + * @param context A Context object used to access application assets. */ public WebView(Context context) { this(context, null); } /** - * Constructs a new WebView with layout parameters. - * - * @param context a Context object used to access application assets - * @param attrs an AttributeSet passed to our parent + * Construct a new WebView with layout parameters. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. */ public WebView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.webViewStyle); } /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context a Context object used 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 - * the view. Can be 0 to not look for defaults. + * Construct a new WebView with layout parameters and a default style. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. */ - public WebView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); + public WebView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); } - - /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context a Context object used 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 - * the view. Can be 0 to not look for defaults. - * @param defStyleRes a resource identifier of a style resource that - * supplies default values for the view, used only if - * defStyleAttr is 0 or can not be found in the theme. Can be 0 - * to not look for defaults. - */ - public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - this(context, attrs, defStyleAttr, defStyleRes, null, false); - } - - /** - * Constructs a new WebView with layout parameters and a default style. - * - * @param context a Context object used 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 - * the view. Can be 0 to not look for defaults. - * @param privateBrowsing whether this WebView will be initialized in - * private mode - * - * @deprecated Private browsing is no longer supported directly via - * WebView and will be removed in a future release. Prefer using - * {@link WebSettings}, {@link WebViewDatabase}, {@link CookieManager} - * and {@link WebStorage} for fine-grained control of privacy data. - */ - @Deprecated - public WebView(Context context, AttributeSet attrs, int defStyleAttr, - boolean privateBrowsing) { - this(context, attrs, defStyleAttr, 0, null, privateBrowsing); - } - - /** - * Constructs a new WebView with layout parameters, a default style and a set - * of custom JavaScript interfaces to be added to this WebView at initialization - * 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 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 - * the view. Can be 0 to not look for defaults. - * @param javaScriptInterfaces a Map of interface names, as keys, and - * object implementing those interfaces, as - * values - * @param privateBrowsing whether this WebView will be initialized in - * private mode - * @hide This is used internally by dumprendertree, as it requires the JavaScript interfaces to - * be added synchronously, before a subsequent loadUrl call takes effect. - */ - protected WebView(Context context, AttributeSet attrs, int defStyleAttr, - Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { - this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing); - } - - /** - * @hide - */ - @SuppressWarnings("deprecation") // for super() call into deprecated base class constructor. - protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, - Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { - super(context, attrs, defStyleAttr, defStyleRes); - - // WebView is important by default, unless app developer overrode attribute. - if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) { - setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES); - } - - if (context == null) { - throw new IllegalArgumentException("Invalid context argument"); - } - sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >= - Build.VERSION_CODES.JELLY_BEAN_MR2; - checkThread(); - - ensureProviderCreated(); - mProvider.init(javaScriptInterfaces, privateBrowsing); - // Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed. - CookieSyncManager.setGetInstanceIsAllowed(); - } - - /** - * Specifies whether the horizontal scrollbar has overlay style. - * - * @deprecated This method has no effect. - * @param overlay {@code true} if horizontal scrollbar should have overlay style - */ - @Deprecated + + // START FAKE PUBLIC METHODS + public void setHorizontalScrollbarOverlay(boolean overlay) { } - /** - * Specifies whether the vertical scrollbar has overlay style. - * - * @deprecated This method has no effect. - * @param overlay {@code true} if vertical scrollbar should have overlay style - */ - @Deprecated public void setVerticalScrollbarOverlay(boolean overlay) { } - /** - * Gets whether horizontal scrollbar has overlay style. - * - * @deprecated This method is now obsolete. - * @return {@code true} - */ - @Deprecated public boolean overlayHorizontalScrollbar() { - // The old implementation defaulted to true, so return true for consistency - return true; + return false; } - /** - * Gets whether vertical scrollbar has overlay style. - * - * @deprecated This method is now obsolete. - * @return {@code false} - */ - @Deprecated public boolean overlayVerticalScrollbar() { - // The old implementation defaulted to false, so return false for consistency return false; } - /** - * Gets the visible height (in pixels) of the embedded title bar (if any). - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public int getVisibleTitleHeight() { - checkThread(); - return mProvider.getVisibleTitleHeight(); - } - - /** - * 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(); - } - - /** - * Sets the SSL certificate for the main top-level page. - * - * @deprecated Calling this function has no useful effect, and will be - * ignored in future releases. - */ - @Deprecated - public void setCertificate(SslCertificate certificate) { - checkThread(); - mProvider.setCertificate(certificate); - } - - //------------------------------------------------------------------------- - // Methods called by activity - //------------------------------------------------------------------------- - - /** - * Sets a username and password pair for the specified host. This data is - * used by the WebView to autocomplete username and password fields in web - * forms. Note that this is unrelated to the credentials used for HTTP - * authentication. - * - * @param host the host that required the credentials - * @param username the username for the given host - * @param password the password for the given host - * @see WebViewDatabase#clearUsernamePassword - * @see WebViewDatabase#hasUsernamePassword - * @deprecated Saving passwords in WebView will not be supported in future versions. - */ - @Deprecated public void savePassword(String host, String username, String password) { - checkThread(); - mProvider.savePassword(host, username, password); } - /** - * Stores HTTP authentication credentials for a given host and realm to the {@link WebViewDatabase} - * instance. - * - * @param host the host to which the credentials apply - * @param realm the realm to which the credentials apply - * @param username the username - * @param password the password - * @deprecated Use {@link WebViewDatabase#setHttpAuthUsernamePassword} instead - */ - @Deprecated public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { - checkThread(); - mProvider.setHttpAuthUsernamePassword(host, realm, username, password); } - /** - * Retrieves HTTP authentication credentials for a given host and realm from the {@link - * WebViewDatabase} instance. - * @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. {@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); + return null; } - /** - * Destroys the internal state of this WebView. This method should be called - * after this WebView has been removed from the view system. No other - * methods may be called on this WebView after destroy. - */ public void destroy() { - checkThread(); - mProvider.destroy(); } - /** - * Enables platform notifications of data state and proxy changes. - * Notifications are enabled by default. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated public static void enablePlatformNotifications() { - // noop } - /** - * Disables platform notifications of data state and proxy changes. - * Notifications are enabled by default. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated public static void disablePlatformNotifications() { - // noop } - /** - * Used only by internal tests to free up memory. - * - * @hide - */ - public static void freeMemoryForTests() { - getFactory().getStatics().freeMemoryForTests(); - } - - /** - * Informs WebView of the network state. This is used to set - * the JavaScript property window.navigator.isOnline and - * generates the online/offline event as specified in HTML5, sec. 5.7.7 - * - * @param networkUp a boolean indicating if network is available - */ - public void setNetworkAvailable(boolean networkUp) { - checkThread(); - mProvider.setNetworkAvailable(networkUp); - } - - /** - * Saves the state of this WebView used in - * {@link android.app.Activity#onSaveInstanceState}. Please note that this - * method no longer stores the display data for this WebView. The previous - * behavior could potentially leak files if {@link #restoreState} was never - * 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, {@code null} if the - * method fails. - */ - @Nullable - public WebBackForwardList saveState(Bundle outState) { - checkThread(); - return mProvider.saveState(outState); - } - - /** - * Saves the current display data to the Bundle given. Used in conjunction - * with {@link #saveState}. - * @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 {@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} - */ - @Deprecated - public boolean savePicture(Bundle b, final File dest) { - checkThread(); - return mProvider.savePicture(b, dest); - } - - /** - * Restores the display data that was saved in {@link #savePicture}. Used in - * conjunction with {@link #restoreState}. Note that this will not work if - * this WebView is hardware accelerated. - * - * @param b a Bundle containing the saved display data - * @param src the file where the picture data was stored - * @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} - */ - @Deprecated - public boolean restorePicture(Bundle b, File src) { - checkThread(); - return mProvider.restorePicture(b, src); - } - - /** - * Restores the state of this WebView from the given Bundle. This method is - * intended for use in {@link android.app.Activity#onRestoreInstanceState} - * and should be called to restore the state of this WebView. If - * it is called after this WebView has had a chance to build state (load - * pages, create a back/forward list, etc.) there may be undesirable - * side-effects. Please note that this method no longer restores the - * display data for this WebView. - * - * @param inState the incoming Bundle of state - * @return the restored back/forward list or {@code null} if restoreState failed - */ - @Nullable - public WebBackForwardList restoreState(Bundle inState) { - checkThread(); - return mProvider.restoreState(inState); - } - - /** - * Loads the given URL with the specified additional HTTP headers. - * <p> - * Also see compatibility note on {@link #evaluateJavascript}. - * - * @param url the URL of the resource to load - * @param additionalHttpHeaders the additional headers to be used in the - * HTTP request for this URL, specified as a map from name to - * value. Note that if this map contains any of the headers - * that are set by default by this WebView, such as those - * controlling caching, accept types or the User-Agent, their - * values may be overridden by this WebView's defaults. - */ - public void loadUrl(String url, Map<String, String> additionalHttpHeaders) { - checkThread(); - mProvider.loadUrl(url, additionalHttpHeaders); - } - - /** - * Loads the given URL. - * <p> - * Also see compatibility note on {@link #evaluateJavascript}. - * - * @param url the URL of the resource to load - */ public void loadUrl(String url) { - checkThread(); - mProvider.loadUrl(url); - } - - /** - * Loads the URL with postData using "POST" method into this WebView. If url - * is not a network URL, it will be loaded with {@link #loadUrl(String)} - * instead, ignoring the postData param. - * - * @param url the URL of the resource to load - * @param postData the data will be passed to "POST" request, which must be - * be "application/x-www-form-urlencoded" encoded. - */ - public void postUrl(String url, byte[] postData) { - checkThread(); - if (URLUtil.isNetworkUrl(url)) { - mProvider.postUrl(url, postData); - } else { - mProvider.loadUrl(url); - } } - /** - * Loads the given data into this WebView using a 'data' scheme URL. - * <p> - * Note that JavaScript's same origin policy means that script running in a - * page loaded using this method will be unable to access content loaded - * using any scheme other than 'data', including 'http(s)'. To avoid this - * restriction, use {@link - * #loadDataWithBaseURL(String,String,String,String,String) - * loadDataWithBaseURL()} with an appropriate base URL. - * <p> - * 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 {@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. - * <p> - * The 'data' scheme URL formed by this method uses the default US-ASCII - * charset. If you need need to set a different charset, you should form a - * 'data' scheme URL which explicitly specifies a charset parameter in the - * mediatype portion of the URL and call {@link #loadUrl(String)} instead. - * Note that the charset obtained from the mediatype portion of a data URL - * always overrides that specified in the HTML or XML document itself. - * - * @param data a String of data in the given encoding - * @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, @Nullable String mimeType, @Nullable String encoding) { - checkThread(); - mProvider.loadData(data, mimeType, encoding); + public void loadData(String data, String mimeType, String encoding) { } - /** - * Loads the given data into this WebView, using baseUrl as the base URL for - * the content. The base URL is used both to resolve relative URLs and when - * applying JavaScript's same origin policy. The historyUrl is used for the - * history entry. - * <p> - * Note that content specified in this way can access local device files - * (via 'file' scheme URLs) only if baseUrl specifies a scheme other than - * 'http', 'https', 'ftp', 'ftps', 'about' or 'javascript'. - * <p> - * If the base URL uses the data scheme, this method is equivalent to - * calling {@link #loadData(String,String,String) loadData()} and the - * historyUrl is ignored, and the data will be treated as part of a data: URL. - * If the base URL uses any other scheme, then the data will be loaded into - * the WebView as a plain string (i.e. not part of a data URL) and any URL-encoded - * entities in the string will not be decoded. - * <p> - * 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 {@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 {@code null}, - * defaults to 'text/html'. - * @param encoding the encoding of the data - * @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(@Nullable String baseUrl, String data, - @Nullable String mimeType, @Nullable String encoding, @Nullable String historyUrl) { - checkThread(); - mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + public void loadDataWithBaseURL(String baseUrl, String data, + String mimeType, String encoding, String failUrl) { } - /** - * Asynchronously evaluates JavaScript in the context of the currently displayed page. - * If non-null, |resultCallback| will be invoked with any result returned from that - * execution. This method must be called on the UI thread and the callback will - * be made on the UI thread. - * <p> - * Compatibility note. Applications targeting {@link android.os.Build.VERSION_CODES#N} or - * later, JavaScript state from an empty WebView is no longer persisted across navigations like - * {@link #loadUrl(String)}. For example, global variables and functions defined before calling - * {@link #loadUrl(String)} will not exist in the loaded page. Applications should use - * {@link #addJavascriptInterface} instead to persist JavaScript objects across navigations. - * - * @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 {@code null} if no notification of the result is required. - */ - public void evaluateJavascript(String script, @Nullable ValueCallback<String> resultCallback) { - checkThread(); - mProvider.evaluateJavaScript(script, resultCallback); - } - - /** - * Saves the current view as a web archive. - * - * @param filename the filename where the archive should be placed - */ - public void saveWebArchive(String filename) { - checkThread(); - mProvider.saveWebArchive(filename); - } - - /** - * Saves the current view as a web archive. - * - * @param basename the filename where the archive should be placed - * @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 {@code null} if saving the - * file failed. - */ - public void saveWebArchive(String basename, boolean autoname, @Nullable ValueCallback<String> - callback) { - checkThread(); - mProvider.saveWebArchive(basename, autoname, callback); - } - - /** - * Stops the current load. - */ public void stopLoading() { - checkThread(); - mProvider.stopLoading(); } - /** - * Reloads the current URL. - */ public void reload() { - checkThread(); - mProvider.reload(); } - /** - * Gets whether this WebView has a back history item. - * - * @return {@code true} iff this WebView has a back history item - */ public boolean canGoBack() { - checkThread(); - return mProvider.canGoBack(); + return false; } - /** - * Goes back in the history of this WebView. - */ public void goBack() { - checkThread(); - mProvider.goBack(); } - /** - * Gets whether this WebView has a forward history item. - * - * @return {@code true} iff this WebView has a forward history item - */ public boolean canGoForward() { - checkThread(); - return mProvider.canGoForward(); + return false; } - /** - * Goes forward in the history of this WebView. - */ public void goForward() { - checkThread(); - mProvider.goForward(); } - /** - * Gets whether the page can go back or forward the given - * number of steps. - * - * @param steps the negative or positive number of steps to move the - * history - */ public boolean canGoBackOrForward(int steps) { - checkThread(); - return mProvider.canGoBackOrForward(steps); + return false; } - /** - * Goes to the history item that is the number of steps away from - * the current item. Steps is negative if backward and positive - * if forward. - * - * @param steps the number of steps to take back or forward in the back - * forward list - */ public void goBackOrForward(int steps) { - checkThread(); - mProvider.goBackOrForward(steps); - } - - /** - * Gets whether private browsing is enabled in this WebView. - */ - public boolean isPrivateBrowsingEnabled() { - checkThread(); - return mProvider.isPrivateBrowsingEnabled(); } - /** - * Scrolls the contents of this WebView up by half the view size. - * - * @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(); - return mProvider.pageUp(top); + return false; } - - /** - * Scrolls the contents of this WebView down by half the page size. - * - * @param bottom {@code true} to jump to bottom of page - * @return {@code true} if the page was scrolled - */ + public boolean pageDown(boolean bottom) { - checkThread(); - return mProvider.pageDown(bottom); - } - - /** - * Posts a {@link VisualStateCallback}, which will be called when - * the current state of the WebView is ready to be drawn. - * - * <p>Because updates to the DOM are processed asynchronously, updates to the DOM may not - * 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>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>The state of the DOM covered by this API includes the following: - * <ul> - * <li>primitive HTML elements (div, img, span, etc..)</li> - * <li>images</li> - * <li>CSS animations</li> - * <li>WebGL</li> - * <li>canvas</li> - * </ul> - * It does not include the state of: - * <ul> - * <li>the video tag</li> - * </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 - * must be met: - * <ul> - * <li>If the {@link WebView}'s visibility is set to {@link View#VISIBLE VISIBLE} then - * the {@link WebView} must be attached to the view hierarchy.</li> - * <li>If the {@link WebView}'s visibility is set to {@link View#INVISIBLE INVISIBLE} - * then the {@link WebView} must be attached to the view hierarchy and must be made - * {@link View#VISIBLE VISIBLE} from the {@link VisualStateCallback#onComplete} method.</li> - * <li>If the {@link WebView}'s visibility is set to {@link View#GONE GONE} then the - * {@link WebView} must be attached to the view hierarchy and its - * {@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>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. - * - * @param requestId An id that will be returned in the callback to allow callers to match - * requests with callbacks. - * @param callback The callback to be invoked. - */ - public void postVisualStateCallback(long requestId, VisualStateCallback callback) { - checkThread(); - mProvider.insertVisualStateCallback(requestId, callback); + return false; } - /** - * Clears this WebView so that onDraw() will draw nothing but white background, - * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY. - * @deprecated Use WebView.loadUrl("about:blank") to reliably reset the view state - * and release page resources (including any running JavaScript). - */ - @Deprecated public void clearView() { - checkThread(); - mProvider.clearView(); } - - /** - * Gets a new picture that captures the current contents of this WebView. - * The picture is of the entire document being displayed, and is not - * limited to the area currently displayed by this WebView. Also, the - * picture is a static copy and is unaffected by later changes to the - * content being displayed. - * <p> - * Note that due to internal changes, for API levels between - * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and - * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} inclusive, the - * picture does not include fixed position elements or scrollable divs. - * <p> - * Note that from {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} the returned picture - * should only be drawn into bitmap-backed Canvas - using any other type of Canvas will involve - * additional conversion at a cost in memory and performance. Also the - * {@link android.graphics.Picture#createFromStream} and - * {@link android.graphics.Picture#writeToStream} methods are not supported on the - * returned object. - * - * @deprecated Use {@link #onDraw} to obtain a bitmap snapshot of the WebView, or - * {@link #saveWebArchive} to save the content to a file. - * - * @return a picture that captures the current contents of this WebView - */ - @Deprecated + public Picture capturePicture() { - checkThread(); - return mProvider.capturePicture(); - } - - /** - * @deprecated Use {@link #createPrintDocumentAdapter(String)} which requires user - * to provide a print document name. - */ - @Deprecated - public PrintDocumentAdapter createPrintDocumentAdapter() { - checkThread(); - return mProvider.createPrintDocumentAdapter("default"); + return null; } - /** - * Creates a PrintDocumentAdapter that provides the content of this WebView for printing. - * - * The adapter works by converting the WebView contents to a PDF stream. The WebView cannot - * be drawn during the conversion process - any such draws are undefined. It is recommended - * to use a dedicated off screen WebView for the printing. If necessary, an application may - * temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance - * wrapped around the object returned and observing the onStart and onFinish methods. See - * {@link android.print.PrintDocumentAdapter} for more information. - * - * @param documentName The user-facing name of the printed document. See - * {@link android.print.PrintDocumentInfo} - */ - public PrintDocumentAdapter createPrintDocumentAdapter(String documentName) { - checkThread(); - return mProvider.createPrintDocumentAdapter(documentName); - } - - /** - * Gets the current scale of this WebView. - * - * @return the current scale - * - * @deprecated This method is prone to inaccuracy due to race conditions - * between the web rendering and UI threads; prefer - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - @ViewDebug.ExportedProperty(category = "webview") public float getScale() { - checkThread(); - return mProvider.getScale(); + return 0; } - /** - * Sets the initial scale for this WebView. 0 means default. - * The behavior for the default scale depends on the state of - * {@link WebSettings#getUseWideViewPort()} and - * {@link WebSettings#getLoadWithOverviewMode()}. - * 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 {@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 - * as initial scale. - * Please note that unlike the scale properties in the viewport meta tag, - * this method doesn't take the screen density into account. - * - * @param scaleInPercent the initial scale in percent - */ public void setInitialScale(int scaleInPercent) { - checkThread(); - mProvider.setInitialScale(scaleInPercent); } - /** - * Invokes the graphical zoom picker widget for this WebView. This will - * result in the zoom widget appearing on the screen to control the zoom - * level of this WebView. - */ public void invokeZoomPicker() { - checkThread(); - mProvider.invokeZoomPicker(); - } - - /** - * Gets a HitTestResult based on the current cursor node. If a HTML::a - * tag is found and the anchor has a non-JavaScript URL, the HitTestResult - * type is set to SRC_ANCHOR_TYPE and the URL is set in the "extra" field. - * If the anchor does not have a URL or if it is a JavaScript URL, the type - * will be UNKNOWN_TYPE and the URL has to be retrieved through - * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is - * found, the HitTestResult type is set to IMAGE_TYPE and the URL is set in - * the "extra" field. A type of - * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a URL that has an image as - * a child node. If a phone number is found, the HitTestResult type is set - * to PHONE_TYPE and the phone number is set in the "extra" field of - * HitTestResult. If a map address is found, the HitTestResult type is set - * to GEO_TYPE and the address is set in the "extra" field of HitTestResult. - * If an email address is found, the HitTestResult type is set to EMAIL_TYPE - * and the email is set in the "extra" field of HitTestResult. Otherwise, - * HitTestResult type is set to UNKNOWN_TYPE. - */ - public HitTestResult getHitTestResult() { - checkThread(); - return mProvider.getHitTestResult(); } - /** - * Requests the anchor or image element URL at the last tapped point. - * 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 - * may be an empty string. - * - * @param hrefMsg the message to be dispatched with the result of the - * request. The message data contains three keys. "url" - * returns the anchor's href attribute. "title" returns the - * anchor's text. "src" returns the image's src attribute. - */ - public void requestFocusNodeHref(@Nullable Message hrefMsg) { - checkThread(); - mProvider.requestFocusNodeHref(hrefMsg); + public void requestFocusNodeHref(Message hrefMsg) { } - /** - * Requests the URL of the image last touched by the user. msg will be sent - * 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 {@code null}. - */ public void requestImageRef(Message msg) { - checkThread(); - mProvider.requestImageRef(msg); } - /** - * Gets the URL for the current page. This is not always the same as the URL - * passed to WebViewClient.onPageStarted because although the load for - * that URL has begun, the current page may not have changed. - * - * @return the URL for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") public String getUrl() { - checkThread(); - return mProvider.getUrl(); + return null; } - /** - * Gets the original URL for the current page. This is not always the same - * as the URL passed to WebViewClient.onPageStarted because although the - * load for that URL has begun, the current page may not have changed. - * Also, there may have been redirects resulting in a different URL to that - * originally requested. - * - * @return the URL that was originally requested for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") - public String getOriginalUrl() { - checkThread(); - return mProvider.getOriginalUrl(); - } - - /** - * Gets the title for the current page. This is the title of the current page - * until WebViewClient.onReceivedTitle is called. - * - * @return the title for the current page - */ - @ViewDebug.ExportedProperty(category = "webview") public String getTitle() { - checkThread(); - return mProvider.getTitle(); + return null; } - /** - * Gets the favicon for the current page. This is the favicon of the current - * page until WebViewClient.onReceivedIcon is called. - * - * @return the favicon for the current page - */ public Bitmap getFavicon() { - checkThread(); - return mProvider.getFavicon(); + return null; } - /** - * Gets the touch icon URL for the apple-touch-icon <link> element, or - * a URL on this site's server pointing to the standard location of a - * touch icon. - * - * @hide - */ - public String getTouchIconUrl() { - return mProvider.getTouchIconUrl(); - } - - /** - * Gets the progress for the current page. - * - * @return the progress for the current page between 0 and 100 - */ public int getProgress() { - checkThread(); - return mProvider.getProgress(); + return 0; } - - /** - * Gets the height of the HTML content. - * - * @return the height of the HTML content - */ - @ViewDebug.ExportedProperty(category = "webview") + public int getContentHeight() { - checkThread(); - return mProvider.getContentHeight(); - } - - /** - * Gets the width of the HTML content. - * - * @return the width of the HTML content - * @hide - */ - @ViewDebug.ExportedProperty(category = "webview") - public int getContentWidth() { - return mProvider.getContentWidth(); + return 0; } - /** - * Pauses all layout, parsing, and JavaScript timers for all WebViews. This - * is a global requests, not restricted to just this WebView. This can be - * useful if the application has been paused. - */ public void pauseTimers() { - checkThread(); - mProvider.pauseTimers(); } - /** - * Resumes all layout, parsing, and JavaScript timers for all WebViews. - * This will resume dispatching all timers. - */ public void resumeTimers() { - checkThread(); - mProvider.resumeTimers(); - } - - /** - * Does a best-effort attempt to pause any processing that can be paused - * safely, such as animations and geolocation. Note that this call - * does not pause JavaScript. To pause JavaScript globally, use - * {@link #pauseTimers}. - * - * To resume WebView, call {@link #onResume}. - */ - public void onPause() { - checkThread(); - mProvider.onPause(); - } - - /** - * Resumes a WebView after a previous call to {@link #onPause}. - */ - public void onResume() { - checkThread(); - mProvider.onResume(); - } - - /** - * Gets whether this WebView is paused, meaning onPause() was called. - * Calling onResume() sets the paused state back to {@code false}. - * - * @hide - */ - public boolean isPaused() { - return mProvider.isPaused(); - } - - /** - * Informs this WebView that memory is low so that it can free any available - * memory. - * @deprecated Memory caches are automatically dropped when no longer needed, and in response - * to system memory pressure. - */ - @Deprecated - public void freeMemory() { - checkThread(); - mProvider.freeMemory(); } - /** - * Clears the resource cache. Note that the cache is per-application, so - * this will clear the cache for all WebViews used. - * - * @param includeDiskFiles if {@code false}, only the RAM cache is cleared - */ - public void clearCache(boolean includeDiskFiles) { - checkThread(); - mProvider.clearCache(includeDiskFiles); + public void clearCache() { } - /** - * Removes the autocomplete popup from the currently focused form field, if - * present. Note this only affects the display of the autocomplete popup, - * it does not remove any saved form data from this WebView's store. To do - * that, use {@link WebViewDatabase#clearFormData}. - */ public void clearFormData() { - checkThread(); - mProvider.clearFormData(); } - /** - * Tells this WebView to clear its internal back/forward list. - */ public void clearHistory() { - checkThread(); - mProvider.clearHistory(); } - /** - * Clears the SSL preferences table stored in response to proceeding with - * SSL certificate errors. - */ public void clearSslPreferences() { - checkThread(); - mProvider.clearSslPreferences(); - } - - /** - * Clears the client certificate preferences stored in response - * to proceeding/cancelling client cert requests. Note that WebView - * automatically clears these preferences when it receives a - * {@link KeyChain#ACTION_STORAGE_CHANGED} intent. The preferences are - * 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 runnable will be called in UI thread. - */ - public static void clearClientCertPreferences(@Nullable Runnable onCleared) { - getFactory().getStatics().clearClientCertPreferences(onCleared); - } - - /** - * Starts Safe Browsing initialization. - * <p> - * URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is - * invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those - * devices {@code callback} will receive {@code false}. - * <p> - * This does not enable the Safe Browsing feature itself, and should only be called if Safe - * Browsing is enabled by the manifest tag or {@link WebSettings#setSafeBrowsingEnabled}. This - * prepares resources used for Safe Browsing. - * <p> - * This should be called with the Application Context (and will always use the Application - * context to do its work regardless). - * - * @param context Application Context. - * @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, - @Nullable ValueCallback<Boolean> callback) { - getFactory().getStatics().initSafeBrowsing(context, callback); - } - - /** - * Sets the list of domains that are exempt from SafeBrowsing checks. The list is - * global for all the WebViews. - * <p> - * Each rule should take one of these: - * <table> - * <tr><th> Rule </th> <th> Example </th> <th> Matches Subdomain</th> </tr> - * <tr><td> HOSTNAME </td> <td> example.com </td> <td> Yes </td> </tr> - * <tr><td> .HOSTNAME </td> <td> .example.com </td> <td> No </td> </tr> - * <tr><td> IPV4_LITERAL </td> <td> 192.168.1.1 </td> <td> No </td></tr> - * <tr><td> IPV6_LITERAL_WITH_BRACKETS </td><td>[10:20:30:40:50:60:70:80]</td><td>No</td></tr> - * </table> - * <p> - * All other rules, including wildcards, are invalid. - * - * @param urls the list of URLs - * @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) { - getFactory().getStatics().setSafeBrowsingWhitelist(urls, callback); - } - - /** - * Returns a URL pointing to the privacy policy for Safe Browsing reporting. - * - * @return the url pointing to a privacy policy document which can be displayed to users. - */ - @NonNull - public static Uri getSafeBrowsingPrivacyPolicyUrl() { - return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl(); - } - - /** - * Gets the WebBackForwardList for this WebView. This contains the - * back/forward list for use in querying each item in the history stack. - * This is a copy of the private WebBackForwardList so it contains only a - * snapshot of the current state. Multiple calls to this method may return - * different objects. The object returned from this method will not be - * updated to reflect any new state. - */ - public WebBackForwardList copyBackForwardList() { - checkThread(); - return mProvider.copyBackForwardList(); - - } - - /** - * Registers the listener to be notified as find-on-page operations - * progress. This will replace the current listener. - * - * @param listener an implementation of {@link FindListener} - */ - public void setFindListener(FindListener listener) { - checkThread(); - setupFindListenerIfNeeded(); - mFindListener.mUserFindListener = listener; - } - - /** - * Highlights and scrolls to the next match found by - * {@link #findAllAsync}, wrapping around page boundaries as necessary. - * Notifies any registered {@link FindListener}. If {@link #findAllAsync(String)} - * has not been called yet, or if {@link #clearMatches} has been called since the - * last find operation, this function does nothing. - * - * @param forward the direction to search - * @see #setFindListener - */ - public void findNext(boolean forward) { - checkThread(); - mProvider.findNext(forward); - } - - /** - * Finds all instances of find on the page and highlights them. - * Notifies any registered {@link FindListener}. - * - * @param find the string to find - * @return the number of occurrences of the String "find" that were found - * @deprecated {@link #findAllAsync} is preferred. - * @see #setFindListener - */ - @Deprecated - public int findAll(String find) { - checkThread(); - StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync"); - return mProvider.findAll(find); - } - - /** - * Finds all instances of find on the page and highlights them, - * asynchronously. Notifies any registered {@link FindListener}. - * Successive calls to this will cancel any pending searches. - * - * @param find the string to find. - * @see #setFindListener - */ - public void findAllAsync(String find) { - checkThread(); - mProvider.findAllAsync(find); - } - - /** - * Starts an ActionMode for finding text in this WebView. Only works if this - * WebView is attached to the view system. - * - * @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 {@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(@Nullable String text, boolean showIme) { - checkThread(); - return mProvider.showFindDialog(text, showIme); } - /** - * Gets the first substring consisting of the address of a physical - * location. Currently, only addresses in the United States are detected, - * and consist of: - * <ul> - * <li>a house number</li> - * <li>a street name</li> - * <li>a street type (Road, Circle, etc), either spelled out or - * abbreviated</li> - * <li>a city name</li> - * <li>a state or territory, either spelled out or two-letter abbr</li> - * <li>an optional 5 digit or 9 digit zip code</li> - * </ul> - * All names must be correctly capitalized, and the zip code, if present, - * must be valid for the state. The street type must be a standard USPS - * spelling or abbreviation. The state or territory must also be spelled - * or abbreviated using USPS standards. The house number may not exceed - * five digits. - * - * @param addr the string to search for addresses - * @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 - return getFactory().getStatics().findAddress(addr); - } - - /** - * For apps targeting the L release, WebView has a new default behavior that reduces - * memory footprint and increases performance by intelligently choosing - * the portion of the HTML document that needs to be drawn. These - * optimizations are transparent to the developers. However, under certain - * circumstances, an App developer may want to disable them: - * <ol> - * <li>When an app uses {@link #onDraw} to do own drawing and accesses portions - * of the page that is way outside the visible portion of the page.</li> - * <li>When an app uses {@link #capturePicture} to capture a very large HTML document. - * Note that capturePicture is a deprecated API.</li> - * </ol> - * Enabling drawing the entire HTML document has a significant performance - * cost. This method should be called before any WebViews are created. - */ - public static void enableSlowWholeDocumentDraw() { - getFactory().getStatics().enableSlowWholeDocumentDraw(); + return null; } - /** - * Clears the highlighting surrounding text matches created by - * {@link #findAllAsync}. - */ - public void clearMatches() { - checkThread(); - mProvider.clearMatches(); - } - - /** - * Queries the document to see if it contains any image references. The - * message object will be dispatched with arg1 being set to 1 if images - * were found and 0 if the document does not reference any images. - * - * @param response the message that will be dispatched with the result - */ public void documentHasImages(Message response) { - checkThread(); - mProvider.documentHasImages(response); } - /** - * Sets the WebViewClient that will receive various notifications and - * requests. This will replace the current handler. - * - * @param client an implementation of WebViewClient - * @see #getWebViewClient - */ public void setWebViewClient(WebViewClient client) { - checkThread(); - mProvider.setWebViewClient(client); - } - - /** - * Gets the WebViewClient. - * - * @return the WebViewClient, or a default client if not yet set - * @see #setWebViewClient - */ - public WebViewClient getWebViewClient() { - checkThread(); - return mProvider.getWebViewClient(); } - /** - * Registers the interface to be used when content can not be handled by - * the rendering engine, and should be downloaded instead. This will replace - * the current handler. - * - * @param listener an implementation of DownloadListener - */ public void setDownloadListener(DownloadListener listener) { - checkThread(); - mProvider.setDownloadListener(listener); } - /** - * Sets the chrome handler. This is an implementation of WebChromeClient for - * use in handling JavaScript dialogs, favicons, titles, and the progress. - * This will replace the current handler. - * - * @param client an implementation of WebChromeClient - * @see #getWebChromeClient - */ public void setWebChromeClient(WebChromeClient client) { - checkThread(); - mProvider.setWebChromeClient(client); } - /** - * Gets the chrome handler. - * - * @return the WebChromeClient, or {@code null} if not yet set - * @see #setWebChromeClient - */ - @Nullable - public WebChromeClient getWebChromeClient() { - checkThread(); - return mProvider.getWebChromeClient(); - } - - /** - * Sets the Picture listener. This is an interface used to receive - * notifications of a new Picture. - * - * @param listener an implementation of WebView.PictureListener - * @deprecated This method is now obsolete. - */ - @Deprecated - public void setPictureListener(PictureListener listener) { - checkThread(); - mProvider.setPictureListener(listener); - } - - /** - * Injects the supplied Java object into this WebView. The object is - * injected into the JavaScript context of the main frame, using the - * supplied name. This allows the Java object's methods to be - * accessed from JavaScript. For applications targeted to API - * level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - * and above, only public methods that are annotated with - * {@link android.webkit.JavascriptInterface} can be accessed from JavaScript. - * For applications targeted to API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or below, - * all public methods (including the inherited ones) can be accessed, see the - * important security note below for implications. - * <p> Note that injected objects will not appear in JavaScript until the page is next - * (re)loaded. JavaScript should be enabled before injecting the object. For example: - * <pre> - * class JsObject { - * {@literal @}JavascriptInterface - * public String toString() { return "injectedObject"; } - * } - * webview.getSettings().setJavaScriptEnabled(true); - * webView.addJavascriptInterface(new JsObject(), "injectedObject"); - * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null); - * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre> - * <p> - * <strong>IMPORTANT:</strong> - * <ul> - * <li> This method can be used to allow JavaScript to control the host - * application. This is a powerful feature, but also presents a security - * risk for apps targeting {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or earlier. - * Apps that target a version later than {@link android.os.Build.VERSION_CODES#JELLY_BEAN} - * are still vulnerable if the app runs on a device running Android earlier than 4.2. - * The most secure way to use this method is to target {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - * and to ensure the method is called only when running on Android 4.2 or later. - * With these older versions, JavaScript could use reflection to access an - * injected object's public fields. Use of this method in a WebView - * containing untrusted content could allow an attacker to manipulate the - * host application in unintended ways, executing Java code with the - * permissions of the host application. Use extreme care when using this - * method in a WebView which could contain untrusted content.</li> - * <li> JavaScript interacts with Java object on a private, background - * thread of this WebView. Care is therefore required to maintain thread - * safety. - * </li> - * <li> The Java object's fields are not accessible.</li> - * <li> For applications targeted to API level {@link android.os.Build.VERSION_CODES#LOLLIPOP} - * and above, methods of injected Java objects are enumerable from - * JavaScript.</li> - * </ul> - * - * @param object the Java object to inject into this WebView's JavaScript - * context. {@code null} values are ignored. - * @param name the name used to expose the object in JavaScript - */ - public void addJavascriptInterface(Object object, String name) { - checkThread(); - mProvider.addJavascriptInterface(object, name); - } - - /** - * Removes a previously injected Java object from this WebView. Note that - * the removal will not be reflected in JavaScript until the page is next - * (re)loaded. See {@link #addJavascriptInterface}. - * - * @param name the name used to expose the object in JavaScript - */ - public void removeJavascriptInterface(@NonNull String name) { - checkThread(); - mProvider.removeJavascriptInterface(name); - } - - /** - * Creates a message channel to communicate with JS and returns the message - * ports that represent the endpoints of this message channel. The HTML5 message - * channel functionality is described - * <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. - * - * @return the two message ports that form the message channel. - */ - public WebMessagePort[] createWebMessageChannel() { - checkThread(); - return mProvider.createWebMessageChannel(); - } - - /** - * Post a message to main frame. The embedded application can restrict the - * messages to a certain target origin. See - * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages"> - * HTML5 spec</a> for how target origin can be used. - * <p> - * A target origin can be set as a wildcard ("*"). However this is not recommended. - * See the page above for security issues. - * - * @param message the WebMessage - * @param targetOrigin the target origin. - */ - public void postWebMessage(WebMessage message, Uri targetOrigin) { - checkThread(); - mProvider.postMessageToMainFrame(message, targetOrigin); - } - - /** - * Gets the WebSettings object used to control the settings for this - * WebView. - * - * @return a WebSettings object that can be used to control this WebView's - * settings - */ - public WebSettings getSettings() { - checkThread(); - return mProvider.getSettings(); - } - - /** - * Enables debugging of web contents (HTML / CSS / JavaScript) - * loaded into any WebViews of this application. This flag can be enabled - * in order to facilitate debugging of web layouts and JavaScript - * code running inside WebViews. Please refer to WebView documentation - * for the debugging guide. - * - * The default is {@code false}. - * - * @param enabled whether to enable web contents debugging - */ - public static void setWebContentsDebuggingEnabled(boolean enabled) { - getFactory().getStatics().setWebContentsDebuggingEnabled(enabled); - } - - /** - * Gets the list of currently loaded plugins. - * - * @return the list of currently loaded plugins - * @deprecated This was used for Gears, which has been deprecated. - * @hide - */ - @Deprecated - public static synchronized PluginList getPluginList() { - return new PluginList(); - } - - /** - * @deprecated This was used for Gears, which has been deprecated. - * @hide - */ - @Deprecated - public void refreshPlugins(boolean reloadOpenPages) { - checkThread(); - } - - /** - * Puts this WebView into text selection mode. Do not rely on this - * functionality; it will be deprecated in the future. - * - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public void emulateShiftHeld() { - checkThread(); - } - - /** - * @deprecated WebView no longer needs to implement - * ViewGroup.OnHierarchyChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onChildViewAdded(View parent, View child) {} - - /** - * @deprecated WebView no longer needs to implement - * ViewGroup.OnHierarchyChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onChildViewRemoved(View p, View child) {} - - /** - * @deprecated WebView should not have implemented - * ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now. - */ - @Override - // Cannot add @hide as this can always be accessed via the interface. - @Deprecated - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - } - - /** - * @deprecated Only the default case, {@code true}, will be supported in a future version. - */ - @Deprecated - public void setMapTrackballToArrowKeys(boolean setMap) { - checkThread(); - mProvider.setMapTrackballToArrowKeys(setMap); + public void addJavascriptInterface(Object obj, String interfaceName) { } - - public void flingScroll(int vx, int vy) { - checkThread(); - mProvider.flingScroll(vx, vy); - } - - /** - * Gets the zoom controls for this WebView, as a separate View. The caller - * is responsible for inserting this View into the layout hierarchy. - * <p/> - * API level {@link android.os.Build.VERSION_CODES#CUPCAKE} introduced - * built-in zoom mechanisms for the WebView, as opposed to these separate - * zoom controls. The built-in mechanisms are preferred and can be enabled - * using {@link WebSettings#setBuiltInZoomControls}. - * - * @deprecated the built-in zoom mechanisms are preferred - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} - */ - @Deprecated public View getZoomControls() { - checkThread(); - return mProvider.getZoomControls(); + return null; } - /** - * Gets whether 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 - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - public boolean canZoomIn() { - checkThread(); - return mProvider.canZoomIn(); - } - - /** - * Gets whether 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 - * {@link WebViewClient#onScaleChanged}. - */ - @Deprecated - public boolean canZoomOut() { - checkThread(); - return mProvider.canZoomOut(); - } - - /** - * Performs a zoom operation in this WebView. - * - * @param zoomFactor the zoom factor to apply. The zoom factor will be clamped to the WebView's - * zoom limits. This value must be in the range 0.01 to 100.0 inclusive. - */ - public void zoomBy(float zoomFactor) { - checkThread(); - if (zoomFactor < 0.01) - throw new IllegalArgumentException("zoomFactor must be greater than 0.01."); - if (zoomFactor > 100.0) - throw new IllegalArgumentException("zoomFactor must be less than 100."); - mProvider.zoomBy(zoomFactor); - } - - /** - * Performs zoom in in this WebView. - * - * @return {@code true} if zoom in succeeds, {@code false} if no zoom changes - */ public boolean zoomIn() { - checkThread(); - return mProvider.zoomIn(); + return false; } - /** - * Performs zoom out in this WebView. - * - * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes - */ public boolean zoomOut() { - checkThread(); - return mProvider.zoomOut(); - } - - /** - * @deprecated This method is now obsolete. - * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} - */ - @Deprecated - public void debugDump() { - checkThread(); - } - - /** - * See {@link ViewDebug.HierarchyHandler#dumpViewHierarchyWithProperties(BufferedWriter, int)} - * @hide - */ - @Override - public void dumpViewHierarchyWithProperties(BufferedWriter out, int level) { - mProvider.dumpViewHierarchyWithProperties(out, level); - } - - /** - * See {@link ViewDebug.HierarchyHandler#findHierarchyView(String, int)} - * @hide - */ - @Override - public View findHierarchyView(String className, int hashCode) { - return mProvider.findHierarchyView(className, hashCode); - } - - /** @hide */ - @IntDef({ - RENDERER_PRIORITY_WAIVED, - RENDERER_PRIORITY_BOUND, - RENDERER_PRIORITY_IMPORTANT - }) - @Retention(RetentionPolicy.SOURCE) - public @interface RendererPriority {} - - /** - * The renderer associated with this WebView is bound with - * {@link Context#BIND_WAIVE_PRIORITY}. At this priority level - * {@link WebView} renderers will be strong targets for out of memory - * killing. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_WAIVED = 0; - /** - * The renderer associated with this WebView is bound with - * the default priority for services. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_BOUND = 1; - /** - * The renderer associated with this WebView is bound with - * {@link Context#BIND_IMPORTANT}. - * - * Use with {@link #setRendererPriorityPolicy}. - */ - public static final int RENDERER_PRIORITY_IMPORTANT = 2; - - /** - * Set the renderer priority policy for this {@link WebView}. The - * priority policy will be used to determine whether an out of - * process renderer should be considered to be a target for OOM - * killing. - * - * Because a renderer can be associated with more than one - * WebView, the final priority it is computed as the maximum of - * any attached WebViews. When a WebView is destroyed it will - * cease to be considerered when calculating the renderer - * priority. Once no WebViews remain associated with the renderer, - * the priority of the renderer will be reduced to - * {@link #RENDERER_PRIORITY_WAIVED}. - * - * The default policy is to set the priority to - * {@link #RENDERER_PRIORITY_IMPORTANT} regardless of visibility, - * and this should not be changed unless the caller also handles - * renderer crashes with - * {@link WebViewClient#onRenderProcessGone}. Any other setting - * will result in WebView renderers being killed by the system - * more aggressively than the application. - * - * @param rendererRequestedPriority the minimum priority at which - * this WebView desires the renderer process to be bound. - * @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}. - */ - public void setRendererPriorityPolicy( - @RendererPriority int rendererRequestedPriority, - boolean waivedWhenNotVisible) { - mProvider.setRendererPriorityPolicy(rendererRequestedPriority, waivedWhenNotVisible); - } - - /** - * Get the requested renderer priority for this WebView. - * - * @return the requested renderer priority policy. - */ - @RendererPriority - public int getRendererRequestedPriority() { - return mProvider.getRendererRequestedPriority(); - } - - /** - * Return whether this WebView requests a priority of - * {@link #RENDERER_PRIORITY_WAIVED} when not visible. - * - * @return whether this WebView requests a priority of - * {@link #RENDERER_PRIORITY_WAIVED} when not visible. - */ - public boolean getRendererPriorityWaivedWhenNotVisible() { - return mProvider.getRendererPriorityWaivedWhenNotVisible(); - } - - /** - * Sets the {@link TextClassifier} for this WebView. - */ - public void setTextClassifier(@Nullable TextClassifier textClassifier) { - mProvider.setTextClassifier(textClassifier); - } - - /** - * Returns the {@link TextClassifier} used by this WebView. - * If no TextClassifier has been set, this WebView uses the default set by the system. - */ - @NonNull - public TextClassifier getTextClassifier() { - return mProvider.getTextClassifier(); - } - - //------------------------------------------------------------------------- - // Interface for WebView providers - //------------------------------------------------------------------------- - - /** - * Gets the WebViewProvider. Used by providers to obtain the underlying - * implementation, e.g. when the application responds to - * WebViewClient.onCreateWindow() request. - * - * @hide WebViewProvider is not public API. - */ - @SystemApi - public WebViewProvider getWebViewProvider() { - return mProvider; - } - - /** - * Callback interface, allows the provider implementation to access non-public methods - * and fields, and make super-class calls in this WebView instance. - * @hide Only for use by WebViewProvider implementations - */ - @SystemApi - public class PrivateAccess { - // ---- Access to super-class methods ---- - public int super_getScrollBarStyle() { - return WebView.super.getScrollBarStyle(); - } - - public void super_scrollTo(int scrollX, int scrollY) { - WebView.super.scrollTo(scrollX, scrollY); - } - - public void super_computeScroll() { - WebView.super.computeScroll(); - } - - public boolean super_onHoverEvent(MotionEvent event) { - return WebView.super.onHoverEvent(event); - } - - public boolean super_performAccessibilityAction(int action, Bundle arguments) { - return WebView.super.performAccessibilityActionInternal(action, arguments); - } - - public boolean super_performLongClick() { - return WebView.super.performLongClick(); - } - - public boolean super_setFrame(int left, int top, int right, int bottom) { - return WebView.super.setFrame(left, top, right, bottom); - } - - public boolean super_dispatchKeyEvent(KeyEvent event) { - return WebView.super.dispatchKeyEvent(event); - } - - public boolean super_onGenericMotionEvent(MotionEvent event) { - return WebView.super.onGenericMotionEvent(event); - } - - public boolean super_requestFocus(int direction, Rect previouslyFocusedRect) { - return WebView.super.requestFocus(direction, previouslyFocusedRect); - } - - public void super_setLayoutParams(ViewGroup.LayoutParams params) { - WebView.super.setLayoutParams(params); - } - - public void super_startActivityForResult(Intent intent, int requestCode) { - WebView.super.startActivityForResult(intent, requestCode); - } - - // ---- Access to non-public methods ---- - public void overScrollBy(int deltaX, int deltaY, - int scrollX, int scrollY, - int scrollRangeX, int scrollRangeY, - int maxOverScrollX, int maxOverScrollY, - boolean isTouchEvent) { - WebView.this.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, - maxOverScrollX, maxOverScrollY, isTouchEvent); - } - - public void awakenScrollBars(int duration) { - WebView.this.awakenScrollBars(duration); - } - - public void awakenScrollBars(int duration, boolean invalidate) { - WebView.this.awakenScrollBars(duration, invalidate); - } - - public float getVerticalScrollFactor() { - return WebView.this.getVerticalScrollFactor(); - } - - public float getHorizontalScrollFactor() { - return WebView.this.getHorizontalScrollFactor(); - } - - public void setMeasuredDimension(int measuredWidth, int measuredHeight) { - WebView.this.setMeasuredDimension(measuredWidth, measuredHeight); - } - - public void onScrollChanged(int l, int t, int oldl, int oldt) { - WebView.this.onScrollChanged(l, t, oldl, oldt); - } - - public int getHorizontalScrollbarHeight() { - return WebView.this.getHorizontalScrollbarHeight(); - } - - public void super_onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, - int l, int t, int r, int b) { - WebView.super.onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b); - } - - // ---- Access to (non-public) fields ---- - /** Raw setter for the scroll X value, without invoking onScrollChanged handlers etc. */ - public void setScrollXRaw(int scrollX) { - WebView.this.mScrollX = scrollX; - } - - /** Raw setter for the scroll Y value, without invoking onScrollChanged handlers etc. */ - public void setScrollYRaw(int scrollY) { - WebView.this.mScrollY = scrollY; - } - - } - - //------------------------------------------------------------------------- - // Package-private internal stuff - //------------------------------------------------------------------------- - - // Only used by android.webkit.FindActionModeCallback. - void setFindDialogFindListener(FindListener listener) { - checkThread(); - setupFindListenerIfNeeded(); - mFindListener.mFindDialogFindListener = listener; - } - - // Only used by android.webkit.FindActionModeCallback. - void notifyFindDialogDismissed() { - checkThread(); - mProvider.notifyFindDialogDismissed(); - } - - //------------------------------------------------------------------------- - // Private internal stuff - //------------------------------------------------------------------------- - - private WebViewProvider mProvider; - - /** - * In addition to the FindListener that the user may set via the WebView.setFindListener - * API, FindActionModeCallback will register it's own FindListener. We keep them separate - * via this class so that the two FindListeners can potentially exist at once. - */ - private class FindListenerDistributor implements FindListener { - private FindListener mFindDialogFindListener; - private FindListener mUserFindListener; - - @Override - public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, - boolean isDoneCounting) { - if (mFindDialogFindListener != null) { - mFindDialogFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, - isDoneCounting); - } - - if (mUserFindListener != null) { - mUserFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, - isDoneCounting); - } - } - } - private FindListenerDistributor mFindListener; - - private void setupFindListenerIfNeeded() { - if (mFindListener == null) { - mFindListener = new FindListenerDistributor(); - mProvider.setFindListener(mFindListener); - } - } - - private void ensureProviderCreated() { - checkThread(); - if (mProvider == null) { - // As this can get called during the base class constructor chain, pass the minimum - // number of dependencies here; the rest are deferred to init(). - mProvider = getFactory().createWebView(this, new PrivateAccess()); - } - } - - private static WebViewFactoryProvider getFactory() { - return WebViewFactory.getProvider(); - } - - private final Looper mWebViewThread = Looper.myLooper(); - - private void checkThread() { - // Ignore mWebViewThread == null because this can be called during in the super class - // constructor, before this class's own constructor has even started. - if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) { - Throwable throwable = new Throwable( - "A WebView method was called on thread '" + - Thread.currentThread().getName() + "'. " + - "All WebView methods must be called on the same thread. " + - "(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() + - ", FYI main Looper is " + Looper.getMainLooper() + ")"); - Log.w(LOGTAG, Log.getStackTraceString(throwable)); - StrictMode.onWebViewMethodCalledOnWrongThread(throwable); - - if (sEnforceThreadChecking) { - throw new RuntimeException(throwable); - } - } - } - - //------------------------------------------------------------------------- - // Override View methods - //------------------------------------------------------------------------- - - // TODO: Add a test that enumerates all methods in ViewDelegte & ScrollDelegate, and ensures - // there's a corresponding override (or better, caller) for each of them in here. - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mProvider.getViewDelegate().onAttachedToWindow(); - } - - /** @hide */ - @Override - protected void onDetachedFromWindowInternal() { - mProvider.getViewDelegate().onDetachedFromWindow(); - super.onDetachedFromWindowInternal(); - } - - /** @hide */ - @Override - public void onMovedToDisplay(int displayId, Configuration config) { - mProvider.getViewDelegate().onMovedToDisplay(displayId, config); - } - - @Override - public void setLayoutParams(ViewGroup.LayoutParams params) { - mProvider.getViewDelegate().setLayoutParams(params); - } - - @Override - public void setOverScrollMode(int mode) { - super.setOverScrollMode(mode); - // This method may be called in the constructor chain, before the WebView provider is - // created. - ensureProviderCreated(); - mProvider.getViewDelegate().setOverScrollMode(mode); - } - - @Override - public void setScrollBarStyle(int style) { - mProvider.getViewDelegate().setScrollBarStyle(style); - super.setScrollBarStyle(style); - } - - @Override - protected int computeHorizontalScrollRange() { - return mProvider.getScrollDelegate().computeHorizontalScrollRange(); - } - - @Override - protected int computeHorizontalScrollOffset() { - return mProvider.getScrollDelegate().computeHorizontalScrollOffset(); - } - - @Override - protected int computeVerticalScrollRange() { - return mProvider.getScrollDelegate().computeVerticalScrollRange(); - } - - @Override - protected int computeVerticalScrollOffset() { - return mProvider.getScrollDelegate().computeVerticalScrollOffset(); - } - - @Override - protected int computeVerticalScrollExtent() { - return mProvider.getScrollDelegate().computeVerticalScrollExtent(); - } - - @Override - public void computeScroll() { - mProvider.getScrollDelegate().computeScroll(); - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - return mProvider.getViewDelegate().onHoverEvent(event); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - return mProvider.getViewDelegate().onTouchEvent(event); - } - - @Override - public boolean onGenericMotionEvent(MotionEvent event) { - return mProvider.getViewDelegate().onGenericMotionEvent(event); - } - - @Override - public boolean onTrackballEvent(MotionEvent event) { - return mProvider.getViewDelegate().onTrackballEvent(event); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyUp(keyCode, event); - } - - @Override - public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { - return mProvider.getViewDelegate().onKeyMultiple(keyCode, repeatCount, event); - } - - /* - TODO: These are not currently implemented in WebViewClassic, but it seems inconsistent not - to be delegating them too. - - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyPreIme(keyCode, event); - } - @Override - public boolean onKeyLongPress(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyLongPress(keyCode, event); - } - @Override - public boolean onKeyShortcut(int keyCode, KeyEvent event) { - return mProvider.getViewDelegate().onKeyShortcut(keyCode, event); - } - */ - - @Override - public AccessibilityNodeProvider getAccessibilityNodeProvider() { - AccessibilityNodeProvider provider = - mProvider.getViewDelegate().getAccessibilityNodeProvider(); - return provider == null ? super.getAccessibilityNodeProvider() : provider; - } - - @Deprecated - @Override - public boolean shouldDelayChildPressedState() { - return mProvider.getViewDelegate().shouldDelayChildPressedState(); - } - - @Override - public CharSequence getAccessibilityClassName() { - return WebView.class.getName(); - } - - @Override - public void onProvideVirtualStructure(ViewStructure structure) { - mProvider.getViewDelegate().onProvideVirtualStructure(structure); - } - - /** - * {@inheritDoc} - * - * <p>The {@link ViewStructure} traditionally represents a {@link View}, while for web pages - * it represent HTML nodes. Hence, it's necessary to "map" the HTML properties in a way that is - * understood by the {@link android.service.autofill.AutofillService} implementations: - * - * <ol> - * <li>Only the HTML nodes inside a {@code FORM} are generated. - * <li>The source of the HTML is set using {@link ViewStructure#setWebDomain(String)} in the - * node representing the WebView. - * <li>If a web page has multiple {@code FORM}s, only the data for the current form is - * represented—if the user taps a field from another form, then the current autofill - * context is canceled (by calling {@link android.view.autofill.AutofillManager#cancel()} and - * a new context is created for that {@code FORM}. - * <li>Similarly, if the page has {@code IFRAME} nodes, they are not initially represented in - * the view structure until the user taps a field from a {@code FORM} inside the - * {@code IFRAME}, in which case it would be treated the same way as multiple forms described - * above, except that the {@link ViewStructure#setWebDomain(String) web domain} of the - * {@code FORM} contains the {@code src} attribute from the {@code IFRAME} node. - * <li>The W3C autofill field ({@code autocomplete} tag attribute) maps to - * {@link ViewStructure#setAutofillHints(String[])}. - * <li>If the view is editable, the {@link ViewStructure#setAutofillType(int)} and - * {@link ViewStructure#setAutofillValue(AutofillValue)} must be set. - * <li>The {@code placeholder} attribute maps to {@link ViewStructure#setHint(CharSequence)}. - * <li>Other HTML attributes can be represented through - * {@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}. - * </ol> - * - * <p>If the WebView implementation can determine that the value of a field was set statically - * (for example, not through Javascript), it should also call - * {@code structure.setDataIsSensitive(false)}. - * - * <p>For example, an HTML form with 2 fields for username and password: - * - * <pre class="prettyprint"> - * <input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username"> - * <input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password"> - * </pre> - * - * <p>Would map to: - * - * <pre class="prettyprint"> - * int index = structure.addChildCount(2); - * ViewStructure username = structure.newChild(index); - * username.setAutofillId(structure.getAutofillId(), 1); // id 1 - first child - * username.setAutofillHints("username"); - * username.setHtmlInfo(username.newHtmlInfoBuilder("input") - * .addAttribute("type", "text") - * .addAttribute("name", "username") - * .build()); - * username.setHint("Email or username"); - * username.setAutofillType(View.AUTOFILL_TYPE_TEXT); - * username.setAutofillValue(AutofillValue.forText("Type your username")); - * // Value of the field is not sensitive because it was created statically and not changed. - * username.setDataIsSensitive(false); - * - * ViewStructure password = structure.newChild(index + 1); - * username.setAutofillId(structure, 2); // id 2 - second child - * password.setAutofillHints("current-password"); - * password.setHtmlInfo(password.newHtmlInfoBuilder("input") - * .addAttribute("type", "password") - * .addAttribute("name", "password") - * .build()); - * password.setHint("Password"); - * password.setAutofillType(View.AUTOFILL_TYPE_TEXT); - * </pre> - */ - @Override - public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) { - mProvider.getViewDelegate().onProvideAutofillVirtualStructure(structure, flags); - } - - @Override - public void autofill(SparseArray<AutofillValue>values) { - mProvider.getViewDelegate().autofill(values); - } - - /** @hide */ - @Override - public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfoInternal(info); - mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info); - } - - /** @hide */ - @Override - public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { - super.onInitializeAccessibilityEventInternal(event); - mProvider.getViewDelegate().onInitializeAccessibilityEvent(event); - } - - /** @hide */ - @Override - public boolean performAccessibilityActionInternal(int action, Bundle arguments) { - return mProvider.getViewDelegate().performAccessibilityAction(action, arguments); - } - - /** @hide */ - @Override - protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, - int l, int t, int r, int b) { - mProvider.getViewDelegate().onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b); - } - - @Override - protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { - mProvider.getViewDelegate().onOverScrolled(scrollX, scrollY, clampedX, clampedY); - } - - @Override - protected void onWindowVisibilityChanged(int visibility) { - super.onWindowVisibilityChanged(visibility); - mProvider.getViewDelegate().onWindowVisibilityChanged(visibility); - } - - @Override - protected void onDraw(Canvas canvas) { - mProvider.getViewDelegate().onDraw(canvas); - } - - @Override - public boolean performLongClick() { - return mProvider.getViewDelegate().performLongClick(); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - mProvider.getViewDelegate().onConfigurationChanged(newConfig); - } - - /** - * Creates a new InputConnection for an InputMethod to interact with the WebView. - * This is similar to {@link View#onCreateInputConnection} but note that WebView - * calls InputConnection methods on a thread other than the UI thread. - * If these methods are overridden, then the overriding methods should respect - * thread restrictions when calling View methods or accessing data. - */ - @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - return mProvider.getViewDelegate().onCreateInputConnection(outAttrs); - } - - @Override - public boolean onDragEvent(DragEvent event) { - return mProvider.getViewDelegate().onDragEvent(event); - } - - @Override - protected void onVisibilityChanged(View changedView, int visibility) { - super.onVisibilityChanged(changedView, visibility); - // This method may be called in the constructor chain, before the WebView provider is - // created. - ensureProviderCreated(); - mProvider.getViewDelegate().onVisibilityChanged(changedView, visibility); - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - mProvider.getViewDelegate().onWindowFocusChanged(hasWindowFocus); - super.onWindowFocusChanged(hasWindowFocus); - } - - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - mProvider.getViewDelegate().onFocusChanged(focused, direction, previouslyFocusedRect); - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - /** @hide */ - @Override - protected boolean setFrame(int left, int top, int right, int bottom) { - return mProvider.getViewDelegate().setFrame(left, top, right, bottom); - } - - @Override - protected void onSizeChanged(int w, int h, int ow, int oh) { - super.onSizeChanged(w, h, ow, oh); - mProvider.getViewDelegate().onSizeChanged(w, h, ow, oh); - } - - @Override - protected void onScrollChanged(int l, int t, int oldl, int oldt) { - super.onScrollChanged(l, t, oldl, oldt); - mProvider.getViewDelegate().onScrollChanged(l, t, oldl, oldt); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return mProvider.getViewDelegate().dispatchKeyEvent(event); - } - - @Override - public boolean requestFocus(int direction, Rect previouslyFocusedRect) { - return mProvider.getViewDelegate().requestFocus(direction, previouslyFocusedRect); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mProvider.getViewDelegate().onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { - return mProvider.getViewDelegate().requestChildRectangleOnScreen(child, rect, immediate); - } - - @Override - public void setBackgroundColor(int color) { - mProvider.getViewDelegate().setBackgroundColor(color); - } - - @Override - public void setLayerType(int layerType, Paint paint) { - super.setLayerType(layerType, paint); - mProvider.getViewDelegate().setLayerType(layerType, paint); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - mProvider.getViewDelegate().preDispatchDraw(canvas); - super.dispatchDraw(canvas); - } - - @Override - public void onStartTemporaryDetach() { - super.onStartTemporaryDetach(); - mProvider.getViewDelegate().onStartTemporaryDetach(); - } - - @Override - public void onFinishTemporaryDetach() { - super.onFinishTemporaryDetach(); - mProvider.getViewDelegate().onFinishTemporaryDetach(); - } - - @Override - public Handler getHandler() { - return mProvider.getViewDelegate().getHandler(super.getHandler()); - } - - @Override - public View findFocus() { - return mProvider.getViewDelegate().findFocus(super.findFocus()); - } - - /** - * If WebView has already been loaded into the current process this method will return the - * package that was used to load it. Otherwise, the package that would be used if the WebView - * was loaded right now will be returned; this does not cause WebView to be loaded, so this - * information may become outdated at any time. - * The WebView package changes either when the current WebView package is updated, disabled, or - * 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 {@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 service.getCurrentWebViewPackage(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** - * Receive the result from a previous call to {@link #startActivityForResult(Intent, int)}. - * - * @param requestCode The integer request code originally supplied to - * startActivityForResult(), allowing you to identify who this - * result came from. - * @param resultCode The integer result code returned by the child activity - * through its setResult(). - * @param data An Intent, which can return result data to the caller - * (various data can be attached to Intent "extras"). - * @hide - */ - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - mProvider.getViewDelegate().onActivityResult(requestCode, resultCode, data); - } - - /** @hide */ - @Override - protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { - super.encodeProperties(encoder); - - checkThread(); - encoder.addProperty("webview:contentHeight", mProvider.getContentHeight()); - encoder.addProperty("webview:contentWidth", mProvider.getContentWidth()); - encoder.addProperty("webview:scale", mProvider.getScale()); - encoder.addProperty("webview:title", mProvider.getTitle()); - encoder.addProperty("webview:url", mProvider.getUrl()); - encoder.addProperty("webview:originalUrl", mProvider.getOriginalUrl()); + return false; } } diff --git a/android/widget/Editor.java b/android/widget/Editor.java index afd11881..384f4f83 100644 --- a/android/widget/Editor.java +++ b/android/widget/Editor.java @@ -165,7 +165,7 @@ public class Editor { private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11; private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100; - private static final float MAGNIFIER_ZOOM = 1.5f; + private static final float MAGNIFIER_ZOOM = 1.25f; @IntDef({MagnifierHandleTrigger.SELECTION_START, MagnifierHandleTrigger.SELECTION_END, MagnifierHandleTrigger.INSERTION}) @@ -3888,7 +3888,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); } } @@ -4539,23 +4539,21 @@ public class Editor { final Layout layout = mTextView.getLayout(); final int lineNumber = layout.getLineForOffset(offset); // Horizontally snap to character offset. - final float xPosInView = getHorizontal(mTextView.getLayout(), 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; - final int[] coordinatesOnScreen = new int[2]; - mTextView.getLocationOnScreen(coordinatesOnScreen); - final float centerXOnScreen = xPosInView + mTextView.getTotalPaddingLeft() - - mTextView.getScrollX() + coordinatesOnScreen[0]; - final float centerYOnScreen = yPosInView + mTextView.getTotalPaddingTop() - - mTextView.getScrollY() + coordinatesOnScreen[1]; + + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f + + mTextView.getTotalPaddingTop() - mTextView.getScrollY(); - mMagnifier.show(centerXOnScreen, centerYOnScreen, MAGNIFIER_ZOOM); + suspendBlink(); + mMagnifier.show(xPosInView, yPosInView, MAGNIFIER_ZOOM); } protected final void dismissMagnifier() { if (mMagnifier != null) { mMagnifier.dismiss(); + resumeBlink(); } } diff --git a/android/widget/RemoteViews.java b/android/widget/RemoteViews.java index 1b26f8e2..631f3882 100644 --- a/android/widget/RemoteViews.java +++ b/android/widget/RemoteViews.java @@ -131,7 +131,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) @@ -1519,8 +1519,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 @@ -2138,8 +2137,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; @@ -2653,7 +2651,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 @@ -3551,6 +3553,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/android/widget/RemoteViewsAdapter.java b/android/widget/RemoteViewsAdapter.java index 09686521..e5ae0ca0 100644 --- a/android/widget/RemoteViewsAdapter.java +++ b/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/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java index 3be42a5b..5e22650a 100644 --- a/android/widget/SelectionActionModeHelper.java +++ b/android/widget/SelectionActionModeHelper.java @@ -95,11 +95,15 @@ public final class SelectionActionModeHelper { } public void startActionModeAsync(boolean adjustSelection) { + // Check if the smart selection should run for editable text. + adjustSelection &= !mTextView.isTextEditable() + || mTextView.getTextClassifier().getSettings() + .isSuggestSelectionEnabledForEditableText(); + mSelectionTracker.onOriginalSelection( getText(mTextView), mTextView.getSelectionStart(), - mTextView.getSelectionEnd(), - mTextView.isTextEditable()); + mTextView.getSelectionEnd()); cancelAsyncTask(); if (skipTextClassification()) { startActionMode(null); @@ -196,7 +200,10 @@ public final class SelectionActionModeHelper { private void startActionMode(@Nullable SelectionResult result) { final CharSequence text = getText(mTextView); if (result != null && text instanceof Spannable) { - Selection.setSelection((Spannable) text, result.mStart, result.mEnd); + // Do not change the selection if TextClassifier should be dark launched. + if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) { + Selection.setSelection((Spannable) text, result.mStart, result.mEnd); + } mTextClassification = result.mClassification; } else { mTextClassification = null; @@ -377,7 +384,7 @@ public final class SelectionActionModeHelper { } private void resetTextClassificationHelper() { - mTextClassificationHelper.reset( + mTextClassificationHelper.init( mTextView.getTextClassifier(), getText(mTextView), mTextView.getSelectionStart(), mTextView.getSelectionEnd(), @@ -415,8 +422,7 @@ public final class SelectionActionModeHelper { /** * Called when the original selection happens, before smart selection is triggered. */ - public void onOriginalSelection( - CharSequence text, int selectionStart, int selectionEnd, boolean editableText) { + public void onOriginalSelection(CharSequence text, int selectionStart, int selectionEnd) { // If we abandoned a selection and created a new one very shortly after, we may still // have a pending request to log ABANDON, which we flush here. mDelayedLogAbandon.flush(); @@ -812,11 +818,11 @@ public final class SelectionActionModeHelper { TextClassificationHelper(TextClassifier textClassifier, CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) { - reset(textClassifier, text, selectionStart, selectionEnd, locales); + init(textClassifier, text, selectionStart, selectionEnd, locales); } @UiThread - public void reset(TextClassifier textClassifier, + public void init(TextClassifier textClassifier, CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) { mTextClassifier = Preconditions.checkNotNull(textClassifier); mText = Preconditions.checkNotNull(text).toString(); @@ -839,8 +845,12 @@ public final class SelectionActionModeHelper { trimText(); final TextSelection selection = mTextClassifier.suggestSelection( mTrimmedText, mRelativeStart, mRelativeEnd, mLocales); - mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart); - mSelectionEnd = Math.min(mText.length(), selection.getSelectionEndIndex() + mTrimStart); + // Do not classify new selection boundaries if TextClassifier should be dark launched. + if (!mTextClassifier.getSettings().isDarkLaunch()) { + mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart); + mSelectionEnd = Math.min( + mText.length(), selection.getSelectionEndIndex() + mTrimStart); + } return performClassification(selection); } diff --git a/android/widget/TextView.java b/android/widget/TextView.java index 24ae03c3..d9bc51ff 100644 --- a/android/widget/TextView.java +++ b/android/widget/TextView.java @@ -10338,6 +10338,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()); |