diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-10-10 15:20:13 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-10-10 15:20:13 -0400 |
commit | 93b7ee4fce01df52a6607f0b1965cbafdfeaf1a6 (patch) | |
tree | 49f76f879a89c256a4f65b674086be50760bdffb /android | |
parent | bc81c7ada5aab3806dd0b17498f5c9672c9b33c4 (diff) | |
download | android-28-93b7ee4fce01df52a6607f0b1965cbafdfeaf1a6.tar.gz |
Import Android SDK Platform P [4386628]
/google/data/ro/projects/android/fetch_artifact \
--bid 4386628 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4386628.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I9b8400ac92116cae4f033d173f7a5682b26ccba9
Diffstat (limited to 'android')
523 files changed, 25881 insertions, 8050 deletions
diff --git a/android/accounts/AbstractAccountAuthenticator.java b/android/accounts/AbstractAccountAuthenticator.java index bf9bd79e..a3b3a9f2 100644 --- a/android/accounts/AbstractAccountAuthenticator.java +++ b/android/accounts/AbstractAccountAuthenticator.java @@ -175,6 +175,9 @@ public abstract class AbstractAccountAuthenticator { } if (result != null) { response.onResult(result); + } else { + response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, + "null bundle returned"); } } catch (Exception e) { handleException(response, "addAccount", accountType, e); diff --git a/android/accounts/AccountManager.java b/android/accounts/AccountManager.java index dd6ad55f..bd9c9fa3 100644 --- a/android/accounts/AccountManager.java +++ b/android/accounts/AccountManager.java @@ -2321,6 +2321,10 @@ public class AccountManager { private class Response extends IAccountManagerResponse.Stub { @Override public void onResult(Bundle bundle) { + if (bundle == null) { + onError(ERROR_CODE_INVALID_RESPONSE, "null bundle returned"); + return; + } Intent intent = bundle.getParcelable(KEY_INTENT); if (intent != null && mActivity != null) { // since the user provided an Activity we will silently start intents diff --git a/android/animation/AnimatorSet.java b/android/animation/AnimatorSet.java index 00d6657e..1a2dc5cd 100644 --- a/android/animation/AnimatorSet.java +++ b/android/animation/AnimatorSet.java @@ -843,7 +843,7 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim // Assumes forward playing from here on. for (int i = 0; i < mEvents.size(); i++) { AnimationEvent event = mEvents.get(i); - if (event.getTime() > currentPlayTime) { + if (event.getTime() > currentPlayTime || event.getTime() == DURATION_INFINITE) { break; } @@ -1264,7 +1264,8 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim } else { for (int i = mLastEventId + 1; i < size; i++) { AnimationEvent event = mEvents.get(i); - if (event.getTime() <= currentPlayTime) { + // TODO: need a function that accounts for infinite duration to compare time + if (event.getTime() != DURATION_INFINITE && event.getTime() <= currentPlayTime) { latestId = i; } } diff --git a/android/annotation/NavigationRes.java b/android/annotation/NavigationRes.java new file mode 100644 index 00000000..3af5ecff --- /dev/null +++ b/android/annotation/NavigationRes.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a navigation resource reference (e.g. {@code R.navigation.flow}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface NavigationRes { +} diff --git a/android/app/Activity.java b/android/app/Activity.java index 4e258a3a..e0ac9113 100644 --- a/android/app/Activity.java +++ b/android/app/Activity.java @@ -114,6 +114,7 @@ import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.view.autofill.AutofillManager; +import android.view.autofill.AutofillManager.AutofillClient; import android.view.autofill.AutofillPopupWindow; import android.view.autofill.IAutofillWindowPresenter; import android.widget.AdapterView; @@ -947,6 +948,18 @@ public class Activity extends ContextThemeWrapper return mAutofillManager; } + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(newBase); + newBase.setAutofillClient(this); + } + + /** @hide */ + @Override + public final AutofillClient getAutofillClient() { + return this; + } + /** * Called when the activity is starting. This is where most initialization * should go: calling {@link #setContentView(int)} to inflate the diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java index a8665037..5e61727f 100644 --- a/android/app/ActivityManager.java +++ b/android/app/ActivityManager.java @@ -16,14 +16,10 @@ package android.app; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; 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; @@ -46,6 +42,7 @@ import android.content.pm.IPackageDataObserver; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.UserInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -675,32 +672,27 @@ public class ActivityManager { /** First static stack ID. * @hide */ - public static final int FIRST_STATIC_STACK_ID = 0; + private static final int FIRST_STATIC_STACK_ID = 0; - /** Home activity stack ID. */ - public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID; - - /** ID of stack where fullscreen activities are normally launched into. */ + /** 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. */ + /** 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. */ + /** 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. */ + /** ID of stack that always on top (always visible) when it exist. + * @hide */ public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1; - /** ID of stack that contains the Recents activity. */ - public static final int RECENTS_STACK_ID = PINNED_STACK_ID + 1; - - /** ID of stack that contains activities launched by the assistant. */ - public static final int ASSISTANT_STACK_ID = RECENTS_STACK_ID + 1; - /** Last static stack stack ID. * @hide */ - public static final int LAST_STATIC_STACK_ID = ASSISTANT_STACK_ID; + private static final int LAST_STATIC_STACK_ID = PINNED_STACK_ID; /** Start of ID range used by stacks that are created dynamically. * @hide */ @@ -720,15 +712,6 @@ public class ActivityManager { } /** - * Returns true if dynamic stacks are allowed to be visible behind the input stack. - * @hide - */ - // TODO: Figure-out a way to remove. - public static boolean isDynamicStacksVisibleBehindAllowed(int stackId) { - return stackId == PINNED_STACK_ID || stackId == ASSISTANT_STACK_ID; - } - - /** * Returns true if we try to maintain focus in the current stack when the top activity * finishes. * @hide @@ -740,15 +723,6 @@ public class ActivityManager { } /** - * Returns true if the input stack is affected by drag resizing. - * @hide - */ - public static boolean isStackAffectedByDragResizing(int stackId) { - return isStaticStack(stackId) && stackId != PINNED_STACK_ID - && stackId != ASSISTANT_STACK_ID; - } - - /** * Returns true if the windows of tasks being moved to the target stack from the source * stack should be replaced, meaning that window manager will keep the old window around * until the new is ready. @@ -760,41 +734,6 @@ public class ActivityManager { } /** - * Return whether a stackId is a stack that be a backdrop to a translucent activity. These - * are generally fullscreen stacks. - * @hide - */ - public static boolean isBackdropToTranslucentActivity(int stackId) { - return stackId == FULLSCREEN_WORKSPACE_STACK_ID - || stackId == ASSISTANT_STACK_ID; - } - - /** - * Returns true if animation specs should be constructed for app transition that moves - * the task to the specified stack. - * @hide - */ - public static boolean useAnimationSpecForAppTransition(int stackId) { - // TODO: INVALID_STACK_ID is also animated because we don't persist stack id's across - // reboots. - return stackId == FREEFORM_WORKSPACE_STACK_ID - || stackId == FULLSCREEN_WORKSPACE_STACK_ID - || stackId == ASSISTANT_STACK_ID - || stackId == DOCKED_STACK_ID - || stackId == INVALID_STACK_ID; - } - - /** - * Returns true if activities from stasks in the given {@param stackId} are allowed to - * enter picture-in-picture. - * @hide - */ - public static boolean isAllowedToEnterPictureInPicture(int stackId) { - return stackId != HOME_STACK_ID && stackId != ASSISTANT_STACK_ID && - stackId != RECENTS_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 @@ -825,34 +764,18 @@ public class ActivityManager { && stackId != DOCKED_STACK_ID; } - /** - * Returns true if the input stack id should only be present on a device that supports - * multi-window mode. - * @see android.app.ActivityManager#supportsMultiWindow - * @hide - */ - // TODO: What about the other side of docked stack if we move this to WindowConfiguration? - public static boolean isMultiWindowStack(int stackId) { - return stackId == PINNED_STACK_ID || stackId == FREEFORM_WORKSPACE_STACK_ID - || stackId == DOCKED_STACK_ID; - } - - /** - * Returns true if the input {@param stackId} is HOME_STACK_ID or RECENTS_STACK_ID - * @hide - */ - public static boolean isHomeOrRecentsStack(int stackId) { - return stackId == HOME_STACK_ID || stackId == RECENTS_STACK_ID; - } - - /** Returns true if the input stack and its content can affect the device orientation. + /** Returns the stack id for the input windowing mode. * @hide */ - public static boolean canSpecifyOrientation(int stackId) { - return stackId == HOME_STACK_ID - || stackId == RECENTS_STACK_ID - || stackId == FULLSCREEN_WORKSPACE_STACK_ID - || stackId == ASSISTANT_STACK_ID - || isDynamicStack(stackId); + // 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. @@ -862,14 +785,9 @@ public class ActivityManager { final int windowingMode; switch (stackId) { case FULLSCREEN_WORKSPACE_STACK_ID: - case HOME_STACK_ID: - case RECENTS_STACK_ID: windowingMode = inSplitScreenMode ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY : WINDOWING_MODE_FULLSCREEN; break; - case ASSISTANT_STACK_ID: - windowingMode = WINDOWING_MODE_FULLSCREEN; - break; case PINNED_STACK_ID: windowingMode = WINDOWING_MODE_PINNED; break; @@ -884,27 +802,6 @@ public class ActivityManager { } return windowingMode; } - - /** Returns the activity type 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 getActivityTypeForStackId(int stackId) { - final int activityType; - switch (stackId) { - case HOME_STACK_ID: - activityType = ACTIVITY_TYPE_HOME; - break; - case RECENTS_STACK_ID: - activityType = ACTIVITY_TYPE_RECENTS; - break; - case ASSISTANT_STACK_ID: - activityType = ACTIVITY_TYPE_ASSISTANT; - break; - default : - activityType = ACTIVITY_TYPE_STANDARD; - } - return activityType; - } } /** @@ -1143,6 +1040,7 @@ public class ActivityManager { * E.g. freeform, split-screen, picture-in-picture. * @hide */ + @TestApi static public boolean supportsMultiWindow(Context context) { // On watches, multi-window is used to present essential system UI, and thus it must be // supported regardless of device memory characteristics. @@ -1157,6 +1055,7 @@ public class ActivityManager { * Returns true if the system supports split screen multi-window. * @hide */ + @TestApi static public boolean supportsSplitScreenMultiWindow(Context context) { return supportsMultiWindow(context) && Resources.getSystem().getBoolean( @@ -1636,6 +1535,12 @@ public class ActivityManager { */ public int resizeMode; + /** + * The current configuration this task is in. + * @hide + */ + final public Configuration configuration = new Configuration(); + public RecentTaskInfo() { } @@ -1681,6 +1586,7 @@ public class ActivityManager { } dest.writeInt(supportsSplitScreenMultiWindow ? 1 : 0); dest.writeInt(resizeMode); + configuration.writeToParcel(dest, flags); } public void readFromParcel(Parcel source) { @@ -1705,6 +1611,7 @@ public class ActivityManager { Rect.CREATOR.createFromParcel(source) : null; supportsSplitScreenMultiWindow = source.readInt() == 1; resizeMode = source.readInt(); + configuration.readFromParcel(source); } public static final Creator<RecentTaskInfo> CREATOR @@ -1899,6 +1806,12 @@ public class ActivityManager { */ public int resizeMode; + /** + * The full configuration the task is currently running in. + * @hide + */ + final public Configuration configuration = new Configuration(); + public RunningTaskInfo() { } @@ -1923,6 +1836,7 @@ public class ActivityManager { dest.writeInt(numRunning); dest.writeInt(supportsSplitScreenMultiWindow ? 1 : 0); dest.writeInt(resizeMode); + configuration.writeToParcel(dest, flags); } public void readFromParcel(Parcel source) { @@ -1940,6 +1854,7 @@ public class ActivityManager { numRunning = source.readInt(); supportsSplitScreenMultiWindow = source.readInt() != 0; resizeMode = source.readInt(); + configuration.readFromParcel(source); } public static final Creator<RunningTaskInfo> CREATOR = new Creator<RunningTaskInfo>() { @@ -2122,6 +2037,73 @@ public class ActivityManager { } /** + * Sets the windowing mode for a specific task. Only works on tasks of type + * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD} + * @param taskId The id of the task to set the windowing mode for. + * @param windowingMode The windowing mode to set for the task. + * @param toTop If the task should be moved to the top once the windowing mode changes. + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void setTaskWindowingMode(int taskId, int windowingMode, boolean toTop) + throws SecurityException { + try { + getService().setTaskWindowingMode(taskId, windowingMode, toTop); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Resizes the input stack id to the given bounds. + * @param stackId Id of the stack to resize. + * @param bounds Bounds to resize the stack to or {@code null} for fullscreen. + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void resizeStack(int stackId, Rect bounds) throws SecurityException { + try { + getService().resizeStack(stackId, bounds, false /* allowResizeInDockedMode */, + false /* preserveWindows */, false /* animate */, -1 /* animationDuration */); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes stacks in the windowing modes from the system if they are of activity type + * ACTIVITY_TYPE_STANDARD or ACTIVITY_TYPE_UNDEFINED + * + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void removeStacksInWindowingModes(int[] windowingModes) throws SecurityException { + try { + getService().removeStacksInWindowingModes(windowingModes); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes stack of the activity types from the system. + * + * @hide + */ + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void removeStacksWithActivityTypes(int[] activityTypes) throws SecurityException { + try { + getService().removeStacksWithActivityTypes(activityTypes); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Represents a task snapshot. * @hide */ @@ -2599,6 +2581,11 @@ public class ActivityManager { public boolean visible; // Index of the stack in the display's stack list, can be used for comparison of stack order public int position; + /** + * The full configuration the stack is currently running in. + * @hide + */ + final public Configuration configuration = new Configuration(); @Override public int describeContents() { @@ -2633,6 +2620,7 @@ public class ActivityManager { } else { dest.writeInt(0); } + configuration.writeToParcel(dest, flags); } public void readFromParcel(Parcel source) { @@ -2660,6 +2648,7 @@ public class ActivityManager { if (source.readInt() > 0) { topActivity = ComponentName.readFromParcel(source); } + configuration.readFromParcel(source); } public static final Creator<StackInfo> CREATOR = new Creator<StackInfo>() { @@ -2687,6 +2676,8 @@ public class ActivityManager { sb.append(" displayId="); sb.append(displayId); sb.append(" userId="); sb.append(userId); sb.append("\n"); + sb.append(" configuration="); sb.append(configuration); + sb.append("\n"); prefix = prefix + " "; for (int i = 0; i < taskIds.length; ++i) { sb.append(prefix); sb.append("taskId="); sb.append(taskIds[i]); diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java index 0bffc9e6..a68c3a5c 100644 --- a/android/app/ActivityOptions.java +++ b/android/app/ActivityOptions.java @@ -18,6 +18,8 @@ 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; @@ -164,10 +166,16 @@ public class ActivityOptions { private static final String KEY_LAUNCH_DISPLAY_ID = "android.activity.launchDisplayId"; /** - * The stack id the activity should be launched into. + * The windowing mode the activity should be launched into. * @hide */ - private static final String KEY_LAUNCH_STACK_ID = "android.activity.launchStackId"; + private static final String KEY_LAUNCH_WINDOWING_MODE = "android.activity.windowingMode"; + + /** + * The activity type the activity should be launched as. + * @hide + */ + private static final String KEY_LAUNCH_ACTIVITY_TYPE = "android.activity.activityType"; /** * The task id the activity should be launched into. @@ -272,7 +280,10 @@ public class ActivityOptions { private int mExitCoordinatorIndex; private PendingIntent mUsageTimeReport; private int mLaunchDisplayId = INVALID_DISPLAY; - private int mLaunchStackId = INVALID_STACK_ID; + @WindowConfiguration.WindowingMode + private int mLaunchWindowingMode = WINDOWING_MODE_UNDEFINED; + @WindowConfiguration.ActivityType + private int mLaunchActivityType = ACTIVITY_TYPE_UNDEFINED; private int mLaunchTaskId = -1; private int mDockCreateMode = DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; private boolean mDisallowEnterPictureInPictureWhileLaunching; @@ -860,7 +871,8 @@ public class ActivityOptions { break; } mLaunchDisplayId = opts.getInt(KEY_LAUNCH_DISPLAY_ID, INVALID_DISPLAY); - mLaunchStackId = opts.getInt(KEY_LAUNCH_STACK_ID, INVALID_STACK_ID); + mLaunchWindowingMode = opts.getInt(KEY_LAUNCH_WINDOWING_MODE, WINDOWING_MODE_UNDEFINED); + mLaunchActivityType = opts.getInt(KEY_LAUNCH_ACTIVITY_TYPE, ACTIVITY_TYPE_UNDEFINED); mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1); mTaskOverlay = opts.getBoolean(KEY_TASK_OVERLAY, false); mTaskOverlayCanResume = opts.getBoolean(KEY_TASK_OVERLAY_CAN_RESUME, false); @@ -1070,14 +1082,34 @@ public class ActivityOptions { } /** @hide */ - public int getLaunchStackId() { - return mLaunchStackId; + public int getLaunchWindowingMode() { + return mLaunchWindowingMode; + } + + /** + * Sets the windowing mode the activity should launch into. If the input windowing mode is + * {@link android.app.WindowConfiguration#WINDOWING_MODE_SPLIT_SCREEN_SECONDARY} and the device + * isn't currently in split-screen windowing mode, then the activity will be launched in + * {@link android.app.WindowConfiguration#WINDOWING_MODE_FULLSCREEN} windowing mode. For clarity + * on this you can use + * {@link android.app.WindowConfiguration#WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY} + * + * @hide + */ + @TestApi + public void setLaunchWindowingMode(int windowingMode) { + mLaunchWindowingMode = windowingMode; + } + + /** @hide */ + public int getLaunchActivityType() { + return mLaunchActivityType; } /** @hide */ @TestApi - public void setLaunchStackId(int launchStackId) { - mLaunchStackId = launchStackId; + public void setLaunchActivityType(int activityType) { + mLaunchActivityType = activityType; } /** @@ -1291,7 +1323,8 @@ public class ActivityOptions { break; } b.putInt(KEY_LAUNCH_DISPLAY_ID, mLaunchDisplayId); - b.putInt(KEY_LAUNCH_STACK_ID, mLaunchStackId); + b.putInt(KEY_LAUNCH_WINDOWING_MODE, mLaunchWindowingMode); + b.putInt(KEY_LAUNCH_ACTIVITY_TYPE, mLaunchActivityType); b.putInt(KEY_LAUNCH_TASK_ID, mLaunchTaskId); b.putBoolean(KEY_TASK_OVERLAY, mTaskOverlay); b.putBoolean(KEY_TASK_OVERLAY_CAN_RESUME, mTaskOverlayCanResume); diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java index 4e8d2400..2516a3e9 100644 --- a/android/app/ActivityThread.java +++ b/android/app/ActivityThread.java @@ -5281,7 +5281,7 @@ public final class ActivityThread { final ApplicationInfo aInfo = sPackageManager.getApplicationInfo( packageName, - 0 /*flags*/, + PackageManager.GET_SHARED_LIBRARY_FILES, UserHandle.myUserId()); if (mActivities.size() > 0) { @@ -5780,7 +5780,7 @@ public final class ActivityThread { final int preloadedFontsResource = info.metaData.getInt( ApplicationInfo.METADATA_PRELOADED_FONTS, 0); if (preloadedFontsResource != 0) { - data.info.mResources.preloadFonts(preloadedFontsResource); + data.info.getResources().preloadFonts(preloadedFontsResource); } } } catch (RemoteException e) { diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java index c48be770..5f343226 100644 --- a/android/app/ContextImpl.java +++ b/android/app/ContextImpl.java @@ -74,6 +74,7 @@ import android.util.Log; import android.util.Slog; import android.view.Display; import android.view.DisplayAdjustments; +import android.view.autofill.AutofillManager.AutofillClient; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; @@ -185,6 +186,8 @@ class ContextImpl extends Context { // The name of the split this Context is representing. May be null. private @Nullable String mSplitName = null; + private AutofillClient mAutofillClient = null; + private final Object mSync = new Object(); @GuardedBy("mSync") @@ -2225,6 +2228,18 @@ class ContextImpl extends Context { return mUser.getIdentifier(); } + /** @hide */ + @Override + public AutofillClient getAutofillClient() { + return mAutofillClient; + } + + /** @hide */ + @Override + public void setAutofillClient(AutofillClient client) { + mAutofillClient = client; + } + static ContextImpl createSystemContext(ActivityThread mainThread) { LoadedApk packageInfo = new LoadedApk(mainThread); ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, diff --git a/android/app/KeyguardManager.java b/android/app/KeyguardManager.java index 76643d60..54f74b15 100644 --- a/android/app/KeyguardManager.java +++ b/android/app/KeyguardManager.java @@ -174,7 +174,7 @@ public class KeyguardManager { */ public Intent createConfirmFactoryResetCredentialIntent( CharSequence title, CharSequence description, CharSequence alternateButtonLabel) { - if (!LockPatternUtils.frpCredentialEnabled()) { + if (!LockPatternUtils.frpCredentialEnabled(mContext)) { Log.w(TAG, "Factory reset credentials not supported."); return null; } diff --git a/android/app/Notification.java b/android/app/Notification.java index ee6c1cba..fee7d6c8 100644 --- a/android/app/Notification.java +++ b/android/app/Notification.java @@ -67,7 +67,6 @@ import android.text.style.TextAppearanceSpan; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; -import android.util.TypedValue; import android.view.Gravity; import android.view.NotificationHeaderView; import android.view.View; @@ -3840,8 +3839,8 @@ public class Notification implements Parcelable contentView.setImageViewBitmap(R.id.profile_badge, profileBadge); contentView.setViewVisibility(R.id.profile_badge, View.VISIBLE); if (isColorized()) { - contentView.setDrawableParameters(R.id.profile_badge, false, -1, - getPrimaryTextColor(), PorterDuff.Mode.SRC_ATOP, -1); + contentView.setDrawableTint(R.id.profile_badge, false, + getPrimaryTextColor(), PorterDuff.Mode.SRC_ATOP); } } } @@ -3906,7 +3905,6 @@ public class Notification implements Parcelable if (p.title != null) { contentView.setViewVisibility(R.id.title, View.VISIBLE); contentView.setTextViewText(R.id.title, processTextSpans(p.title)); - updateTextSizePrimary(contentView, R.id.title); if (!p.ambient) { setTextViewColorPrimary(contentView, R.id.title); } @@ -3918,7 +3916,6 @@ public class Notification implements Parcelable int textId = showProgress ? com.android.internal.R.id.text_line_1 : com.android.internal.R.id.text; contentView.setTextViewText(textId, processTextSpans(p.text)); - updateTextSizeSecondary(contentView, textId); if (!p.ambient) { setTextViewColorSecondary(contentView, textId); } @@ -3930,25 +3927,6 @@ public class Notification implements Parcelable return contentView; } - private void updateTextSizeSecondary(RemoteViews contentView, int textId) { - updateTextSizeColorized(contentView, textId, - com.android.internal.R.dimen.notification_text_size_colorized, - com.android.internal.R.dimen.notification_text_size); - } - - private void updateTextSizePrimary(RemoteViews contentView, int textId) { - updateTextSizeColorized(contentView, textId, - com.android.internal.R.dimen.notification_title_text_size_colorized, - com.android.internal.R.dimen.notification_title_text_size); - } - - private void updateTextSizeColorized(RemoteViews contentView, int textId, - int colorizedDimen, int normalDimen) { - int size = mContext.getResources().getDimensionPixelSize(isColorized() - ? colorizedDimen : normalDimen); - contentView.setTextViewTextSize(textId, TypedValue.COMPLEX_UNIT_PX, size); - } - private CharSequence processTextSpans(CharSequence text) { if (hasForegroundColor()) { return NotificationColorUtil.clearColorSpans(text); @@ -4152,18 +4130,14 @@ public class Notification implements Parcelable if (action != null) { int contrastColor = resolveContrastColor(); - contentView.setDrawableParameters(R.id.reply_icon_action, + contentView.setDrawableTint(R.id.reply_icon_action, true /* targetBackground */, - -1, - contrastColor, - PorterDuff.Mode.SRC_ATOP, -1); + contrastColor, PorterDuff.Mode.SRC_ATOP); int iconColor = NotificationColorUtil.isColorLight(contrastColor) ? Color.BLACK : Color.WHITE; - contentView.setDrawableParameters(R.id.reply_icon_action, + contentView.setDrawableTint(R.id.reply_icon_action, false /* targetBackground */, - -1, - iconColor, - PorterDuff.Mode.SRC_ATOP, -1); + iconColor, PorterDuff.Mode.SRC_ATOP); contentView.setOnClickPendingIntent(R.id.right_icon, action.actionIntent); contentView.setOnClickPendingIntent(R.id.reply_icon_action, @@ -4207,8 +4181,8 @@ public class Notification implements Parcelable private void bindExpandButton(RemoteViews contentView) { int color = getPrimaryHighlightColor(); - contentView.setDrawableParameters(R.id.expand_button, false, -1, color, - PorterDuff.Mode.SRC_ATOP, -1); + contentView.setDrawableTint(R.id.expand_button, false, color, + PorterDuff.Mode.SRC_ATOP); contentView.setInt(R.id.notification_header, "setOriginalNotificationColor", color); } @@ -4315,8 +4289,7 @@ public class Notification implements Parcelable mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon); } contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon); - contentView.setDrawableParameters(R.id.icon, false /* targetBackground */, - -1 /* alpha */, -1 /* colorFilter */, null /* mode */, mN.iconLevel); + contentView.setInt(R.id.icon, "setImageLevel", mN.iconLevel); processSmallIconColor(mN.mSmallIcon, contentView, ambient); } @@ -4706,8 +4679,8 @@ public class Notification implements Parcelable bgColor = mContext.getColor(oddAction ? R.color.notification_action_list : R.color.notification_action_list_dark); } - button.setDrawableParameters(R.id.button_holder, true, -1, bgColor, - PorterDuff.Mode.SRC_ATOP, -1); + button.setDrawableTint(R.id.button_holder, true, + bgColor, PorterDuff.Mode.SRC_ATOP); CharSequence title = action.title; ColorStateList[] outResultColor = null; if (isLegacy()) { @@ -4840,8 +4813,8 @@ public class Notification implements Parcelable boolean colorable = !isLegacy() || getColorUtil().isGrayscaleIcon(mContext, smallIcon); int color = ambient ? resolveAmbientColor() : getPrimaryHighlightColor(); if (colorable) { - contentView.setDrawableParameters(R.id.icon, false, -1, color, - PorterDuff.Mode.SRC_ATOP, -1); + contentView.setDrawableTint(R.id.icon, false, color, + PorterDuff.Mode.SRC_ATOP); } contentView.setInt(R.id.notification_header, "setOriginalIconColor", @@ -4857,8 +4830,8 @@ public class Notification implements Parcelable if (largeIcon != null && isLegacy() && getColorUtil().isGrayscaleIcon(mContext, largeIcon)) { // resolve color will fall back to the default when legacy - contentView.setDrawableParameters(R.id.icon, false, -1, resolveContrastColor(), - PorterDuff.Mode.SRC_ATOP, -1); + contentView.setDrawableTint(R.id.icon, false, resolveContrastColor(), + PorterDuff.Mode.SRC_ATOP); } } @@ -5874,7 +5847,6 @@ public class Notification implements Parcelable builder.setTextViewColorSecondary(contentView, R.id.big_text); contentView.setViewVisibility(R.id.big_text, TextUtils.isEmpty(bigTextText) ? View.GONE : View.VISIBLE); - builder.updateTextSizeSecondary(contentView, R.id.big_text); contentView.setBoolean(R.id.big_text, "setHasImage", builder.mN.hasLargeIcon()); } } @@ -6208,7 +6180,6 @@ public class Notification implements Parcelable contentView.setViewVisibility(rowId, View.VISIBLE); contentView.setTextViewText(rowId, mBuilder.processTextSpans( makeMessageLine(m, mBuilder))); - mBuilder.updateTextSizeSecondary(contentView, rowId); mBuilder.setTextViewColorSecondary(contentView, rowId); if (contractedMessage == m) { @@ -6576,7 +6547,6 @@ public class Notification implements Parcelable contentView.setViewVisibility(rowIds[i], View.VISIBLE); contentView.setTextViewText(rowIds[i], mBuilder.processTextSpans(mBuilder.processLegacyText(str))); - mBuilder.updateTextSizeSecondary(contentView, rowIds[i]); mBuilder.setTextViewColorSecondary(contentView, rowIds[i]); contentView.setViewPadding(rowIds[i], 0, topPadding, 0, 0); handleInboxImageMargin(contentView, rowIds[i], first); @@ -6775,8 +6745,8 @@ public class Notification implements Parcelable : NotificationColorUtil.resolveColor(mBuilder.mContext, Notification.COLOR_DEFAULT); - button.setDrawableParameters(R.id.action0, false, -1, tintColor, - PorterDuff.Mode.SRC_ATOP, -1); + button.setDrawableTint(R.id.action0, false, tintColor, + PorterDuff.Mode.SRC_ATOP); if (!tombstone) { button.setOnClickPendingIntent(R.id.action0, action.actionIntent); } diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java index 163a8dca..47063f08 100644 --- a/android/app/NotificationChannel.java +++ b/android/app/NotificationChannel.java @@ -15,8 +15,11 @@ */ package android.app; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.NotificationManager.Importance; +import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; import android.media.AudioAttributes; import android.net.Uri; @@ -25,6 +28,9 @@ import android.os.Parcelable; import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.text.TextUtils; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.Preconditions; import org.json.JSONException; import org.json.JSONObject; @@ -135,12 +141,15 @@ public final class NotificationChannel implements Parcelable { private boolean mLights; private int mLightColor = DEFAULT_LIGHT_COLOR; private long[] mVibration; + // Bitwise representation of fields that have been changed by the user, preventing the app from + // making changes to these fields. private int mUserLockedFields; private boolean mVibrationEnabled; private boolean mShowBadge = DEFAULT_SHOW_BADGE; private boolean mDeleted = DEFAULT_DELETED; private String mGroup; private AudioAttributes mAudioAttributes = Notification.AUDIO_ATTRIBUTES_DEFAULT; + // If this is a blockable system notification channel. private boolean mBlockableSystem = false; /** @@ -565,14 +574,35 @@ public final class NotificationChannel implements Parcelable { /** * @hide */ + public void populateFromXmlForRestore(XmlPullParser parser, Context context) { + populateFromXml(parser, true, context); + } + + /** + * @hide + */ @SystemApi public void populateFromXml(XmlPullParser parser) { + populateFromXml(parser, false, null); + } + + /** + * If {@param forRestore} is true, {@param Context} MUST be non-null. + */ + private void populateFromXml(XmlPullParser parser, boolean forRestore, + @Nullable Context context) { + Preconditions.checkArgument(!forRestore || context != null, + "forRestore is true but got null context"); + // Name, id, and importance are set in the constructor. setDescription(parser.getAttributeValue(null, ATT_DESC)); setBypassDnd(Notification.PRIORITY_DEFAULT != safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT)); setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY)); - setSound(safeUri(parser, ATT_SOUND), safeAudioAttributes(parser)); + + Uri sound = safeUri(parser, ATT_SOUND); + setSound(forRestore ? restoreSoundUri(context, sound) : sound, safeAudioAttributes(parser)); + enableLights(safeBool(parser, ATT_LIGHTS, false)); setLightColor(safeInt(parser, ATT_LIGHT_COLOR, DEFAULT_LIGHT_COLOR)); setVibrationPattern(safeLongArray(parser, ATT_VIBRATION, null)); @@ -584,11 +614,62 @@ public final class NotificationChannel implements Parcelable { setBlockableSystem(safeBool(parser, ATT_BLOCKABLE_SYSTEM, false)); } + @Nullable + private Uri restoreSoundUri(Context context, @Nullable Uri uri) { + if (uri == null) { + return null; + } + ContentResolver contentResolver = context.getContentResolver(); + // There are backups out there with uncanonical uris (because we fixed this after + // shipping). If uncanonical uris are given to MediaProvider.uncanonicalize it won't + // verify the uri against device storage and we'll possibly end up with a broken uri. + // We then canonicalize the uri to uncanonicalize it back, which means we properly check + // the uri and in the case of not having the resource we end up with the default - better + // than broken. As a side effect we'll canonicalize already canonicalized uris, this is fine + // according to the docs because canonicalize method has to handle canonical uris as well. + Uri canonicalizedUri = contentResolver.canonicalize(uri); + if (canonicalizedUri == null) { + // We got a null because the uri in the backup does not exist here, so we return default + return Settings.System.DEFAULT_NOTIFICATION_URI; + } + return contentResolver.uncanonicalize(canonicalizedUri); + } + /** * @hide */ @SystemApi public void writeXml(XmlSerializer out) throws IOException { + writeXml(out, false, null); + } + + /** + * @hide + */ + public void writeXmlForBackup(XmlSerializer out, Context context) throws IOException { + writeXml(out, true, context); + } + + private Uri getSoundForBackup(Context context) { + Uri sound = getSound(); + if (sound == null) { + return null; + } + Uri canonicalSound = context.getContentResolver().canonicalize(sound); + if (canonicalSound == null) { + // The content provider does not support canonical uris so we backup the default + return Settings.System.DEFAULT_NOTIFICATION_URI; + } + return canonicalSound; + } + + /** + * If {@param forBackup} is true, {@param Context} MUST be non-null. + */ + private void writeXml(XmlSerializer out, boolean forBackup, @Nullable Context context) + throws IOException { + Preconditions.checkArgument(!forBackup || context != null, + "forBackup is true but got null context"); out.startTag(null, TAG_CHANNEL); out.attribute(null, ATT_ID, getId()); if (getName() != null) { @@ -609,8 +690,9 @@ public final class NotificationChannel implements Parcelable { out.attribute(null, ATT_VISIBILITY, Integer.toString(getLockscreenVisibility())); } - if (getSound() != null) { - out.attribute(null, ATT_SOUND, getSound().toString()); + Uri sound = forBackup ? getSoundForBackup(context) : getSound(); + if (sound != null) { + out.attribute(null, ATT_SOUND, sound.toString()); } if (getAudioAttributes() != null) { out.attribute(null, ATT_USAGE, Integer.toString(getAudioAttributes().getUsage())); @@ -850,4 +932,35 @@ public final class NotificationChannel implements Parcelable { + ", mBlockableSystem=" + mBlockableSystem + '}'; } + + /** @hide */ + public void toProto(ProtoOutputStream proto) { + proto.write(NotificationChannelProto.ID, mId); + proto.write(NotificationChannelProto.NAME, mName); + proto.write(NotificationChannelProto.DESCRIPTION, mDesc); + proto.write(NotificationChannelProto.IMPORTANCE, mImportance); + proto.write(NotificationChannelProto.CAN_BYPASS_DND, mBypassDnd); + proto.write(NotificationChannelProto.LOCKSCREEN_VISIBILITY, mLockscreenVisibility); + if (mSound != null) { + proto.write(NotificationChannelProto.SOUND, mSound.toString()); + } + proto.write(NotificationChannelProto.USE_LIGHTS, mLights); + proto.write(NotificationChannelProto.LIGHT_COLOR, mLightColor); + if (mVibration != null) { + for (long v : mVibration) { + proto.write(NotificationChannelProto.VIBRATION, v); + } + } + proto.write(NotificationChannelProto.USER_LOCKED_FIELDS, mUserLockedFields); + proto.write(NotificationChannelProto.IS_VIBRATION_ENABLED, mVibrationEnabled); + proto.write(NotificationChannelProto.SHOW_BADGE, mShowBadge); + proto.write(NotificationChannelProto.IS_DELETED, mDeleted); + proto.write(NotificationChannelProto.GROUP, mGroup); + if (mAudioAttributes != null) { + long aToken = proto.start(NotificationChannelProto.AUDIO_ATTRIBUTES); + mAudioAttributes.toProto(proto); + proto.end(aToken); + } + proto.write(NotificationChannelProto.IS_BLOCKABLE_SYSTEM, mBlockableSystem); + } } diff --git a/android/app/NotificationChannelGroup.java b/android/app/NotificationChannelGroup.java index 51733114..5cb7fb7a 100644 --- a/android/app/NotificationChannelGroup.java +++ b/android/app/NotificationChannelGroup.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.proto.ProtoOutputStream; import org.json.JSONException; import org.json.JSONObject; @@ -295,4 +296,15 @@ public final class NotificationChannelGroup implements Parcelable { + ", mChannels=" + mChannels + '}'; } + + /** @hide */ + public void toProto(ProtoOutputStream proto) { + proto.write(NotificationChannelGroupProto.ID, mId); + proto.write(NotificationChannelGroupProto.NAME, mName.toString()); + proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription); + proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked); + for (NotificationChannel channel : mChannels) { + channel.toProto(proto); + } + } } diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java index 8fa7d6c3..eb52cb7f 100644 --- a/android/app/NotificationManager.java +++ b/android/app/NotificationManager.java @@ -41,6 +41,7 @@ import android.provider.Settings.Global; import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.util.Log; +import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1061,6 +1062,27 @@ public class NotificationManager { + "]"; } + /** @hide */ + public void toProto(ProtoOutputStream proto, long fieldId) { + final long pToken = proto.start(fieldId); + + bitwiseToProtoEnum(proto, PolicyProto.PRIORITY_CATEGORIES, priorityCategories); + proto.write(PolicyProto.PRIORITY_CALL_SENDER, priorityCallSenders); + proto.write(PolicyProto.PRIORITY_MESSAGE_SENDER, priorityMessageSenders); + bitwiseToProtoEnum( + proto, PolicyProto.SUPPRESSED_VISUAL_EFFECTS, suppressedVisualEffects); + + proto.end(pToken); + } + + private static void bitwiseToProtoEnum(ProtoOutputStream proto, long fieldId, int data) { + for (int i = 1; data > 0; ++i, data >>>= 1) { + if ((data & 1) == 1) { + proto.write(fieldId, i); + } + } + } + public static String suppressedEffectsToString(int effects) { if (effects <= 0) return ""; final StringBuilder sb = new StringBuilder(); diff --git a/android/app/SharedElementCallback.java b/android/app/SharedElementCallback.java index af13e695..80fb8058 100644 --- a/android/app/SharedElementCallback.java +++ b/android/app/SharedElementCallback.java @@ -27,6 +27,7 @@ import android.os.Bundle; import android.os.Parcelable; import android.transition.TransitionUtils; import android.view.View; +import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ImageView.ScaleType; @@ -176,7 +177,7 @@ public abstract class SharedElementCallback { Drawable d = imageView.getDrawable(); Drawable bg = imageView.getBackground(); if (d != null && (bg == null || bg.getAlpha() == 0)) { - Bitmap bitmap = TransitionUtils.createDrawableBitmap(d); + Bitmap bitmap = TransitionUtils.createDrawableBitmap(d, imageView); if (bitmap != null) { Bundle bundle = new Bundle(); if (bitmap.getConfig() != Bitmap.Config.HARDWARE) { @@ -202,7 +203,8 @@ public abstract class SharedElementCallback { } else { mTempMatrix.set(viewToGlobalMatrix); } - return TransitionUtils.createViewBitmap(sharedElement, mTempMatrix, screenBounds); + ViewGroup parent = (ViewGroup) sharedElement.getParent(); + return TransitionUtils.createViewBitmap(sharedElement, mTempMatrix, screenBounds, parent); } /** diff --git a/android/app/StatusBarManager.java b/android/app/StatusBarManager.java index 4a092140..8987bc02 100644 --- a/android/app/StatusBarManager.java +++ b/android/app/StatusBarManager.java @@ -14,15 +14,14 @@ * limitations under the License. */ - package android.app; import android.annotation.IntDef; import android.annotation.SystemService; import android.content.Context; import android.os.Binder; -import android.os.RemoteException; import android.os.IBinder; +import android.os.RemoteException; import android.os.ServiceManager; import android.util.Slog; import android.view.View; @@ -71,14 +70,18 @@ public class StatusBarManager { * Setting this flag disables quick settings completely, but does not disable expanding the * notification shade. */ - public static final int DISABLE2_QUICK_SETTINGS = 0x00000001; + public static final int DISABLE2_QUICK_SETTINGS = 1; + public static final int DISABLE2_SYSTEM_ICONS = 1 << 1; + public static final int DISABLE2_NOTIFICATION_SHADE = 1 << 2; public static final int DISABLE2_NONE = 0x00000000; - public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS; + public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS | DISABLE2_SYSTEM_ICONS + | DISABLE2_NOTIFICATION_SHADE; @IntDef(flag = true, - value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS}) + value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS, DISABLE2_SYSTEM_ICONS, + DISABLE2_NOTIFICATION_SHADE}) @Retention(RetentionPolicy.SOURCE) public @interface Disable2Flags {} diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java index ab70f0e7..50f1f364 100644 --- a/android/app/SystemServiceRegistry.java +++ b/android/app/SystemServiceRegistry.java @@ -81,10 +81,10 @@ import android.net.INetworkPolicyManager; import android.net.IpSecManager; import android.net.NetworkPolicyManager; import android.net.NetworkScoreManager; -import android.net.nsd.INsdManager; -import android.net.nsd.NsdManager; import android.net.lowpan.ILowpanManager; import android.net.lowpan.LowpanManager; +import android.net.nsd.INsdManager; +import android.net.nsd.NsdManager; import android.net.wifi.IRttManager; import android.net.wifi.IWifiManager; import android.net.wifi.IWifiScanner; @@ -95,6 +95,8 @@ import android.net.wifi.aware.IWifiAwareManager; import android.net.wifi.aware.WifiAwareManager; import android.net.wifi.p2p.IWifiP2pManager; import android.net.wifi.p2p.WifiP2pManager; +import android.net.wifi.rtt.IWifiRttManager; +import android.net.wifi.rtt.WifiRttManager; import android.nfc.NfcManager; import android.os.BatteryManager; import android.os.BatteryStats; @@ -603,6 +605,16 @@ final class SystemServiceRegistry { ConnectivityThread.getInstanceLooper()); }}); + registerService(Context.WIFI_RTT2_SERVICE, WifiRttManager.class, + new CachedServiceFetcher<WifiRttManager>() { + @Override + public WifiRttManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_RTT2_SERVICE); + IWifiRttManager service = IWifiRttManager.Stub.asInterface(b); + return new WifiRttManager(ctx.getOuterContext(), service); + }}); + registerService(Context.ETHERNET_SERVICE, EthernetManager.class, new CachedServiceFetcher<EthernetManager>() { @Override diff --git a/android/app/TaskStackListener.java b/android/app/TaskStackListener.java index a52ca0a6..402e2095 100644 --- a/android/app/TaskStackListener.java +++ b/android/app/TaskStackListener.java @@ -31,7 +31,8 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override - public void onActivityPinned(String packageName, int taskId) throws RemoteException { + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) + throws RemoteException { } @Override diff --git a/android/app/VrManager.java b/android/app/VrManager.java index 363e20a7..5c6ffa39 100644 --- a/android/app/VrManager.java +++ b/android/app/VrManager.java @@ -4,6 +4,7 @@ import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.content.ComponentName; import android.content.Context; import android.os.Handler; import android.os.RemoteException; @@ -62,7 +63,10 @@ public class VrManager { * @param callback The callback to register. * @hide */ - @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + @RequiresPermission(anyOf = { + android.Manifest.permission.RESTRICTED_VR_ACCESS, + android.Manifest.permission.ACCESS_VR_STATE + }) public void registerVrStateCallback(VrStateCallback callback, @NonNull Handler handler) { if (callback == null || mCallbackMap.containsKey(callback)) { return; @@ -88,7 +92,10 @@ public class VrManager { * @param callback The callback to deregister. * @hide */ - @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + @RequiresPermission(anyOf = { + android.Manifest.permission.RESTRICTED_VR_ACCESS, + android.Manifest.permission.ACCESS_VR_STATE + }) public void unregisterVrStateCallback(VrStateCallback callback) { CallbackEntry entry = mCallbackMap.remove(callback); if (entry != null) { @@ -110,7 +117,10 @@ public class VrManager { * Returns the current VrMode state. * @hide */ - @RequiresPermission(android.Manifest.permission.ACCESS_VR_STATE) + @RequiresPermission(anyOf = { + android.Manifest.permission.RESTRICTED_VR_ACCESS, + android.Manifest.permission.ACCESS_VR_STATE + }) public boolean getVrModeEnabled() { try { return mService.getVrModeState(); @@ -124,7 +134,10 @@ public class VrManager { * Returns the current VrMode state. * @hide */ - @RequiresPermission(android.Manifest.permission.ACCESS_VR_STATE) + @RequiresPermission(anyOf = { + android.Manifest.permission.RESTRICTED_VR_ACCESS, + android.Manifest.permission.ACCESS_VR_STATE + }) public boolean getPersistentVrModeEnabled() { try { return mService.getPersistentVrModeEnabled(); @@ -169,4 +182,20 @@ public class VrManager { e.rethrowFromSystemServer(); } } + + /** + * Set the component name of the compositor service to bind. + * + * @param componentName ComponentName of a Service in the application's compositor process to + * bind to, or null to clear the current binding. + */ + @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS) + public void setAndBindVrCompositor(ComponentName componentName) { + try { + mService.setAndBindCompositor( + (componentName == null) ? null : componentName.flattenToString()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } } diff --git a/android/app/WindowConfiguration.java b/android/app/WindowConfiguration.java index 5d87e1c2..6b405384 100644 --- a/android/app/WindowConfiguration.java +++ b/android/app/WindowConfiguration.java @@ -17,6 +17,9 @@ package android.app; import static android.app.ActivityThread.isSystem; +import static android.app.WindowConfigurationProto.ACTIVITY_TYPE; +import static android.app.WindowConfigurationProto.APP_BOUNDS; +import static android.app.WindowConfigurationProto.WINDOWING_MODE; import android.annotation.IntDef; import android.annotation.NonNull; @@ -25,6 +28,7 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; import android.view.DisplayInfo; /** @@ -48,26 +52,33 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu /** The current windowing mode of the configuration. */ private @WindowingMode int mWindowingMode; - /** Windowing mode is currently not defined. - * @hide */ + /** Windowing mode is currently not defined. */ public static final int WINDOWING_MODE_UNDEFINED = 0; - /** Occupies the full area of the screen or the parent container. - * @hide */ + /** Occupies the full area of the screen or the parent container. */ public static final int WINDOWING_MODE_FULLSCREEN = 1; - /** Always on-top (always visible). of other siblings in its parent container. - * @hide */ + /** Always on-top (always visible). of other siblings in its parent container. */ public static final int WINDOWING_MODE_PINNED = 2; - /** The primary container driving the screen to be in split-screen mode. - * @hide */ + /** The primary container driving the screen to be in split-screen mode. */ public static final int WINDOWING_MODE_SPLIT_SCREEN_PRIMARY = 3; /** * The containers adjacent to the {@link #WINDOWING_MODE_SPLIT_SCREEN_PRIMARY} container in * split-screen mode. - * @hide + * NOTE: Containers launched with the windowing mode with APIs like + * {@link ActivityOptions#setLaunchWindowingMode(int)} will be launched in + * {@link #WINDOWING_MODE_FULLSCREEN} if the display isn't currently in split-screen windowing + * mode + * @see #WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY */ public static final int WINDOWING_MODE_SPLIT_SCREEN_SECONDARY = 4; - /** Can be freely resized within its parent container. - * @hide */ + /** + * Alias for {@link #WINDOWING_MODE_SPLIT_SCREEN_SECONDARY} that makes it clear that the usage + * points for APIs like {@link ActivityOptions#setLaunchWindowingMode(int)} that the container + * will launch into fullscreen or split-screen secondary depending on if the device is currently + * in fullscreen mode or split-screen mode. + */ + public static final int WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY = + WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; + /** Can be freely resized within its parent container. */ public static final int WINDOWING_MODE_FREEFORM = 5; /** @hide */ @@ -77,6 +88,7 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu WINDOWING_MODE_PINNED, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, + WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY, WINDOWING_MODE_FREEFORM, }) public @interface WindowingMode {} @@ -84,18 +96,15 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu /** The current activity type of the configuration. */ private @ActivityType int mActivityType; - /** Activity type is currently not defined. - * @hide */ + /** Activity type is currently not defined. */ public static final int ACTIVITY_TYPE_UNDEFINED = 0; - /** Standard activity type. Nothing special about the activity... - * @hide */ + /** Standard activity type. Nothing special about the activity... */ public static final int ACTIVITY_TYPE_STANDARD = 1; /** Home/Launcher activity type. */ public static final int ACTIVITY_TYPE_HOME = 2; /** Recents/Overview activity type. */ public static final int ACTIVITY_TYPE_RECENTS = 3; - /** Assistant activity type. - * @hide */ + /** Assistant activity type. */ public static final int ACTIVITY_TYPE_ASSISTANT = 4; /** @hide */ @@ -127,7 +136,6 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu }) public @interface WindowConfig {} - /** @hide */ public WindowConfiguration() { unset(); } @@ -176,7 +184,6 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu * Set {@link #mAppBounds} to the input Rect. * @param rect The rect value to set {@link #mAppBounds} to. * @see #getAppBounds() - * @hide */ public void setAppBounds(Rect rect) { if (rect == null) { @@ -200,26 +207,20 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu mAppBounds.set(left, top, right, bottom); } - /** - * @see #setAppBounds(Rect) - * @hide - */ + /** @see #setAppBounds(Rect) */ public Rect getAppBounds() { return mAppBounds; } - /** @hide */ public void setWindowingMode(@WindowingMode int windowingMode) { mWindowingMode = windowingMode; } - /** @hide */ @WindowingMode public int getWindowingMode() { return mWindowingMode; } - /** @hide */ public void setActivityType(@ActivityType int activityType) { if (mActivityType == activityType) { return; @@ -237,13 +238,11 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu mActivityType = activityType; } - /** @hide */ @ActivityType public int getActivityType() { return mActivityType; } - /** @hide */ public void setTo(WindowConfiguration other) { setAppBounds(other.mAppBounds); setWindowingMode(other.mWindowingMode); @@ -382,6 +381,24 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu } /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.app.WindowConfigurationProto} + * + * @param protoOutputStream Stream to write the WindowConfiguration object to. + * @param fieldId Field Id of the WindowConfiguration as defined in the parent message + * @hide + */ + public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + final long token = protoOutputStream.start(fieldId); + if (mAppBounds != null) { + mAppBounds.writeToProto(protoOutputStream, APP_BOUNDS); + } + protoOutputStream.write(WINDOWING_MODE, mWindowingMode); + protoOutputStream.write(ACTIVITY_TYPE, mActivityType); + protoOutputStream.end(token); + } + + /** * Returns true if the activities associated with this window configuration display a shadow * around their border. * @hide @@ -483,10 +500,15 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu * @hide */ public boolean supportSplitScreenWindowingMode() { - if (mActivityType == ACTIVITY_TYPE_ASSISTANT) { + return supportSplitScreenWindowingMode(mWindowingMode, mActivityType); + } + + /** @hide */ + public static boolean supportSplitScreenWindowingMode(int windowingMode, int activityType) { + if (activityType == ACTIVITY_TYPE_ASSISTANT) { return false; } - return mWindowingMode != WINDOWING_MODE_FREEFORM && mWindowingMode != WINDOWING_MODE_PINNED; + return windowingMode != WINDOWING_MODE_FREEFORM && windowingMode != WINDOWING_MODE_PINNED; } private static String windowingModeToString(@WindowingMode int windowingMode) { diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java index 6bccad9f..3c530633 100644 --- a/android/app/admin/DevicePolicyManager.java +++ b/android/app/admin/DevicePolicyManager.java @@ -63,7 +63,9 @@ import android.telephony.TelephonyManager; import android.util.ArraySet; import android.util.Log; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; import com.android.org.conscrypt.TrustedCertificateStore; import java.io.ByteArrayInputStream; @@ -3142,6 +3144,7 @@ public class DevicePolicyManager { */ public static final int WIPE_EUICC = 0x0004; + /** * Ask that all user data be wiped. If called as a secondary user, the user will be removed and * other users will remain unaffected. Calling from the primary user will cause the device to @@ -3157,10 +3160,47 @@ public class DevicePolicyManager { * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} */ public void wipeData(int flags) { - throwIfParentInstance("wipeData"); + final String wipeReasonForUser = mContext.getString( + R.string.work_profile_deleted_description_dpm_wipe); + wipeDataInternal(flags, wipeReasonForUser); + } + + /** + * Ask that all user data be wiped. If called as a secondary user, the user will be removed and + * other users will remain unaffected, the provided reason for wiping data can be shown to + * user. Calling from the primary user will cause the device to reboot, erasing all device data + * - including all the secondary users and their data - while booting up. In this case, we don't + * show the reason to the user since the device would be factory reset. + * <p> + * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to + * be able to call this method; if it has not, a security exception will be thrown. + * + * @param flags Bit mask of additional options: currently supported flags are + * {@link #WIPE_EXTERNAL_STORAGE} and {@link #WIPE_RESET_PROTECTION_DATA}. + * @param reason a string that contains the reason for wiping data, which can be + * presented to the user. + * @throws SecurityException if the calling application does not own an active administrator + * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} + * @throws IllegalArgumentException if the input reason string is null or empty. + */ + public void wipeDataWithReason(int flags, @NonNull CharSequence reason) { + Preconditions.checkNotNull(reason, "CharSequence is null"); + wipeDataInternal(flags, reason.toString()); + } + + /** + * Internal function for both {@link #wipeData(int)} and + * {@link #wipeDataWithReason(int, CharSequence)} to call. + * + * @see #wipeData(int) + * @see #wipeDataWithReason(int, CharSequence) + * @hide + */ + private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser) { + throwIfParentInstance("wipeDataWithReason"); if (mService != null) { try { - mService.wipeData(flags); + mService.wipeDataWithReason(flags, wipeReasonForUser); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -7534,12 +7574,12 @@ public class DevicePolicyManager { } /** - * Called by the device owner or profile owner to set the name of the organization under - * management. - * <p> - * If the organization name needs to be localized, it is the responsibility of the - * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast - * and set a new version of this string accordingly. + * Called by the device owner (since API 26) or profile owner (since API 24) to set the name of + * the organization under management. + * + * <p>If the organization name needs to be localized, it is the responsibility of the {@link + * DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast and set + * a new version of this string accordingly. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param title The organization name or {@code null} to clear a previously set name. diff --git a/android/app/assist/AssistStructure.java b/android/app/assist/AssistStructure.java index 55c22de5..d9b7cd7e 100644 --- a/android/app/assist/AssistStructure.java +++ b/android/app/assist/AssistStructure.java @@ -674,6 +674,7 @@ public class AssistStructure implements Parcelable { ViewNodeText mText; int mInputType; + String mWebScheme; String mWebDomain; Bundle mExtras; LocaleList mLocaleList; @@ -751,6 +752,7 @@ public class AssistStructure implements Parcelable { mInputType = in.readInt(); } if ((flags&FLAGS_HAS_URL) != 0) { + mWebScheme = in.readString(); mWebDomain = in.readString(); } if ((flags&FLAGS_HAS_LOCALE_LIST) != 0) { @@ -813,7 +815,7 @@ public class AssistStructure implements Parcelable { if (mInputType != 0) { flags |= FLAGS_HAS_INPUT_TYPE; } - if (mWebDomain != null) { + if (mWebScheme != null || mWebDomain != null) { flags |= FLAGS_HAS_URL; } if (mLocaleList != null) { @@ -908,6 +910,7 @@ public class AssistStructure implements Parcelable { out.writeInt(mInputType); } if ((flags&FLAGS_HAS_URL) != 0) { + out.writeString(mWebScheme); out.writeString(mWebDomain); } if ((flags&FLAGS_HAS_LOCALE_LIST) != 0) { @@ -1260,18 +1263,31 @@ public class AssistStructure implements Parcelable { * <p>Typically used when the view associated with the view is a container for an HTML * document. * - * <strong>WARNING:</strong> a {@link android.service.autofill.AutofillService} should only - * use this domain for autofill purposes when it trusts the app generating it (i.e., the app - * defined by {@link AssistStructure#getActivityComponent()}). + * <p><b>Warning:</b> an autofill service cannot trust the value reported by this method + * without verifing its authenticity—see the "Web security" section of + * {@link android.service.autofill.AutofillService} for more details. * * @return domain-only part of the document. For example, if the full URL is - * {@code http://my.site/login?user=my_user}, it returns {@code my.site}. + * {@code https://example.com/login?user=my_user}, it returns {@code example.com}. */ @Nullable public String getWebDomain() { return mWebDomain; } /** + * Returns the scheme of the HTML document represented by this view. + * + * <p>Typically used when the view associated with the view is a container for an HTML + * document. + * + * @return scheme-only part of the document. For example, if the full URL is + * {@code https://example.com/login?user=my_user}, it returns {@code https}. + */ + @Nullable public String getWebScheme() { + return mWebScheme; + } + + /** * Returns the HTML properties associated with this view. * * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes, @@ -1767,10 +1783,13 @@ public class AssistStructure implements Parcelable { @Override public void setWebDomain(@Nullable String domain) { if (domain == null) { + mNode.mWebScheme = null; mNode.mWebDomain = null; return; } - mNode.mWebDomain = Uri.parse(domain).getHost(); + Uri uri = Uri.parse(domain); + mNode.mWebScheme = uri.getScheme(); + mNode.mWebDomain = uri.getHost(); } @Override diff --git a/android/app/job/JobInfo.java b/android/app/job/JobInfo.java index 87e516ca..1434c9ba 100644 --- a/android/app/job/JobInfo.java +++ b/android/app/job/JobInfo.java @@ -317,7 +317,8 @@ public class JobInfo implements Parcelable { } /** - * Whether this job needs the device to be plugged in. + * Whether this job requires that the device be charging (or be a non-battery-powered + * device connected to permanent power, such as Android TV devices). */ public boolean isRequireCharging() { return (constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0; @@ -331,7 +332,10 @@ public class JobInfo implements Parcelable { } /** - * Whether this job needs the device to be in an Idle maintenance window. + * Whether this job requires that the user <em>not</em> be interacting with the device. + * + * <p class="note">This is <em>not</em> the same as "doze" or "device idle"; + * it is purely about the user's direct interactions.</p> */ public boolean isRequireDeviceIdle() { return (constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0; @@ -918,9 +922,19 @@ public class JobInfo implements Parcelable { } /** - * Specify that to run this job, the device needs to be plugged in. This defaults to - * false. - * @param requiresCharging Whether or not the device is plugged in. + * Specify that to run this job, the device must be charging (or be a + * non-battery-powered device connected to permanent power, such as Android TV + * devices). This defaults to {@code false}. + * + * <p class="note">For purposes of running jobs, a battery-powered device + * "charging" is not quite the same as simply being connected to power. If the + * device is so busy that the battery is draining despite a power connection, jobs + * with this constraint will <em>not</em> run. This can happen during some + * common use cases such as video chat, particularly if the device is plugged in + * to USB rather than to wall power. + * + * @param requiresCharging Pass {@code true} to require that the device be + * charging in order to run the job. */ public Builder setRequiresCharging(boolean requiresCharging) { mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_CHARGING) @@ -942,14 +956,22 @@ public class JobInfo implements Parcelable { } /** - * Specify that to run, the job needs the device to be in idle mode. This defaults to - * false. - * <p>Idle mode is a loose definition provided by the system, which means that the device - * is not in use, and has not been in use for some time. As such, it is a good time to - * perform resource heavy jobs. Bear in mind that battery usage will still be attributed - * to your application, and surfaced to the user in battery stats.</p> - * @param requiresDeviceIdle Whether or not the device need be within an idle maintenance - * window. + * When set {@code true}, ensure that this job will not run if the device is in active use. + * The default state is {@code false}: that is, the for the job to be runnable even when + * someone is interacting with the device. + * + * <p>This state is a loose definition provided by the system. In general, it means that + * the device is not currently being used interactively, and has not been in use for some + * time. As such, it is a good time to perform resource heavy jobs. Bear in mind that + * battery usage will still be attributed to your application, and surfaced to the user in + * battery stats.</p> + * + * <p class="note">Despite the similar naming, this job constraint is <em>not</em> + * related to the system's "device idle" or "doze" states. This constraint only + * determines whether a job is allowed to run while the device is directly in use. + * + * @param requiresDeviceIdle Pass {@code true} to prevent the job from running + * while the device is being used interactively. */ public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) { mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_DEVICE_IDLE) diff --git a/android/app/timezone/RulesUpdaterContract.java b/android/app/timezone/RulesUpdaterContract.java index 9c62f46b..74ed6588 100644 --- a/android/app/timezone/RulesUpdaterContract.java +++ b/android/app/timezone/RulesUpdaterContract.java @@ -51,7 +51,7 @@ public final class RulesUpdaterContract { * applies. */ public static final String ACTION_TRIGGER_RULES_UPDATE_CHECK = - "android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK"; + "com.android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK"; /** * The extra containing the {@code byte[]} that should be passed to @@ -61,7 +61,7 @@ public final class RulesUpdaterContract { * {@link #ACTION_TRIGGER_RULES_UPDATE_CHECK} intent has been processed. */ public static final String EXTRA_CHECK_TOKEN = - "android.intent.extra.timezone.CHECK_TOKEN"; + "com.android.intent.extra.timezone.CHECK_TOKEN"; /** * Creates an intent that would trigger a time zone rules update check. @@ -83,8 +83,7 @@ public final class RulesUpdaterContract { Intent intent = createUpdaterIntent(updaterAppPackageName); intent.putExtra(EXTRA_CHECK_TOKEN, checkTokenBytes); context.sendBroadcastAsUser( - intent, - UserHandle.of(UserHandle.myUserId()), + intent, UserHandle.SYSTEM, RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION); } } diff --git a/android/appwidget/AppWidgetHostView.java b/android/appwidget/AppWidgetHostView.java index 8a1eae2d..dc9970a7 100644 --- a/android/appwidget/AppWidgetHostView.java +++ b/android/appwidget/AppWidgetHostView.java @@ -23,17 +23,12 @@ import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; -import android.os.Parcel; import android.os.Parcelable; -import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -58,17 +53,17 @@ import java.util.concurrent.Executor; * {@link RemoteViews}. */ public class AppWidgetHostView extends FrameLayout { + static final String TAG = "AppWidgetHostView"; + private static final String KEY_JAILED_ARRAY = "jail"; + static final boolean LOGD = false; - static final boolean CROSSFADE = false; static final int VIEW_MODE_NOINIT = 0; static final int VIEW_MODE_CONTENT = 1; static final int VIEW_MODE_ERROR = 2; static final int VIEW_MODE_DEFAULT = 3; - static final int FADE_DURATION = 1000; - // When we're inflating the initialLayout for a AppWidget, we only allow // views that are allowed in RemoteViews. static final LayoutInflater.Filter sInflaterFilter = new LayoutInflater.Filter() { @@ -85,9 +80,6 @@ public class AppWidgetHostView extends FrameLayout { View mView; int mViewMode = VIEW_MODE_NOINIT; int mLayoutId = -1; - long mFadeStartTime = -1; - Bitmap mOld; - Paint mOldPaint = new Paint(); private OnClickHandler mOnClickHandler; private Executor mAsyncExecutor; @@ -212,9 +204,12 @@ public class AppWidgetHostView extends FrameLayout { @Override protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { - final ParcelableSparseArray jail = new ParcelableSparseArray(); + final SparseArray<Parcelable> jail = new SparseArray<>(); super.dispatchSaveInstanceState(jail); - container.put(generateId(), jail); + + Bundle bundle = new Bundle(); + bundle.putSparseParcelableArray(KEY_JAILED_ARRAY, jail); + container.put(generateId(), bundle); } private int generateId() { @@ -226,12 +221,12 @@ public class AppWidgetHostView extends FrameLayout { protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { final Parcelable parcelable = container.get(generateId()); - ParcelableSparseArray jail = null; - if (parcelable != null && parcelable instanceof ParcelableSparseArray) { - jail = (ParcelableSparseArray) parcelable; + SparseArray<Parcelable> jail = null; + if (parcelable instanceof Bundle) { + jail = ((Bundle) parcelable).getSparseParcelableArray(KEY_JAILED_ARRAY); } - if (jail == null) jail = new ParcelableSparseArray(); + if (jail == null) jail = new SparseArray<>(); try { super.dispatchRestoreInstanceState(jail); @@ -383,31 +378,10 @@ public class AppWidgetHostView extends FrameLayout { * @hide */ protected void applyRemoteViews(RemoteViews remoteViews, boolean useAsyncIfPossible) { - if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); - boolean recycled = false; View content = null; Exception exception = null; - // Capture the old view into a bitmap so we can do the crossfade. - if (CROSSFADE) { - if (mFadeStartTime < 0) { - if (mView != null) { - final int width = mView.getWidth(); - final int height = mView.getHeight(); - try { - mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - } catch (OutOfMemoryError e) { - // we just won't do the fade - mOld = null; - } - if (mOld != null) { - //mView.drawIntoBitmap(mOld); - } - } - } - } - if (mLastExecutionSignal != null) { mLastExecutionSignal.cancel(); mLastExecutionSignal = null; @@ -484,16 +458,6 @@ public class AppWidgetHostView extends FrameLayout { removeView(mView); mView = content; } - - if (CROSSFADE) { - if (mFadeStartTime < 0) { - // if there is already an animation in progress, don't do anything -- - // the new view will pop in on top of the old one during the cross fade, - // and that looks okay. - mFadeStartTime = SystemClock.uptimeMillis(); - invalidate(); - } - } } private void updateContentDescription(AppWidgetProviderInfo info) { @@ -617,45 +581,6 @@ public class AppWidgetHostView extends FrameLayout { } } - @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - if (CROSSFADE) { - int alpha; - int l = child.getLeft(); - int t = child.getTop(); - if (mFadeStartTime > 0) { - alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); - if (alpha > 255) { - alpha = 255; - } - Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t - + " w=" + child.getWidth()); - if (alpha != 255 && mOld != null) { - mOldPaint.setAlpha(255-alpha); - //canvas.drawBitmap(mOld, l, t, mOldPaint); - } - } else { - alpha = 255; - } - int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, - Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); - boolean rv = super.drawChild(canvas, child, drawingTime); - canvas.restoreToCount(restoreTo); - if (alpha < 255) { - invalidate(); - } else { - mFadeStartTime = -1; - if (mOld != null) { - mOld.recycle(); - mOld = null; - } - } - return rv; - } else { - return super.drawChild(canvas, child, drawingTime); - } - } - /** * Prepare the given view to be shown. This might include adjusting * {@link FrameLayout.LayoutParams} before inserting. @@ -740,36 +665,4 @@ public class AppWidgetHostView extends FrameLayout { super.onInitializeAccessibilityNodeInfoInternal(info); info.setClassName(AppWidgetHostView.class.getName()); } - - private static class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable { - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - final int count = size(); - dest.writeInt(count); - for (int i = 0; i < count; i++) { - dest.writeInt(keyAt(i)); - dest.writeParcelable(valueAt(i), 0); - } - } - - public static final Parcelable.Creator<ParcelableSparseArray> CREATOR = - new Parcelable.Creator<ParcelableSparseArray>() { - public ParcelableSparseArray createFromParcel(Parcel source) { - final ParcelableSparseArray array = new ParcelableSparseArray(); - final ClassLoader loader = array.getClass().getClassLoader(); - final int count = source.readInt(); - for (int i = 0; i < count; i++) { - array.put(source.readInt(), source.readParcelable(loader)); - } - return array; - } - - public ParcelableSparseArray[] newArray(int size) { - return new ParcelableSparseArray[size]; - } - }; - } } diff --git a/android/arch/core/executor/AppToolkitTaskExecutor.java b/android/arch/core/executor/ArchTaskExecutor.java index 7337f74a..2401a730 100644 --- a/android/arch/core/executor/AppToolkitTaskExecutor.java +++ b/android/arch/core/executor/ArchTaskExecutor.java @@ -29,8 +29,8 @@ import java.util.concurrent.Executor; * @hide This API is not final. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class AppToolkitTaskExecutor extends TaskExecutor { - private static volatile AppToolkitTaskExecutor sInstance; +public class ArchTaskExecutor extends TaskExecutor { + private static volatile ArchTaskExecutor sInstance; @NonNull private TaskExecutor mDelegate; @@ -54,7 +54,7 @@ public class AppToolkitTaskExecutor extends TaskExecutor { } }; - private AppToolkitTaskExecutor() { + private ArchTaskExecutor() { mDefaultTaskExecutor = new DefaultTaskExecutor(); mDelegate = mDefaultTaskExecutor; } @@ -62,15 +62,15 @@ public class AppToolkitTaskExecutor extends TaskExecutor { /** * Returns an instance of the task executor. * - * @return The singleton AppToolkitTaskExecutor. + * @return The singleton ArchTaskExecutor. */ - public static AppToolkitTaskExecutor getInstance() { + public static ArchTaskExecutor getInstance() { if (sInstance != null) { return sInstance; } - synchronized (AppToolkitTaskExecutor.class) { + synchronized (ArchTaskExecutor.class) { if (sInstance == null) { - sInstance = new AppToolkitTaskExecutor(); + sInstance = new ArchTaskExecutor(); } } return sInstance; diff --git a/android/arch/core/executor/JunitTaskExecutorRule.java b/android/arch/core/executor/JunitTaskExecutorRule.java index cd4f8f57..c3366f35 100644 --- a/android/arch/core/executor/JunitTaskExecutorRule.java +++ b/android/arch/core/executor/JunitTaskExecutorRule.java @@ -46,11 +46,11 @@ public class JunitTaskExecutorRule implements TestRule { } private void beforeStart() { - AppToolkitTaskExecutor.getInstance().setDelegate(mTaskExecutor); + ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor); } private void afterFinished() { - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } public TaskExecutor getTaskExecutor() { diff --git a/android/arch/core/executor/testing/CountingTaskExecutorRule.java b/android/arch/core/executor/testing/CountingTaskExecutorRule.java index ad930aa8..77133d5b 100644 --- a/android/arch/core/executor/testing/CountingTaskExecutorRule.java +++ b/android/arch/core/executor/testing/CountingTaskExecutorRule.java @@ -16,7 +16,7 @@ package android.arch.core.executor.testing; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.DefaultTaskExecutor; import android.os.SystemClock; @@ -39,7 +39,7 @@ public class CountingTaskExecutorRule extends TestWatcher { @Override protected void starting(Description description) { super.starting(description); - AppToolkitTaskExecutor.getInstance().setDelegate(new DefaultTaskExecutor() { + ArchTaskExecutor.getInstance().setDelegate(new DefaultTaskExecutor() { @Override public void executeOnDiskIO(Runnable runnable) { super.executeOnDiskIO(new CountingRunnable(runnable)); @@ -55,7 +55,7 @@ public class CountingTaskExecutorRule extends TestWatcher { @Override protected void finished(Description description) { super.finished(description); - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } private void increment() { diff --git a/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java b/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java index ad36b9bc..a6a5b2ee 100644 --- a/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java +++ b/android/arch/core/executor/testing/CountingTaskExecutorRuleTest.java @@ -19,7 +19,7 @@ package android.arch.core.executor.testing; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; @@ -115,13 +115,13 @@ public class CountingTaskExecutorRuleTest { private LatchRunnable runOnIO() { LatchRunnable latchRunnable = new LatchRunnable(); - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(latchRunnable); + ArchTaskExecutor.getInstance().executeOnDiskIO(latchRunnable); return latchRunnable; } private LatchRunnable runOnMain() { LatchRunnable latchRunnable = new LatchRunnable(); - AppToolkitTaskExecutor.getInstance().executeOnMainThread(latchRunnable); + ArchTaskExecutor.getInstance().executeOnMainThread(latchRunnable); return latchRunnable; } diff --git a/android/arch/core/executor/testing/InstantTaskExecutorRule.java b/android/arch/core/executor/testing/InstantTaskExecutorRule.java index 07dcf1fe..f88a3e3f 100644 --- a/android/arch/core/executor/testing/InstantTaskExecutorRule.java +++ b/android/arch/core/executor/testing/InstantTaskExecutorRule.java @@ -16,7 +16,7 @@ package android.arch.core.executor.testing; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.TaskExecutor; import org.junit.rules.TestWatcher; @@ -32,7 +32,7 @@ public class InstantTaskExecutorRule extends TestWatcher { @Override protected void starting(Description description) { super.starting(description); - AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() { + ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { @Override public void executeOnDiskIO(Runnable runnable) { runnable.run(); @@ -53,6 +53,6 @@ public class InstantTaskExecutorRule extends TestWatcher { @Override protected void finished(Description description) { super.finished(description); - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } } diff --git a/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java b/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java index 4345fd19..0fdcbfbb 100644 --- a/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java +++ b/android/arch/core/executor/testing/InstantTaskExecutorRuleTest.java @@ -18,7 +18,7 @@ package android.arch.core.executor.testing; import static org.junit.Assert.assertTrue; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import org.junit.Rule; import org.junit.Test; @@ -46,7 +46,7 @@ public class InstantTaskExecutorRuleTest { return null; } }); - AppToolkitTaskExecutor.getInstance().executeOnMainThread(check); + ArchTaskExecutor.getInstance().executeOnMainThread(check); check.get(1, TimeUnit.SECONDS); } @@ -60,7 +60,7 @@ public class InstantTaskExecutorRuleTest { return null; } }); - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(check); + ArchTaskExecutor.getInstance().executeOnDiskIO(check); check.get(1, TimeUnit.SECONDS); } } diff --git a/android/arch/lifecycle/ClassesInfoCache.java b/android/arch/lifecycle/ClassesInfoCache.java new file mode 100644 index 00000000..f077daed --- /dev/null +++ b/android/arch/lifecycle/ClassesInfoCache.java @@ -0,0 +1,237 @@ +/* + * 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.support.annotation.Nullable; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Reflection is expensive, so we cache information about methods + * for {@link ReflectiveGenericLifecycleObserver}, so it can call them, + * and for {@link Lifecycling} to determine which observer adapter to use. + */ +class ClassesInfoCache { + + static ClassesInfoCache sInstance = new ClassesInfoCache(); + + private static final int CALL_TYPE_NO_ARG = 0; + private static final int CALL_TYPE_PROVIDER = 1; + private static final int CALL_TYPE_PROVIDER_WITH_EVENT = 2; + + private final Map<Class, CallbackInfo> mCallbackMap = new HashMap<>(); + private final Map<Class, Boolean> mHasLifecycleMethods = new HashMap<>(); + + boolean hasLifecycleMethods(Class klass) { + if (mHasLifecycleMethods.containsKey(klass)) { + return mHasLifecycleMethods.get(klass); + } + + Method[] methods = klass.getDeclaredMethods(); + for (Method method : methods) { + OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class); + if (annotation != null) { + // Optimization for reflection, we know that this method is called + // when there is no generated adapter. But there are methods with @OnLifecycleEvent + // so we know that will use ReflectiveGenericLifecycleObserver, + // so we createInfo in advance. + // CreateInfo always initialize mHasLifecycleMethods for a class, so we don't do it + // here. + createInfo(klass, methods); + return true; + } + } + mHasLifecycleMethods.put(klass, false); + return false; + } + + CallbackInfo getInfo(Class klass) { + CallbackInfo existing = mCallbackMap.get(klass); + if (existing != null) { + return existing; + } + existing = createInfo(klass, null); + return existing; + } + + private void verifyAndPutHandler(Map<MethodReference, Lifecycle.Event> handlers, + MethodReference newHandler, Lifecycle.Event newEvent, Class klass) { + Lifecycle.Event event = handlers.get(newHandler); + if (event != null && newEvent != event) { + Method method = newHandler.mMethod; + throw new IllegalArgumentException( + "Method " + method.getName() + " in " + klass.getName() + + " already declared with different @OnLifecycleEvent value: previous" + + " value " + event + ", new value " + newEvent); + } + if (event == null) { + handlers.put(newHandler, newEvent); + } + } + + private CallbackInfo createInfo(Class klass, @Nullable Method[] declaredMethods) { + Class superclass = klass.getSuperclass(); + Map<MethodReference, Lifecycle.Event> handlerToEvent = new HashMap<>(); + if (superclass != null) { + CallbackInfo superInfo = getInfo(superclass); + if (superInfo != null) { + handlerToEvent.putAll(superInfo.mHandlerToEvent); + } + } + + Class[] interfaces = klass.getInterfaces(); + for (Class intrfc : interfaces) { + for (Map.Entry<MethodReference, Lifecycle.Event> entry : getInfo( + intrfc).mHandlerToEvent.entrySet()) { + verifyAndPutHandler(handlerToEvent, entry.getKey(), entry.getValue(), klass); + } + } + + Method[] methods = declaredMethods != null ? declaredMethods : klass.getDeclaredMethods(); + boolean hasLifecycleMethods = false; + for (Method method : methods) { + OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class); + if (annotation == null) { + continue; + } + hasLifecycleMethods = true; + Class<?>[] params = method.getParameterTypes(); + int callType = CALL_TYPE_NO_ARG; + if (params.length > 0) { + callType = CALL_TYPE_PROVIDER; + if (!params[0].isAssignableFrom(LifecycleOwner.class)) { + throw new IllegalArgumentException( + "invalid parameter type. Must be one and instanceof LifecycleOwner"); + } + } + Lifecycle.Event event = annotation.value(); + + if (params.length > 1) { + callType = CALL_TYPE_PROVIDER_WITH_EVENT; + if (!params[1].isAssignableFrom(Lifecycle.Event.class)) { + throw new IllegalArgumentException( + "invalid parameter type. second arg must be an event"); + } + if (event != Lifecycle.Event.ON_ANY) { + throw new IllegalArgumentException( + "Second arg is supported only for ON_ANY value"); + } + } + if (params.length > 2) { + throw new IllegalArgumentException("cannot have more than 2 params"); + } + MethodReference methodReference = new MethodReference(callType, method); + verifyAndPutHandler(handlerToEvent, methodReference, event, klass); + } + CallbackInfo info = new CallbackInfo(handlerToEvent); + mCallbackMap.put(klass, info); + mHasLifecycleMethods.put(klass, hasLifecycleMethods); + return info; + } + + @SuppressWarnings("WeakerAccess") + static class CallbackInfo { + final Map<Lifecycle.Event, List<MethodReference>> mEventToHandlers; + final Map<MethodReference, Lifecycle.Event> mHandlerToEvent; + + CallbackInfo(Map<MethodReference, Lifecycle.Event> handlerToEvent) { + mHandlerToEvent = handlerToEvent; + mEventToHandlers = new HashMap<>(); + for (Map.Entry<MethodReference, Lifecycle.Event> entry : handlerToEvent.entrySet()) { + Lifecycle.Event event = entry.getValue(); + List<MethodReference> methodReferences = mEventToHandlers.get(event); + if (methodReferences == null) { + methodReferences = new ArrayList<>(); + mEventToHandlers.put(event, methodReferences); + } + methodReferences.add(entry.getKey()); + } + } + + @SuppressWarnings("ConstantConditions") + void invokeCallbacks(LifecycleOwner source, Lifecycle.Event event, Object target) { + invokeMethodsForEvent(mEventToHandlers.get(event), source, event, target); + invokeMethodsForEvent(mEventToHandlers.get(Lifecycle.Event.ON_ANY), source, event, + target); + } + + private static void invokeMethodsForEvent(List<MethodReference> handlers, + LifecycleOwner source, Lifecycle.Event event, Object mWrapped) { + if (handlers != null) { + for (int i = handlers.size() - 1; i >= 0; i--) { + handlers.get(i).invokeCallback(source, event, mWrapped); + } + } + } + } + + @SuppressWarnings("WeakerAccess") + static class MethodReference { + final int mCallType; + final Method mMethod; + + MethodReference(int callType, Method method) { + mCallType = callType; + mMethod = method; + mMethod.setAccessible(true); + } + + void invokeCallback(LifecycleOwner source, Lifecycle.Event event, Object target) { + //noinspection TryWithIdenticalCatches + try { + switch (mCallType) { + case CALL_TYPE_NO_ARG: + mMethod.invoke(target); + break; + case CALL_TYPE_PROVIDER: + mMethod.invoke(target, source); + break; + case CALL_TYPE_PROVIDER_WITH_EVENT: + mMethod.invoke(target, source, event); + break; + } + } catch (InvocationTargetException e) { + throw new RuntimeException("Failed to call observer method", e.getCause()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MethodReference that = (MethodReference) o; + return mCallType == that.mCallType && mMethod.getName().equals(that.mMethod.getName()); + } + + @Override + public int hashCode() { + return 31 * mCallType + mMethod.getName().hashCode(); + } + } +} diff --git a/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.java b/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.java new file mode 100644 index 00000000..e8cbe7ca --- /dev/null +++ b/android/arch/lifecycle/CompositeGeneratedAdaptersObserver.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.support.annotation.RestrictTo; + +/** + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class CompositeGeneratedAdaptersObserver implements GenericLifecycleObserver { + + private final GeneratedAdapter[] mGeneratedAdapters; + + CompositeGeneratedAdaptersObserver(GeneratedAdapter[] generatedAdapters) { + mGeneratedAdapters = generatedAdapters; + } + + @Override + public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { + MethodCallsLogger logger = new MethodCallsLogger(); + for (GeneratedAdapter mGenerated: mGeneratedAdapters) { + mGenerated.callMethods(source, event, false, logger); + } + for (GeneratedAdapter mGenerated: mGeneratedAdapters) { + mGenerated.callMethods(source, event, true, logger); + } + } +} diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java index fe18243f..f1352446 100644 --- a/android/arch/lifecycle/ComputableLiveData.java +++ b/android/arch/lifecycle/ComputableLiveData.java @@ -1,136 +1,9 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - +//ComputableLiveData interface for tests package android.arch.lifecycle; - -import android.arch.core.executor.AppToolkitTaskExecutor; -import android.support.annotation.MainThread; -import android.support.annotation.NonNull; -import android.support.annotation.RestrictTo; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A LiveData class that can be invalidated & computed on demand. - * <p> - * This is an internal class for now, might be public if we see the necessity. - * - * @param <T> The type of the live data - * @hide internal - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +import android.arch.lifecycle.LiveData; public abstract class ComputableLiveData<T> { - - private final LiveData<T> mLiveData; - - private AtomicBoolean mInvalid = new AtomicBoolean(true); - private AtomicBoolean mComputing = new AtomicBoolean(false); - - /** - * Creates a computable live data which is computed when there are active observers. - * <p> - * It can also be invalidated via {@link #invalidate()} which will result in a call to - * {@link #compute()} if there are active observers (or when they start observing) - */ - @SuppressWarnings("WeakerAccess") - public ComputableLiveData() { - mLiveData = new LiveData<T>() { - @Override - protected void onActive() { - // TODO if we make this class public, we should accept an executor - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); - } - }; - } - - /** - * Returns the LiveData managed by this class. - * - * @return A LiveData that is controlled by ComputableLiveData. - */ - @SuppressWarnings("WeakerAccess") - @NonNull - public LiveData<T> getLiveData() { - return mLiveData; - } - - @VisibleForTesting - final Runnable mRefreshRunnable = new Runnable() { - @WorkerThread - @Override - public void run() { - boolean computed; - do { - computed = false; - // compute can happen only in 1 thread but no reason to lock others. - if (mComputing.compareAndSet(false, true)) { - // as long as it is invalid, keep computing. - try { - T value = null; - while (mInvalid.compareAndSet(true, false)) { - computed = true; - value = compute(); - } - if (computed) { - mLiveData.postValue(value); - } - } finally { - // release compute lock - mComputing.set(false); - } - } - // check invalid after releasing compute lock to avoid the following scenario. - // Thread A runs compute() - // Thread A checks invalid, it is false - // Main thread sets invalid to true - // Thread B runs, fails to acquire compute lock and skips - // Thread A releases compute lock - // We've left invalid in set state. The check below recovers. - } while (computed && mInvalid.get()); - } - }; - - // invalidation check always happens on the main thread - @VisibleForTesting - final Runnable mInvalidationRunnable = new Runnable() { - @MainThread - @Override - public void run() { - boolean isActive = mLiveData.hasActiveObservers(); - if (mInvalid.compareAndSet(false, true)) { - if (isActive) { - // TODO if we make this class public, we should accept an executor. - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); - } - } - } - }; - - /** - * Invalidates the LiveData. - * <p> - * When there are active observers, this will trigger a call to {@link #compute()}. - */ - public void invalidate() { - AppToolkitTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable); - } - - @SuppressWarnings("WeakerAccess") - @WorkerThread - protected abstract T compute(); + public ComputableLiveData(){} + abstract protected T compute(); + public LiveData<T> getLiveData() {return null;} + public void invalidate() {} } diff --git a/android/arch/lifecycle/ComputableLiveDataTest.java b/android/arch/lifecycle/ComputableLiveDataTest.java index 0a3fbed6..eb89d8da 100644 --- a/android/arch/lifecycle/ComputableLiveDataTest.java +++ b/android/arch/lifecycle/ComputableLiveDataTest.java @@ -27,7 +27,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.TaskExecutor; import android.arch.core.executor.TaskExecutorWithFakeMainThread; import android.arch.lifecycle.util.InstantTaskExecutor; @@ -58,12 +58,12 @@ public class ComputableLiveDataTest { @Before public void swapExecutorDelegate() { mTaskExecutor = spy(new InstantTaskExecutor()); - AppToolkitTaskExecutor.getInstance().setDelegate(mTaskExecutor); + ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor); } @After public void removeExecutorDelegate() { - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } @Test @@ -76,7 +76,7 @@ public class ComputableLiveDataTest { @Test public void noConcurrentCompute() throws InterruptedException { TaskExecutorWithFakeMainThread executor = new TaskExecutorWithFakeMainThread(2); - AppToolkitTaskExecutor.getInstance().setDelegate(executor); + ArchTaskExecutor.getInstance().setDelegate(executor); try { // # of compute calls final Semaphore computeCounter = new Semaphore(0); @@ -121,7 +121,7 @@ public class ComputableLiveDataTest { // assert no other results arrive verify(observer, never()).onChanged(anyInt()); } finally { - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } } diff --git a/android/arch/lifecycle/DefaultLifecycleObserver.java b/android/arch/lifecycle/DefaultLifecycleObserver.java new file mode 100644 index 00000000..b6f468cf --- /dev/null +++ b/android/arch/lifecycle/DefaultLifecycleObserver.java @@ -0,0 +1,100 @@ +/* + * 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.support.annotation.NonNull; + +/** + * Callback interface for listening to {@link LifecycleOwner} state changes. + * <p> + * If you use Java 8 language, <b>always</b> prefer it over annotations. + */ +@SuppressWarnings("unused") +public interface DefaultLifecycleObserver extends FullLifecycleObserver { + + /** + * Notifies that {@code ON_CREATE} event occurred. + * <p> + * This method will be called after the {@link LifecycleOwner}'s {@code onCreate} + * method returns. + * + * @param owner the component, whose state was changed + */ + @Override + default void onCreate(@NonNull LifecycleOwner owner) { + } + + /** + * Notifies that {@code ON_START} event occurred. + * <p> + * This method will be called after the {@link LifecycleOwner}'s {@code onStart} method returns. + * + * @param owner the component, whose state was changed + */ + @Override + default void onStart(@NonNull LifecycleOwner owner) { + } + + /** + * Notifies that {@code ON_RESUME} event occurred. + * <p> + * This method will be called after the {@link LifecycleOwner}'s {@code onResume} + * method returns. + * + * @param owner the component, whose state was changed + */ + @Override + default void onResume(@NonNull LifecycleOwner owner) { + } + + /** + * Notifies that {@code ON_PAUSE} event occurred. + * <p> + * This method will be called before the {@link LifecycleOwner}'s {@code onPause} method + * is called. + * + * @param owner the component, whose state was changed + */ + @Override + default void onPause(@NonNull LifecycleOwner owner) { + } + + /** + * Notifies that {@code ON_STOP} event occurred. + * <p> + * This method will be called before the {@link LifecycleOwner}'s {@code onStop} method + * is called. + * + * @param owner the component, whose state was changed + */ + @Override + default void onStop(@NonNull LifecycleOwner owner) { + } + + /** + * Notifies that {@code ON_DESTROY} event occurred. + * <p> + * This method will be called before the {@link LifecycleOwner}'s {@code onStop} method + * is called. + * + * @param owner the component, whose state was changed + */ + @Override + default void onDestroy(@NonNull LifecycleOwner owner) { + } +} + diff --git a/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java b/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java index 3397f5fd..f48f7886 100644 --- a/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java +++ b/android/arch/lifecycle/FragmentInBackStackLifecycleTest.java @@ -37,6 +37,7 @@ import android.arch.lifecycle.testapp.R; import android.support.test.filters.SmallTest; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; @@ -58,20 +59,20 @@ public class FragmentInBackStackLifecycleTest { final ArrayList<Event> collectedEvents = new ArrayList<>(); LifecycleObserver collectingObserver = new LifecycleObserver() { @OnLifecycleEvent(Event.ON_ANY) - void onAny(LifecycleOwner owner, Event event) { + void onAny(@SuppressWarnings("unused") LifecycleOwner owner, Event event) { collectedEvents.add(event); } }; final FragmentActivity activity = activityTestRule.getActivity(); activityTestRule.runOnUiThread(() -> { FragmentManager fm = activity.getSupportFragmentManager(); - LifecycleFragment fragment = new LifecycleFragment(); + Fragment fragment = new Fragment(); fm.beginTransaction().add(R.id.fragment_container, fragment, "tag").addToBackStack(null) .commit(); fm.executePendingTransactions(); fragment.getLifecycle().addObserver(collectingObserver); - LifecycleFragment fragment2 = new LifecycleFragment(); + Fragment fragment2 = new Fragment(); fm.beginTransaction().replace(R.id.fragment_container, fragment2).addToBackStack(null) .commit(); fm.executePendingTransactions(); @@ -82,12 +83,13 @@ public class FragmentInBackStackLifecycleTest { EmptyActivity newActivity = recreateActivity(activityTestRule.getActivity(), activityTestRule); + //noinspection ArraysAsListWithZeroOrOneArgument assertThat(collectedEvents, is(asList(ON_DESTROY))); collectedEvents.clear(); EmptyActivity lastActivity = recreateActivity(newActivity, activityTestRule); activityTestRule.runOnUiThread(() -> { FragmentManager fm = lastActivity.getSupportFragmentManager(); - LifecycleFragment fragment = (LifecycleFragment) fm.findFragmentByTag("tag"); + Fragment fragment = fm.findFragmentByTag("tag"); fragment.getLifecycle().addObserver(collectingObserver); assertThat(collectedEvents, iterableWithSize(0)); fm.popBackStackImmediate(); diff --git a/android/arch/lifecycle/FragmentOperationsLifecycleTest.java b/android/arch/lifecycle/FragmentOperationsLifecycleTest.java index be062cba..3e61277f 100644 --- a/android/arch/lifecycle/FragmentOperationsLifecycleTest.java +++ b/android/arch/lifecycle/FragmentOperationsLifecycleTest.java @@ -34,6 +34,7 @@ import android.support.test.annotation.UiThreadTest; import android.support.test.filters.MediumTest; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import org.junit.Rule; @@ -55,7 +56,7 @@ public class FragmentOperationsLifecycleTest { @UiThreadTest public void addRemoveFragment() { EmptyActivity activity = mActivityTestRule.getActivity(); - LifecycleFragment fragment = new LifecycleFragment(); + Fragment fragment = new Fragment(); FragmentManager fm = activity.getSupportFragmentManager(); fm.beginTransaction().add(fragment, "tag").commitNow(); CollectingObserver observer = observeAndCollectIn(fragment); @@ -70,7 +71,7 @@ public class FragmentOperationsLifecycleTest { @UiThreadTest public void fragmentInBackstack() { EmptyActivity activity = mActivityTestRule.getActivity(); - LifecycleFragment fragment1 = new LifecycleFragment(); + Fragment fragment1 = new Fragment(); FragmentManager fm = activity.getSupportFragmentManager(); fm.beginTransaction().add(R.id.fragment_container, fragment1, "tag").addToBackStack(null) .commit(); @@ -78,7 +79,7 @@ public class FragmentOperationsLifecycleTest { CollectingObserver observer1 = observeAndCollectIn(fragment1); assertThat(observer1.getEventsAndReset(), is(asList(ON_CREATE, ON_START, ON_RESUME))); - LifecycleFragment fragment2 = new LifecycleFragment(); + Fragment fragment2 = new Fragment(); fm.beginTransaction().replace(R.id.fragment_container, fragment2).addToBackStack(null) .commit(); fm.executePendingTransactions(); @@ -95,7 +96,7 @@ public class FragmentOperationsLifecycleTest { assertThat(observer1.getEventsAndReset(), is(asList(ON_PAUSE, ON_STOP, ON_DESTROY))); } - private static CollectingObserver observeAndCollectIn(LifecycleFragment fragment) { + private static CollectingObserver observeAndCollectIn(Fragment fragment) { CollectingObserver observer = new CollectingObserver(); fragment.getLifecycle().addObserver(observer); return observer; diff --git a/android/arch/lifecycle/FullLifecycleObserver.java b/android/arch/lifecycle/FullLifecycleObserver.java new file mode 100644 index 00000000..f1792747 --- /dev/null +++ b/android/arch/lifecycle/FullLifecycleObserver.java @@ -0,0 +1,32 @@ +/* + * 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; + +interface FullLifecycleObserver extends LifecycleObserver { + + void onCreate(LifecycleOwner owner); + + void onStart(LifecycleOwner owner); + + void onResume(LifecycleOwner owner); + + void onPause(LifecycleOwner owner); + + void onStop(LifecycleOwner owner); + + void onDestroy(LifecycleOwner owner); +} diff --git a/android/arch/lifecycle/FullLifecycleObserverAdapter.java b/android/arch/lifecycle/FullLifecycleObserverAdapter.java new file mode 100644 index 00000000..0a91a668 --- /dev/null +++ b/android/arch/lifecycle/FullLifecycleObserverAdapter.java @@ -0,0 +1,52 @@ +/* + * 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; + +class FullLifecycleObserverAdapter implements GenericLifecycleObserver { + + private final FullLifecycleObserver mObserver; + + FullLifecycleObserverAdapter(FullLifecycleObserver observer) { + mObserver = observer; + } + + @Override + public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { + switch (event) { + case ON_CREATE: + mObserver.onCreate(source); + break; + case ON_START: + mObserver.onStart(source); + break; + case ON_RESUME: + mObserver.onResume(source); + break; + case ON_PAUSE: + mObserver.onPause(source); + break; + case ON_STOP: + mObserver.onStop(source); + break; + case ON_DESTROY: + mObserver.onDestroy(source); + break; + case ON_ANY: + throw new IllegalArgumentException("ON_ANY must not been send by anybody"); + } + } +} diff --git a/android/arch/lifecycle/FullLifecycleObserverTest.java b/android/arch/lifecycle/FullLifecycleObserverTest.java new file mode 100644 index 00000000..def67557 --- /dev/null +++ b/android/arch/lifecycle/FullLifecycleObserverTest.java @@ -0,0 +1,89 @@ +/* + * 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_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.INITIALIZED; +import static android.arch.lifecycle.Lifecycle.State.RESUMED; +import static android.arch.lifecycle.Lifecycle.State.STARTED; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.InOrder; +import org.mockito.Mockito; + +@RunWith(JUnit4.class) +public class FullLifecycleObserverTest { + private LifecycleOwner mOwner; + private Lifecycle mLifecycle; + + @Before + public void initMocks() { + mOwner = mock(LifecycleOwner.class); + mLifecycle = mock(Lifecycle.class); + when(mOwner.getLifecycle()).thenReturn(mLifecycle); + } + + @Test + public void eachEvent() { + FullLifecycleObserver obj = mock(FullLifecycleObserver.class); + FullLifecycleObserverAdapter observer = new FullLifecycleObserverAdapter(obj); + when(mLifecycle.getCurrentState()).thenReturn(CREATED); + + observer.onStateChanged(mOwner, ON_CREATE); + InOrder inOrder = Mockito.inOrder(obj); + inOrder.verify(obj).onCreate(mOwner); + reset(obj); + + when(mLifecycle.getCurrentState()).thenReturn(STARTED); + observer.onStateChanged(mOwner, ON_START); + inOrder.verify(obj).onStart(mOwner); + reset(obj); + + when(mLifecycle.getCurrentState()).thenReturn(RESUMED); + observer.onStateChanged(mOwner, ON_RESUME); + inOrder.verify(obj).onResume(mOwner); + reset(obj); + + when(mLifecycle.getCurrentState()).thenReturn(STARTED); + observer.onStateChanged(mOwner, ON_PAUSE); + inOrder.verify(obj).onPause(mOwner); + reset(obj); + + when(mLifecycle.getCurrentState()).thenReturn(CREATED); + observer.onStateChanged(mOwner, ON_STOP); + inOrder.verify(obj).onStop(mOwner); + reset(obj); + + when(mLifecycle.getCurrentState()).thenReturn(INITIALIZED); + observer.onStateChanged(mOwner, ON_DESTROY); + inOrder.verify(obj).onDestroy(mOwner); + reset(obj); + } +} diff --git a/android/arch/lifecycle/GeneratedAdapter.java b/android/arch/lifecycle/GeneratedAdapter.java new file mode 100644 index 00000000..a8862da4 --- /dev/null +++ b/android/arch/lifecycle/GeneratedAdapter.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle; + +import android.support.annotation.RestrictTo; + +/** + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public interface GeneratedAdapter { + + /** + * Called when a state transition event happens. + * + * @param source The source of the event + * @param event The event + * @param onAny approveCall onAny handlers + * @param logger if passed, used to track called methods and prevent calling the same method + * twice + */ + void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny, + MethodCallsLogger logger); +} diff --git a/android/arch/lifecycle/GeneratedAdaptersTest.java b/android/arch/lifecycle/GeneratedAdaptersTest.java new file mode 100644 index 00000000..2abb511c --- /dev/null +++ b/android/arch/lifecycle/GeneratedAdaptersTest.java @@ -0,0 +1,212 @@ +/* + * 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_ANY; +import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(JUnit4.class) +public class GeneratedAdaptersTest { + + private LifecycleOwner mOwner; + @SuppressWarnings("FieldCanBeLocal") + private Lifecycle mLifecycle; + + @Before + public void initMocks() { + mOwner = mock(LifecycleOwner.class); + mLifecycle = mock(Lifecycle.class); + when(mOwner.getLifecycle()).thenReturn(mLifecycle); + } + + static class SimpleObserver implements LifecycleObserver { + List<String> mLog; + + SimpleObserver(List<String> log) { + mLog = log; + } + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + void onCreate() { + mLog.add("onCreate"); + } + } + + @Test + public void testSimpleSingleGeneratedAdapter() { + List<String> actual = new ArrayList<>(); + GenericLifecycleObserver callback = Lifecycling.getCallback(new SimpleObserver(actual)); + callback.onStateChanged(mOwner, Lifecycle.Event.ON_CREATE); + assertThat(callback, instanceOf(SingleGeneratedAdapterObserver.class)); + assertThat(actual, is(singletonList("onCreate"))); + } + + static class TestObserver implements LifecycleObserver { + List<String> mLog; + + TestObserver(List<String> log) { + mLog = log; + } + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + void onCreate() { + mLog.add("onCreate"); + } + + @OnLifecycleEvent(ON_ANY) + void onAny() { + mLog.add("onAny"); + } + } + + @Test + public void testOnAny() { + List<String> actual = new ArrayList<>(); + GenericLifecycleObserver callback = Lifecycling.getCallback(new TestObserver(actual)); + callback.onStateChanged(mOwner, Lifecycle.Event.ON_CREATE); + assertThat(callback, instanceOf(SingleGeneratedAdapterObserver.class)); + assertThat(actual, is(asList("onCreate", "onAny"))); + } + + interface OnPauses extends LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + void onPause(); + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + void onPause(LifecycleOwner owner); + } + + interface OnPauseResume extends LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + void onPause(); + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + void onResume(); + } + + class Impl1 implements OnPauses, OnPauseResume { + + List<String> mLog; + + Impl1(List<String> log) { + mLog = log; + } + + @Override + public void onPause() { + mLog.add("onPause_0"); + } + + @Override + public void onResume() { + mLog.add("onResume"); + } + + @Override + public void onPause(LifecycleOwner owner) { + mLog.add("onPause_1"); + } + } + + @Test + public void testClashingInterfaces() { + List<String> actual = new ArrayList<>(); + GenericLifecycleObserver callback = Lifecycling.getCallback(new Impl1(actual)); + callback.onStateChanged(mOwner, Lifecycle.Event.ON_PAUSE); + assertThat(callback, instanceOf(CompositeGeneratedAdaptersObserver.class)); + assertThat(actual, is(asList("onPause_0", "onPause_1"))); + actual.clear(); + callback.onStateChanged(mOwner, Lifecycle.Event.ON_RESUME); + assertThat(actual, is(singletonList("onResume"))); + } + + class Base implements LifecycleObserver { + + List<String> mLog; + + Base(List<String> log) { + mLog = log; + } + + @OnLifecycleEvent(ON_ANY) + void onAny() { + mLog.add("onAny_0"); + } + + @OnLifecycleEvent(ON_ANY) + void onAny(LifecycleOwner owner) { + mLog.add("onAny_1"); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + void onResume() { + mLog.add("onResume"); + } + } + + interface OnAny extends LifecycleObserver { + @OnLifecycleEvent(ON_ANY) + void onAny(); + + @OnLifecycleEvent(ON_ANY) + void onAny(LifecycleOwner owner, Lifecycle.Event event); + } + + class Derived extends Base implements OnAny { + Derived(List<String> log) { + super(log); + } + + @Override + public void onAny() { + super.onAny(); + } + + @Override + public void onAny(LifecycleOwner owner, Lifecycle.Event event) { + mLog.add("onAny_2"); + assertThat(event, is(ON_RESUME)); + } + } + + @Test + public void testClashingClassAndInterface() { + List<String> actual = new ArrayList<>(); + GenericLifecycleObserver callback = Lifecycling.getCallback(new Derived(actual)); + callback.onStateChanged(mOwner, Lifecycle.Event.ON_RESUME); + assertThat(callback, instanceOf(CompositeGeneratedAdaptersObserver.class)); + assertThat(actual, is(asList("onResume", "onAny_0", "onAny_1", "onAny_2"))); + } + +} diff --git a/android/arch/lifecycle/Lifecycle.java b/android/arch/lifecycle/Lifecycle.java index fcbd50ad..02db5ff9 100644 --- a/android/arch/lifecycle/Lifecycle.java +++ b/android/arch/lifecycle/Lifecycle.java @@ -34,21 +34,25 @@ import android.support.annotation.MainThread; * before {@link android.app.Activity#onStop onStop} is called. * This gives you certain guarantees on which state the owner is in. * <p> - * Lifecycle events are observed using annotations. + * If you use <b>Java 8 Language</b>, then observe events with {@link DefaultLifecycleObserver}. + * To include it you should add {@code "android.arch.lifecycle:common-java8:<version>"} to your + * build.gradle file. * <pre> - * class TestObserver implements LifecycleObserver { - * {@literal @}OnLifecycleEvent(ON_STOP) - * void onStopped() {} + * class TestObserver implements DefaultLifecycleObserver { + * {@literal @}Override + * public void onCreate(LifecycleOwner owner) { + * // your code + * } * } * </pre> - * <p> - * Multiple methods can observe the same event. + * If you use <b>Java 7 Language</b>, Lifecycle events are observed using annotations. + * Once Java 8 Language becomes mainstream on Android, annotations will be deprecated, so between + * {@link DefaultLifecycleObserver} and annotations, + * you must always prefer {@code DefaultLifecycleObserver}. * <pre> * class TestObserver implements LifecycleObserver { * {@literal @}OnLifecycleEvent(ON_STOP) - * void onStoppedFirst() {} - * {@literal @}OnLifecycleEvent(ON_STOP) - * void onStoppedSecond() {} + * void onStopped() {} * } * </pre> * <p> diff --git a/android/arch/lifecycle/Lifecycling.java b/android/arch/lifecycle/Lifecycling.java index 3a5c0b9e..7d6b37fd 100644 --- a/android/arch/lifecycle/Lifecycling.java +++ b/android/arch/lifecycle/Lifecycling.java @@ -22,52 +22,61 @@ import android.support.annotation.RestrictTo; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; /** * Internal class to handle lifecycle conversion etc. + * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class Lifecycling { - private static Constructor<? extends GenericLifecycleObserver> sREFLECTIVE; - - static { - try { - sREFLECTIVE = ReflectiveGenericLifecycleObserver.class - .getDeclaredConstructor(Object.class); - } catch (NoSuchMethodException ignored) { +public class Lifecycling { - } - } + private static final int REFLECTIVE_CALLBACK = 1; + private static final int GENERATED_CALLBACK = 2; - private static Map<Class, Constructor<? extends GenericLifecycleObserver>> sCallbackCache = + private static Map<Class, Integer> sCallbackCache = new HashMap<>(); + private static Map<Class, List<Constructor<? extends GeneratedAdapter>>> sClassToAdapters = new HashMap<>(); @NonNull static GenericLifecycleObserver getCallback(Object object) { + if (object instanceof FullLifecycleObserver) { + return new FullLifecycleObserverAdapter((FullLifecycleObserver) object); + } + if (object instanceof GenericLifecycleObserver) { return (GenericLifecycleObserver) object; } - //noinspection TryWithIdenticalCatches - try { - final Class<?> klass = object.getClass(); - Constructor<? extends GenericLifecycleObserver> cachedConstructor = sCallbackCache.get( - klass); - if (cachedConstructor != null) { - return cachedConstructor.newInstance(object); + + final Class<?> klass = object.getClass(); + int type = getObserverConstructorType(klass); + if (type == GENERATED_CALLBACK) { + List<Constructor<? extends GeneratedAdapter>> constructors = + sClassToAdapters.get(klass); + if (constructors.size() == 1) { + GeneratedAdapter generatedAdapter = createGeneratedAdapter( + constructors.get(0), object); + return new SingleGeneratedAdapterObserver(generatedAdapter); } - cachedConstructor = getGeneratedAdapterConstructor(klass); - if (cachedConstructor != null) { - if (!cachedConstructor.isAccessible()) { - cachedConstructor.setAccessible(true); - } - } else { - cachedConstructor = sREFLECTIVE; + GeneratedAdapter[] adapters = new GeneratedAdapter[constructors.size()]; + for (int i = 0; i < constructors.size(); i++) { + adapters[i] = createGeneratedAdapter(constructors.get(i), object); } - sCallbackCache.put(klass, cachedConstructor); - return cachedConstructor.newInstance(object); + return new CompositeGeneratedAdaptersObserver(adapters); + } + return new ReflectiveGenericLifecycleObserver(object); + } + + private static GeneratedAdapter createGeneratedAdapter( + Constructor<? extends GeneratedAdapter> constructor, Object object) { + //noinspection TryWithIdenticalCatches + try { + return constructor.newInstance(object); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InstantiationException e) { @@ -78,37 +87,95 @@ class Lifecycling { } @Nullable - private static Constructor<? extends GenericLifecycleObserver> getGeneratedAdapterConstructor( - Class<?> klass) { - Package aPackage = klass.getPackage(); - final String fullPackage = aPackage != null ? aPackage.getName() : ""; - - String name = klass.getCanonicalName(); - // anonymous class bug:35073837 - if (name == null) { - return null; - } - final String adapterName = getAdapterName(fullPackage.isEmpty() ? name : - name.substring(fullPackage.length() + 1)); + private static Constructor<? extends GeneratedAdapter> generatedConstructor(Class<?> klass) { try { - @SuppressWarnings("unchecked") - final Class<? extends GenericLifecycleObserver> aClass = - (Class<? extends GenericLifecycleObserver>) Class.forName( + Package aPackage = klass.getPackage(); + String name = klass.getCanonicalName(); + final String fullPackage = aPackage != null ? aPackage.getName() : ""; + final String adapterName = getAdapterName(fullPackage.isEmpty() ? name : + name.substring(fullPackage.length() + 1)); + + @SuppressWarnings("unchecked") final Class<? extends GeneratedAdapter> aClass = + (Class<? extends GeneratedAdapter>) Class.forName( fullPackage.isEmpty() ? adapterName : fullPackage + "." + adapterName); - return aClass.getDeclaredConstructor(klass); - } catch (ClassNotFoundException e) { - final Class<?> superclass = klass.getSuperclass(); - if (superclass != null) { - return getGeneratedAdapterConstructor(superclass); + Constructor<? extends GeneratedAdapter> constructor = + aClass.getDeclaredConstructor(klass); + if (!constructor.isAccessible()) { + constructor.setAccessible(true); } + return constructor; + } catch (ClassNotFoundException e) { + return null; } catch (NoSuchMethodException e) { // this should not happen throw new RuntimeException(e); } - return null; } - static String getAdapterName(String className) { + private static int getObserverConstructorType(Class<?> klass) { + if (sCallbackCache.containsKey(klass)) { + return sCallbackCache.get(klass); + } + int type = resolveObserverCallbackType(klass); + sCallbackCache.put(klass, type); + return type; + } + + private static int resolveObserverCallbackType(Class<?> klass) { + // anonymous class bug:35073837 + if (klass.getCanonicalName() == null) { + return REFLECTIVE_CALLBACK; + } + + Constructor<? extends GeneratedAdapter> constructor = generatedConstructor(klass); + if (constructor != null) { + sClassToAdapters.put(klass, Collections + .<Constructor<? extends GeneratedAdapter>>singletonList(constructor)); + return GENERATED_CALLBACK; + } + + boolean hasLifecycleMethods = ClassesInfoCache.sInstance.hasLifecycleMethods(klass); + if (hasLifecycleMethods) { + return REFLECTIVE_CALLBACK; + } + + Class<?> superclass = klass.getSuperclass(); + List<Constructor<? extends GeneratedAdapter>> adapterConstructors = null; + if (isLifecycleParent(superclass)) { + if (getObserverConstructorType(superclass) == REFLECTIVE_CALLBACK) { + return REFLECTIVE_CALLBACK; + } + adapterConstructors = new ArrayList<>(sClassToAdapters.get(superclass)); + } + + for (Class<?> intrface : klass.getInterfaces()) { + if (!isLifecycleParent(intrface)) { + continue; + } + if (getObserverConstructorType(intrface) == REFLECTIVE_CALLBACK) { + return REFLECTIVE_CALLBACK; + } + if (adapterConstructors == null) { + adapterConstructors = new ArrayList<>(); + } + adapterConstructors.addAll(sClassToAdapters.get(intrface)); + } + if (adapterConstructors != null) { + sClassToAdapters.put(klass, adapterConstructors); + return GENERATED_CALLBACK; + } + + return REFLECTIVE_CALLBACK; + } + + private static boolean isLifecycleParent(Class<?> klass) { + return klass != null && LifecycleObserver.class.isAssignableFrom(klass); + } + + /** + * Create a name for an adapter class. + */ + public static String getAdapterName(String className) { return className.replace(".", "_") + "_LifecycleAdapter"; } } diff --git a/android/arch/lifecycle/LifecyclingTest.java b/android/arch/lifecycle/LifecyclingTest.java new file mode 100644 index 00000000..70ce84c3 --- /dev/null +++ b/android/arch/lifecycle/LifecyclingTest.java @@ -0,0 +1,83 @@ +/* + * 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.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; + +import android.arch.lifecycle.observers.DerivedSequence1; +import android.arch.lifecycle.observers.DerivedSequence2; +import android.arch.lifecycle.observers.DerivedWithNewMethods; +import android.arch.lifecycle.observers.DerivedWithNoNewMethods; +import android.arch.lifecycle.observers.DerivedWithOverridenMethodsWithLfAnnotation; +import android.arch.lifecycle.observers.InterfaceImpl1; +import android.arch.lifecycle.observers.InterfaceImpl2; +import android.arch.lifecycle.observers.InterfaceImpl3; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class LifecyclingTest { + + @Test + public void testDerivedWithNewLfMethodsNoGeneratedAdapter() { + GenericLifecycleObserver callback = Lifecycling.getCallback(new DerivedWithNewMethods()); + assertThat(callback, instanceOf(ReflectiveGenericLifecycleObserver.class)); + } + + @Test + public void testDerivedWithNoNewLfMethodsNoGeneratedAdapter() { + GenericLifecycleObserver callback = Lifecycling.getCallback(new DerivedWithNoNewMethods()); + assertThat(callback, instanceOf(SingleGeneratedAdapterObserver.class)); + } + + @Test + public void testDerivedWithOverridenMethodsNoGeneratedAdapter() { + GenericLifecycleObserver callback = Lifecycling.getCallback( + new DerivedWithOverridenMethodsWithLfAnnotation()); + // that is not effective but... + assertThat(callback, instanceOf(ReflectiveGenericLifecycleObserver.class)); + } + + @Test + public void testInterfaceImpl1NoGeneratedAdapter() { + GenericLifecycleObserver callback = Lifecycling.getCallback(new InterfaceImpl1()); + assertThat(callback, instanceOf(SingleGeneratedAdapterObserver.class)); + } + + @Test + public void testInterfaceImpl2NoGeneratedAdapter() { + GenericLifecycleObserver callback = Lifecycling.getCallback(new InterfaceImpl2()); + assertThat(callback, instanceOf(CompositeGeneratedAdaptersObserver.class)); + } + + @Test + public void testInterfaceImpl3NoGeneratedAdapter() { + GenericLifecycleObserver callback = Lifecycling.getCallback(new InterfaceImpl3()); + assertThat(callback, instanceOf(CompositeGeneratedAdaptersObserver.class)); + } + + @Test + public void testDerivedSequence() { + GenericLifecycleObserver callback2 = Lifecycling.getCallback(new DerivedSequence2()); + assertThat(callback2, instanceOf(ReflectiveGenericLifecycleObserver.class)); + GenericLifecycleObserver callback1 = Lifecycling.getCallback(new DerivedSequence1()); + assertThat(callback1, instanceOf(SingleGeneratedAdapterObserver.class)); + } +} diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java index 99d859c4..3aea6acb 100644 --- a/android/arch/lifecycle/LiveData.java +++ b/android/arch/lifecycle/LiveData.java @@ -1,411 +1,4 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - +//LiveData interface for tests package android.arch.lifecycle; - -import static android.arch.lifecycle.Lifecycle.State.DESTROYED; -import static android.arch.lifecycle.Lifecycle.State.STARTED; - -import android.arch.core.executor.AppToolkitTaskExecutor; -import android.arch.core.internal.SafeIterableMap; -import android.arch.lifecycle.Lifecycle.State; -import android.support.annotation.MainThread; -import android.support.annotation.Nullable; - -import java.util.Iterator; -import java.util.Map; - -/** - * LiveData is a data holder class that can be observed within a given lifecycle. - * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and - * this observer will be notified about modifications of the wrapped data only if the paired - * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is - * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via - * {@link #observeForever(Observer)} is considered as always active and thus will be always notified - * about modifications. For those observers, you should manually call - * {@link #removeObserver(Observer)}. - * - * <p> An observer added with a Lifecycle will be automatically removed if the corresponding - * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for - * activities and fragments where they can safely observe LiveData and not worry about leaks: - * they will be instantly unsubscribed when they are destroyed. - * - * <p> - * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods - * to get notified when number of active {@link Observer}s change between 0 and 1. - * This allows LiveData to release any heavy resources when it does not have any Observers that - * are actively observing. - * <p> - * This class is designed to hold individual data fields of {@link ViewModel}, - * but can also be used for sharing data between different modules in your application - * in a decoupled fashion. - * - * @param <T> The type of data hold by this instance - * @see ViewModel - */ -@SuppressWarnings({"WeakerAccess", "unused"}) -// TODO: Thread checks are too strict right now, we may consider automatically moving them to main -// thread. -public abstract class LiveData<T> { - private final Object mDataLock = new Object(); - static final int START_VERSION = -1; - private static final Object NOT_SET = new Object(); - - private static final LifecycleOwner ALWAYS_ON = new LifecycleOwner() { - - private LifecycleRegistry mRegistry = init(); - - private LifecycleRegistry init() { - LifecycleRegistry registry = new LifecycleRegistry(this); - registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); - registry.handleLifecycleEvent(Lifecycle.Event.ON_START); - registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); - return registry; - } - - @Override - public Lifecycle getLifecycle() { - return mRegistry; - } - }; - - private SafeIterableMap<Observer<T>, LifecycleBoundObserver> mObservers = - new SafeIterableMap<>(); - - // how many observers are in active state - private int mActiveCount = 0; - private volatile Object mData = NOT_SET; - // when setData is called, we set the pending data and actual data swap happens on the main - // thread - private volatile Object mPendingData = NOT_SET; - private int mVersion = START_VERSION; - - private boolean mDispatchingValue; - @SuppressWarnings("FieldCanBeLocal") - private boolean mDispatchInvalidated; - private final Runnable mPostValueRunnable = new Runnable() { - @Override - public void run() { - Object newValue; - synchronized (mDataLock) { - newValue = mPendingData; - mPendingData = NOT_SET; - } - //noinspection unchecked - setValue((T) newValue); - } - }; - - private void considerNotify(LifecycleBoundObserver observer) { - if (!observer.active) { - return; - } - // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet. - // - // we still first check observer.active to keep it as the entrance for events. So even if - // the observer moved to an active state, if we've not received that event, we better not - // notify for a more predictable notification order. - if (!isActiveState(observer.owner.getLifecycle().getCurrentState())) { - return; - } - if (observer.lastVersion >= mVersion) { - return; - } - observer.lastVersion = mVersion; - //noinspection unchecked - observer.observer.onChanged((T) mData); - } - - private void dispatchingValue(@Nullable LifecycleBoundObserver initiator) { - if (mDispatchingValue) { - mDispatchInvalidated = true; - return; - } - mDispatchingValue = true; - do { - mDispatchInvalidated = false; - if (initiator != null) { - considerNotify(initiator); - initiator = null; - } else { - for (Iterator<Map.Entry<Observer<T>, LifecycleBoundObserver>> iterator = - mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { - considerNotify(iterator.next().getValue()); - if (mDispatchInvalidated) { - break; - } - } - } - } while (mDispatchInvalidated); - mDispatchingValue = false; - } - - /** - * Adds the given observer to the observers list within the lifespan of the given - * owner. The events are dispatched on the main thread. If LiveData already has data - * set, it will be delivered to the observer. - * <p> - * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED} - * or {@link Lifecycle.State#RESUMED} state (active). - * <p> - * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will - * automatically be removed. - * <p> - * When data changes while the {@code owner} is not active, it will not receive any updates. - * If it becomes active again, it will receive the last available data automatically. - * <p> - * LiveData keeps a strong reference to the observer and the owner as long as the - * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to - * the observer & the owner. - * <p> - * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData - * ignores the call. - * <p> - * If the given owner, observer tuple is already in the list, the call is ignored. - * If the observer is already in the list with another owner, LiveData throws an - * {@link IllegalArgumentException}. - * - * @param owner The LifecycleOwner which controls the observer - * @param observer The observer that will receive the events - */ - @MainThread - public void observe(LifecycleOwner owner, Observer<T> observer) { - if (owner.getLifecycle().getCurrentState() == DESTROYED) { - // ignore - return; - } - LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); - LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper); - if (existing != null && existing.owner != wrapper.owner) { - throw new IllegalArgumentException("Cannot add the same observer" - + " with different lifecycles"); - } - if (existing != null) { - return; - } - owner.getLifecycle().addObserver(wrapper); - wrapper.activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState())); - } - - /** - * Adds the given observer to the observers list. This call is similar to - * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which - * is always active. This means that the given observer will receive all events and will never - * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop - * observing this LiveData. - * While LiveData has one of such observers, it will be considered - * as active. - * <p> - * If the observer was already added with an owner to this LiveData, LiveData throws an - * {@link IllegalArgumentException}. - * - * @param observer The observer that will receive the events - */ - @MainThread - public void observeForever(Observer<T> observer) { - observe(ALWAYS_ON, observer); - } - - /** - * Removes the given observer from the observers list. - * - * @param observer The Observer to receive events. - */ - @MainThread - public void removeObserver(final Observer<T> observer) { - assertMainThread("removeObserver"); - LifecycleBoundObserver removed = mObservers.remove(observer); - if (removed == null) { - return; - } - removed.owner.getLifecycle().removeObserver(removed); - removed.activeStateChanged(false); - } - - /** - * Removes all observers that are tied to the given {@link LifecycleOwner}. - * - * @param owner The {@code LifecycleOwner} scope for the observers to be removed. - */ - @MainThread - public void removeObservers(final LifecycleOwner owner) { - assertMainThread("removeObservers"); - for (Map.Entry<Observer<T>, LifecycleBoundObserver> entry : mObservers) { - if (entry.getValue().owner == owner) { - removeObserver(entry.getKey()); - } - } - } - - /** - * Posts a task to a main thread to set the given value. So if you have a following code - * executed in the main thread: - * <pre class="prettyprint"> - * liveData.postValue("a"); - * liveData.setValue("b"); - * </pre> - * The value "b" would be set at first and later the main thread would override it with - * the value "a". - * <p> - * If you called this method multiple times before a main thread executed a posted task, only - * the last value would be dispatched. - * - * @param value The new value - */ - protected void postValue(T value) { - boolean postTask; - synchronized (mDataLock) { - postTask = mPendingData == NOT_SET; - mPendingData = value; - } - if (!postTask) { - return; - } - AppToolkitTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); - } - - /** - * Sets the value. If there are active observers, the value will be dispatched to them. - * <p> - * This method must be called from the main thread. If you need set a value from a background - * thread, you can use {@link #postValue(Object)} - * - * @param value The new value - */ - @MainThread - protected void setValue(T value) { - assertMainThread("setValue"); - mVersion++; - mData = value; - dispatchingValue(null); - } - - /** - * Returns the current value. - * Note that calling this method on a background thread does not guarantee that the latest - * value set will be received. - * - * @return the current value - */ - @Nullable - public T getValue() { - Object data = mData; - if (data != NOT_SET) { - //noinspection unchecked - return (T) data; - } - return null; - } - - int getVersion() { - return mVersion; - } - - /** - * Called when the number of active observers change to 1 from 0. - * <p> - * This callback can be used to know that this LiveData is being used thus should be kept - * up to date. - */ - protected void onActive() { - - } - - /** - * Called when the number of active observers change from 1 to 0. - * <p> - * This does not mean that there are no observers left, there may still be observers but their - * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED} - * (like an Activity in the back stack). - * <p> - * You can check if there are observers via {@link #hasObservers()}. - */ - protected void onInactive() { - - } - - /** - * Returns true if this LiveData has observers. - * - * @return true if this LiveData has observers - */ - public boolean hasObservers() { - return mObservers.size() > 0; - } - - /** - * Returns true if this LiveData has active observers. - * - * @return true if this LiveData has active observers - */ - public boolean hasActiveObservers() { - return mActiveCount > 0; - } - - class LifecycleBoundObserver implements LifecycleObserver { - public final LifecycleOwner owner; - public final Observer<T> observer; - public boolean active; - public int lastVersion = START_VERSION; - - LifecycleBoundObserver(LifecycleOwner owner, Observer<T> observer) { - this.owner = owner; - this.observer = observer; - } - - @SuppressWarnings("unused") - @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - void onStateChange() { - if (owner.getLifecycle().getCurrentState() == DESTROYED) { - removeObserver(observer); - return; - } - // immediately set active state, so we'd never dispatch anything to inactive - // owner - activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState())); - - } - - void activeStateChanged(boolean newActive) { - if (newActive == active) { - return; - } - active = newActive; - boolean wasInactive = LiveData.this.mActiveCount == 0; - LiveData.this.mActiveCount += active ? 1 : -1; - if (wasInactive && active) { - onActive(); - } - if (LiveData.this.mActiveCount == 0 && !active) { - onInactive(); - } - if (active) { - dispatchingValue(this); - } - } - } - - static boolean isActiveState(State state) { - return state.isAtLeast(STARTED); - } - - private void assertMainThread(String methodName) { - if (!AppToolkitTaskExecutor.getInstance().isMainThread()) { - throw new IllegalStateException("Cannot invoke " + methodName + " on a background" - + " thread"); - } - } +public class LiveData<T> { } diff --git a/android/arch/lifecycle/LiveDataReactiveStreams.java b/android/arch/lifecycle/LiveDataReactiveStreams.java index 0be01496..2b25bc9b 100644 --- a/android/arch/lifecycle/LiveDataReactiveStreams.java +++ b/android/arch/lifecycle/LiveDataReactiveStreams.java @@ -16,7 +16,8 @@ package android.arch.lifecycle; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.reactivestreams.Publisher; @@ -85,7 +86,7 @@ public final class LiveDataReactiveStreams { if (n < 0 || mCanceled) { return; } - AppToolkitTaskExecutor.getInstance().executeOnMainThread(new Runnable() { + ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() { @Override public void run() { if (mCanceled) { @@ -110,7 +111,7 @@ public final class LiveDataReactiveStreams { if (mCanceled) { return; } - AppToolkitTaskExecutor.getInstance().executeOnMainThread(new Runnable() { + ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() { @Override public void run() { if (mCanceled) { @@ -133,40 +134,101 @@ public final class LiveDataReactiveStreams { /** * Creates an Observable {@link LiveData} stream from a ReactiveStreams publisher. + * + * <p> + * When the LiveData becomes active, it subscribes to the emissions from the Publisher. + * + * <p> + * When the LiveData becomes inactive, the subscription is cleared. + * LiveData holds the last value emitted by the Publisher when the LiveData was active. + * <p> + * Therefore, in the case of a hot RxJava Observable, when a new LiveData {@link Observer} is + * added, it will automatically notify with the last value held in LiveData, + * which might not be the last value emitted by the Publisher. + * + * @param <T> The type of data hold by this instance. */ public static <T> LiveData<T> fromPublisher(final Publisher<T> publisher) { - MutableLiveData<T> liveData = new MutableLiveData<>(); - // Since we don't have a way to directly observe cancels, weakly hold the live data. - final WeakReference<MutableLiveData<T>> liveDataRef = new WeakReference<>(liveData); + return new PublisherLiveData<>(publisher); + } - publisher.subscribe(new Subscriber<T>() { - @Override - public void onSubscribe(Subscription s) { - // Don't worry about backpressure. If the stream is too noisy then backpressure can - // be handled upstream. - s.request(Long.MAX_VALUE); - } + /** + * Defines a {@link LiveData} object that wraps a {@link Publisher}. + * + * <p> + * When the LiveData becomes active, it subscribes to the emissions from the Publisher. + * + * <p> + * When the LiveData becomes inactive, the subscription is cleared. + * LiveData holds the last value emitted by the Publisher when the LiveData was active. + * <p> + * Therefore, in the case of a hot RxJava Observable, when a new LiveData {@link Observer} is + * added, it will automatically notify with the last value held in LiveData, + * which might not be the last value emitted by the Publisher. + * + * @param <T> The type of data hold by this instance. + */ + private static class PublisherLiveData<T> extends LiveData<T> { + private WeakReference<Subscription> mSubscriptionRef; + private final Publisher mPublisher; + private final Object mLock = new Object(); + + PublisherLiveData(@NonNull final Publisher publisher) { + mPublisher = publisher; + } + + @Override + protected void onActive() { + super.onActive(); + + mPublisher.subscribe(new Subscriber<T>() { + @Override + public void onSubscribe(Subscription s) { + // Don't worry about backpressure. If the stream is too noisy then + // backpressure can be handled upstream. + synchronized (mLock) { + s.request(Long.MAX_VALUE); + mSubscriptionRef = new WeakReference<>(s); + } + } - @Override - public void onNext(final T t) { - final LiveData<T> liveData = liveDataRef.get(); - if (liveData != null) { - liveData.postValue(t); + @Override + public void onNext(final T t) { + postValue(t); } - } - @Override - public void onError(Throwable t) { - // Errors should be handled upstream, so propagate as a crash. - throw new RuntimeException(t); - } + @Override + public void onError(Throwable t) { + synchronized (mLock) { + mSubscriptionRef = null; + } + // Errors should be handled upstream, so propagate as a crash. + throw new RuntimeException(t); + } - @Override - public void onComplete() { + @Override + public void onComplete() { + synchronized (mLock) { + mSubscriptionRef = null; + } + } + }); + + } + + @Override + protected void onInactive() { + super.onInactive(); + synchronized (mLock) { + WeakReference<Subscription> subscriptionRef = mSubscriptionRef; + if (subscriptionRef != null) { + Subscription subscription = subscriptionRef.get(); + if (subscription != null) { + subscription.cancel(); + } + mSubscriptionRef = null; + } } - }); - - return liveData; + } } - } diff --git a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java index 87fba27c..7278847c 100644 --- a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java +++ b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java @@ -16,12 +16,10 @@ package android.arch.lifecycle; -import static android.arch.lifecycle.Lifecycle.State.RESUMED; - import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.TaskExecutor; import android.support.annotation.Nullable; import android.support.test.filters.SmallTest; @@ -47,28 +45,7 @@ import io.reactivex.subjects.AsyncSubject; @SmallTest public class LiveDataReactiveStreamsTest { - private static final Lifecycle sLifecycle = new Lifecycle() { - @Override - public void addObserver(LifecycleObserver observer) { - } - - @Override - public void removeObserver(LifecycleObserver observer) { - } - - @Override - public State getCurrentState() { - return RESUMED; - } - }; - private static final LifecycleOwner S_LIFECYCLE_OWNER = new LifecycleOwner() { - - @Override - public Lifecycle getLifecycle() { - return sLifecycle; - } - - }; + private LifecycleOwner mLifecycleOwner; private final List<String> mLiveDataOutput = new ArrayList<>(); private final Observer<String> mObserver = new Observer<String>() { @@ -85,8 +62,19 @@ public class LiveDataReactiveStreamsTest { @Before public void init() { + mLifecycleOwner = new LifecycleOwner() { + LifecycleRegistry mRegistry = new LifecycleRegistry(this); + { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + } + + @Override + public Lifecycle getLifecycle() { + return mRegistry; + } + }; mTestThread = Thread.currentThread(); - AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() { + ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { @Override public void executeOnDiskIO(Runnable runnable) { @@ -109,7 +97,7 @@ public class LiveDataReactiveStreamsTest { @After public void removeExecutorDelegate() { - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } @Test @@ -117,7 +105,7 @@ public class LiveDataReactiveStreamsTest { PublishProcessor<String> processor = PublishProcessor.create(); LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); - liveData.observe(S_LIFECYCLE_OWNER, mObserver); + liveData.observe(mLifecycleOwner, mObserver); processor.onNext("foo"); processor.onNext("bar"); @@ -132,13 +120,13 @@ public class LiveDataReactiveStreamsTest { PublishProcessor<String> processor = PublishProcessor.create(); LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); - liveData.observe(S_LIFECYCLE_OWNER, mObserver); + liveData.observe(mLifecycleOwner, mObserver); processor.onNext("foo"); processor.onNext("bar"); // The second mObserver should only get the newest value and any later values. - liveData.observe(S_LIFECYCLE_OWNER, new Observer<String>() { + liveData.observe(mLifecycleOwner, new Observer<String>() { @Override public void onChanged(@Nullable String s) { output2.add(s); @@ -152,12 +140,44 @@ public class LiveDataReactiveStreamsTest { } @Test + public void convertsFromPublisherAfterInactive() { + PublishProcessor<String> processor = PublishProcessor.create(); + LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); + + liveData.observe(mLifecycleOwner, mObserver); + processor.onNext("foo"); + liveData.removeObserver(mObserver); + processor.onNext("bar"); + + liveData.observe(mLifecycleOwner, mObserver); + processor.onNext("baz"); + + assertThat(mLiveDataOutput, is(Arrays.asList("foo", "foo", "baz"))); + } + + @Test + public void convertsFromPublisherManagesSubcriptions() { + PublishProcessor<String> processor = PublishProcessor.create(); + LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(processor); + + assertThat(processor.hasSubscribers(), is(false)); + liveData.observe(mLifecycleOwner, mObserver); + + // once the live data is active, there's a subscriber + assertThat(processor.hasSubscribers(), is(true)); + + liveData.removeObserver(mObserver); + // once the live data is inactive, the subscriber is removed + assertThat(processor.hasSubscribers(), is(false)); + } + + @Test public void convertsFromAsyncPublisher() { Flowable<String> input = Flowable.just("foo") .concatWith(Flowable.just("bar", "baz").observeOn(sBackgroundScheduler)); LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(input); - liveData.observe(S_LIFECYCLE_OWNER, mObserver); + liveData.observe(mLifecycleOwner, mObserver); assertThat(mLiveDataOutput, is(Collections.singletonList("foo"))); sBackgroundScheduler.triggerActions(); @@ -170,7 +190,7 @@ public class LiveDataReactiveStreamsTest { liveData.setValue("foo"); assertThat(liveData.getValue(), is("foo")); - Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(S_LIFECYCLE_OWNER, liveData)) + Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData)) .subscribe(mOutputProcessor); liveData.setValue("bar"); @@ -188,7 +208,7 @@ public class LiveDataReactiveStreamsTest { assertThat(liveData.getValue(), is("foo")); Disposable disposable = Flowable - .fromPublisher(LiveDataReactiveStreams.toPublisher(S_LIFECYCLE_OWNER, liveData)) + .fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData)) .subscribe(new Consumer<String>() { @Override public void accept(String s) throws Exception { @@ -216,7 +236,7 @@ public class LiveDataReactiveStreamsTest { final AsyncSubject<Subscription> subscriptionSubject = AsyncSubject.create(); - Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(S_LIFECYCLE_OWNER, liveData)) + Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData)) .subscribe(new Subscriber<String>() { @Override public void onSubscribe(Subscription s) { @@ -275,7 +295,7 @@ public class LiveDataReactiveStreamsTest { public void convertsToPublisherWithAsyncData() { MutableLiveData<String> liveData = new MutableLiveData<>(); - Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(S_LIFECYCLE_OWNER, liveData)) + Flowable.fromPublisher(LiveDataReactiveStreams.toPublisher(mLifecycleOwner, liveData)) .observeOn(sBackgroundScheduler) .subscribe(mOutputProcessor); diff --git a/android/arch/lifecycle/LiveDataTest.java b/android/arch/lifecycle/LiveDataTest.java index ed2a35dc..9f0b4257 100644 --- a/android/arch/lifecycle/LiveDataTest.java +++ b/android/arch/lifecycle/LiveDataTest.java @@ -36,16 +36,20 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.lifecycle.util.InstantTaskExecutor; import android.support.annotation.Nullable; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.InOrder; import org.mockito.Mockito; @SuppressWarnings({"unchecked"}) +@RunWith(JUnit4.class) public class LiveDataTest { private PublicLiveData<String> mLiveData; private LifecycleOwner mOwner; @@ -66,12 +70,12 @@ public class LiveDataTest { @Before public void swapExecutorDelegate() { - AppToolkitTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor()); + ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor()); } @After public void removeExecutorDelegate() { - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } @Test @@ -418,6 +422,40 @@ public class LiveDataTest { verify(mActiveObserversChanged, never()).onCall(anyBoolean()); } + @Test + public void testRemoveDuringAddition() { + mRegistry.handleLifecycleEvent(ON_START); + mLiveData.setValue("bla"); + mLiveData.observeForever(new Observer<String>() { + @Override + public void onChanged(@Nullable String s) { + mLiveData.removeObserver(this); + } + }); + assertThat(mLiveData.hasActiveObservers(), is(false)); + InOrder inOrder = Mockito.inOrder(mActiveObserversChanged); + inOrder.verify(mActiveObserversChanged).onCall(true); + inOrder.verify(mActiveObserversChanged).onCall(false); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testRemoveDuringBringingUpToState() { + mLiveData.setValue("bla"); + mLiveData.observeForever(new Observer<String>() { + @Override + public void onChanged(@Nullable String s) { + mLiveData.removeObserver(this); + } + }); + mRegistry.handleLifecycleEvent(ON_RESUME); + assertThat(mLiveData.hasActiveObservers(), is(false)); + InOrder inOrder = Mockito.inOrder(mActiveObserversChanged); + inOrder.verify(mActiveObserversChanged).onCall(true); + inOrder.verify(mActiveObserversChanged).onCall(false); + inOrder.verifyNoMoreInteractions(); + } + @SuppressWarnings("WeakerAccess") static class PublicLiveData<T> extends LiveData<T> { // cannot spy due to internal calls diff --git a/android/arch/lifecycle/MediatorLiveDataTest.java b/android/arch/lifecycle/MediatorLiveDataTest.java index 3de3eeeb..e2eadbe8 100644 --- a/android/arch/lifecycle/MediatorLiveDataTest.java +++ b/android/arch/lifecycle/MediatorLiveDataTest.java @@ -25,7 +25,7 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.lifecycle.util.InstantTaskExecutor; import android.support.annotation.Nullable; @@ -69,7 +69,7 @@ public class MediatorLiveDataTest { @Before public void swapExecutorDelegate() { - AppToolkitTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor()); + ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor()); } @Test diff --git a/android/arch/lifecycle/MethodCallsLogger.java b/android/arch/lifecycle/MethodCallsLogger.java new file mode 100644 index 00000000..031e43e4 --- /dev/null +++ b/android/arch/lifecycle/MethodCallsLogger.java @@ -0,0 +1,42 @@ +/* + * 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.support.annotation.RestrictTo; + +import java.util.HashMap; +import java.util.Map; + +/** + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class MethodCallsLogger { + private Map<String, Integer> mCalledMethods = new HashMap<>(); + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public boolean approveCall(String name, int type) { + Integer nullableMask = mCalledMethods.get(name); + int mask = nullableMask != null ? nullableMask : 0; + boolean wasCalled = (mask & type) != 0; + mCalledMethods.put(name, mask | type); + return !wasCalled; + } +} diff --git a/android/arch/lifecycle/ProcessOwnerTest.java b/android/arch/lifecycle/ProcessOwnerTest.java index e80e11c4..37bdcdb4 100644 --- a/android/arch/lifecycle/ProcessOwnerTest.java +++ b/android/arch/lifecycle/ProcessOwnerTest.java @@ -37,6 +37,7 @@ import android.support.test.InstrumentationRegistry; 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.After; import org.junit.Rule; @@ -78,7 +79,7 @@ public class ProcessOwnerTest { @Test public void testNavigation() throws Throwable { - LifecycleActivity firstActivity = setupObserverOnResume(); + FragmentActivity firstActivity = setupObserverOnResume(); Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor( NavigationTestActivitySecond.class.getCanonicalName(), null, false); Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); @@ -88,15 +89,15 @@ public class ProcessOwnerTest { firstActivity.finish(); firstActivity.startActivity(intent); - LifecycleActivity secondActivity = (LifecycleActivity) monitor.waitForActivity(); + FragmentActivity secondActivity = (FragmentActivity) monitor.waitForActivity(); assertThat("Failed to navigate", secondActivity, notNullValue()); checkProcessObserverSilent(secondActivity); } @Test public void testRecreation() throws Throwable { - LifecycleActivity activity = setupObserverOnResume(); - LifecycleActivity recreated = TestUtils.recreateActivity(activity, activityTestRule); + FragmentActivity activity = setupObserverOnResume(); + FragmentActivity recreated = TestUtils.recreateActivity(activity, activityTestRule); assertThat("Failed to recreate", recreated, notNullValue()); checkProcessObserverSilent(recreated); } @@ -112,14 +113,15 @@ public class ProcessOwnerTest { NavigationTestActivityFirst activity = activityTestRule.getActivity(); activity.startActivity(new Intent(activity, NavigationDialogActivity.class)); - LifecycleActivity dialogActivity = (LifecycleActivity) monitor.waitForActivity(); + FragmentActivity dialogActivity = (FragmentActivity) monitor.waitForActivity(); checkProcessObserverSilent(dialogActivity); List<Event> events = Collections.synchronizedList(new ArrayList<>()); LifecycleObserver collectingObserver = new LifecycleObserver() { @OnLifecycleEvent(Event.ON_ANY) - public void onStateChanged(LifecycleOwner provider, Event event) { + public void onStateChanged(@SuppressWarnings("unused") LifecycleOwner provider, + Event event) { events.add(event); } }; @@ -138,8 +140,8 @@ public class ProcessOwnerTest { dialogActivity.finish(); } - private LifecycleActivity setupObserverOnResume() throws Throwable { - LifecycleActivity firstActivity = activityTestRule.getActivity(); + private FragmentActivity setupObserverOnResume() throws Throwable { + FragmentActivity firstActivity = activityTestRule.getActivity(); waitTillResumed(firstActivity, activityTestRule); addProcessObserver(mObserver); mObserver.mChangedState = false; @@ -156,7 +158,7 @@ public class ProcessOwnerTest { ProcessLifecycleOwner.get().getLifecycle().removeObserver(observer)); } - private void checkProcessObserverSilent(LifecycleActivity activity) throws Throwable { + private void checkProcessObserverSilent(FragmentActivity activity) throws Throwable { waitTillResumed(activity, activityTestRule); assertThat(mObserver.mChangedState, is(false)); activityTestRule.runOnUiThread(() -> diff --git a/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java b/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java index 44815e6d..f010ed84 100644 --- a/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java +++ b/android/arch/lifecycle/ReflectiveGenericLifecycleObserver.java @@ -16,204 +16,23 @@ package android.arch.lifecycle; +import android.arch.lifecycle.ClassesInfoCache.CallbackInfo; import android.arch.lifecycle.Lifecycle.Event; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - /** * An internal implementation of {@link GenericLifecycleObserver} that relies on reflection. */ class ReflectiveGenericLifecycleObserver implements GenericLifecycleObserver { private final Object mWrapped; private final CallbackInfo mInfo; - @SuppressWarnings("WeakerAccess") - static final Map<Class, CallbackInfo> sInfoCache = new HashMap<>(); ReflectiveGenericLifecycleObserver(Object wrapped) { mWrapped = wrapped; - mInfo = getInfo(mWrapped.getClass()); + mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass()); } @Override public void onStateChanged(LifecycleOwner source, Event event) { - invokeCallbacks(mInfo, source, event); - } - - private void invokeMethodsForEvent(List<MethodReference> handlers, LifecycleOwner source, - Event event) { - if (handlers != null) { - for (int i = handlers.size() - 1; i >= 0; i--) { - MethodReference reference = handlers.get(i); - invokeCallback(reference, source, event); - } - } - } - - @SuppressWarnings("ConstantConditions") - private void invokeCallbacks(CallbackInfo info, LifecycleOwner source, Event event) { - invokeMethodsForEvent(info.mEventToHandlers.get(event), source, event); - invokeMethodsForEvent(info.mEventToHandlers.get(Event.ON_ANY), source, event); - } - - private void invokeCallback(MethodReference reference, LifecycleOwner source, Event event) { - //noinspection TryWithIdenticalCatches - try { - switch (reference.mCallType) { - case CALL_TYPE_NO_ARG: - reference.mMethod.invoke(mWrapped); - break; - case CALL_TYPE_PROVIDER: - reference.mMethod.invoke(mWrapped, source); - break; - case CALL_TYPE_PROVIDER_WITH_EVENT: - reference.mMethod.invoke(mWrapped, source, event); - break; - } - } catch (InvocationTargetException e) { - throw new RuntimeException("Failed to call observer method", e.getCause()); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } + mInfo.invokeCallbacks(source, event, mWrapped); } - - private static CallbackInfo getInfo(Class klass) { - CallbackInfo existing = sInfoCache.get(klass); - if (existing != null) { - return existing; - } - existing = createInfo(klass); - return existing; - } - - private static void verifyAndPutHandler(Map<MethodReference, Event> handlers, - MethodReference newHandler, Event newEvent, Class klass) { - Event event = handlers.get(newHandler); - if (event != null && newEvent != event) { - Method method = newHandler.mMethod; - throw new IllegalArgumentException( - "Method " + method.getName() + " in " + klass.getName() - + " already declared with different @OnLifecycleEvent value: previous" - + " value " + event + ", new value " + newEvent); - } - if (event == null) { - handlers.put(newHandler, newEvent); - } - } - - private static CallbackInfo createInfo(Class klass) { - Class superclass = klass.getSuperclass(); - Map<MethodReference, Event> handlerToEvent = new HashMap<>(); - if (superclass != null) { - CallbackInfo superInfo = getInfo(superclass); - if (superInfo != null) { - handlerToEvent.putAll(superInfo.mHandlerToEvent); - } - } - - Method[] methods = klass.getDeclaredMethods(); - - Class[] interfaces = klass.getInterfaces(); - for (Class intrfc : interfaces) { - for (Entry<MethodReference, Event> entry : getInfo(intrfc).mHandlerToEvent.entrySet()) { - verifyAndPutHandler(handlerToEvent, entry.getKey(), entry.getValue(), klass); - } - } - - for (Method method : methods) { - OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class); - if (annotation == null) { - continue; - } - Class<?>[] params = method.getParameterTypes(); - int callType = CALL_TYPE_NO_ARG; - if (params.length > 0) { - callType = CALL_TYPE_PROVIDER; - if (!params[0].isAssignableFrom(LifecycleOwner.class)) { - throw new IllegalArgumentException( - "invalid parameter type. Must be one and instanceof LifecycleOwner"); - } - } - Event event = annotation.value(); - - if (params.length > 1) { - callType = CALL_TYPE_PROVIDER_WITH_EVENT; - if (!params[1].isAssignableFrom(Event.class)) { - throw new IllegalArgumentException( - "invalid parameter type. second arg must be an event"); - } - if (event != Event.ON_ANY) { - throw new IllegalArgumentException( - "Second arg is supported only for ON_ANY value"); - } - } - if (params.length > 2) { - throw new IllegalArgumentException("cannot have more than 2 params"); - } - MethodReference methodReference = new MethodReference(callType, method); - verifyAndPutHandler(handlerToEvent, methodReference, event, klass); - } - CallbackInfo info = new CallbackInfo(handlerToEvent); - sInfoCache.put(klass, info); - return info; - } - - @SuppressWarnings("WeakerAccess") - static class CallbackInfo { - final Map<Event, List<MethodReference>> mEventToHandlers; - final Map<MethodReference, Event> mHandlerToEvent; - - CallbackInfo(Map<MethodReference, Event> handlerToEvent) { - mHandlerToEvent = handlerToEvent; - mEventToHandlers = new HashMap<>(); - for (Entry<MethodReference, Event> entry : handlerToEvent.entrySet()) { - Event event = entry.getValue(); - List<MethodReference> methodReferences = mEventToHandlers.get(event); - if (methodReferences == null) { - methodReferences = new ArrayList<>(); - mEventToHandlers.put(event, methodReferences); - } - methodReferences.add(entry.getKey()); - } - } - } - - @SuppressWarnings("WeakerAccess") - static class MethodReference { - final int mCallType; - final Method mMethod; - - MethodReference(int callType, Method method) { - mCallType = callType; - mMethod = method; - mMethod.setAccessible(true); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - MethodReference that = (MethodReference) o; - return mCallType == that.mCallType && mMethod.getName().equals(that.mMethod.getName()); - } - - @Override - public int hashCode() { - return 31 * mCallType + mMethod.getName().hashCode(); - } - } - - private static final int CALL_TYPE_NO_ARG = 0; - private static final int CALL_TYPE_PROVIDER = 1; - private static final int CALL_TYPE_PROVIDER_WITH_EVENT = 2; } diff --git a/android/arch/lifecycle/SingleGeneratedAdapterObserver.java b/android/arch/lifecycle/SingleGeneratedAdapterObserver.java new file mode 100644 index 00000000..d176a3a9 --- /dev/null +++ b/android/arch/lifecycle/SingleGeneratedAdapterObserver.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle; + +import android.support.annotation.RestrictTo; + +/** + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class SingleGeneratedAdapterObserver implements GenericLifecycleObserver { + + private final GeneratedAdapter mGeneratedAdapter; + + SingleGeneratedAdapterObserver(GeneratedAdapter generatedAdapter) { + mGeneratedAdapter = generatedAdapter; + } + + @Override + public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { + mGeneratedAdapter.callMethods(source, event, false, null); + mGeneratedAdapter.callMethods(source, event, true, null); + } +} diff --git a/android/arch/lifecycle/TestUtils.java b/android/arch/lifecycle/TestUtils.java index c5a520fe..f0214bfb 100644 --- a/android/arch/lifecycle/TestUtils.java +++ b/android/arch/lifecycle/TestUtils.java @@ -23,15 +23,16 @@ import android.app.Instrumentation; import android.app.Instrumentation.ActivityMonitor; import android.support.test.InstrumentationRegistry; import android.support.test.rule.ActivityTestRule; +import android.support.v4.app.FragmentActivity; import java.util.concurrent.CountDownLatch; -public class TestUtils { +class TestUtils { private static final long TIMEOUT_MS = 2000; @SuppressWarnings("unchecked") - public static <T extends Activity> T recreateActivity(final T activity, ActivityTestRule rule) + static <T extends Activity> T recreateActivity(final T activity, ActivityTestRule rule) throws Throwable { ActivityMonitor monitor = new ActivityMonitor( activity.getClass().getCanonicalName(), null, false); @@ -60,7 +61,7 @@ public class TestUtils { return result; } - static void waitTillResumed(final LifecycleActivity a, ActivityTestRule<?> activityRule) + static void waitTillResumed(final FragmentActivity a, ActivityTestRule<?> activityRule) throws Throwable { final CountDownLatch latch = new CountDownLatch(1); activityRule.runOnUiThread(() -> { diff --git a/android/arch/lifecycle/Transformations.java b/android/arch/lifecycle/Transformations.java index c316563d..9ce9cbb7 100644 --- a/android/arch/lifecycle/Transformations.java +++ b/android/arch/lifecycle/Transformations.java @@ -22,6 +22,12 @@ import android.support.annotation.Nullable; /** * Transformations for a {@link LiveData} class. + * <p> + * You can use transformation methods to carry information across the observer's lifecycle. The + * transformations aren't calculated unless an observer is observing the returned LiveData object. + * <p> + * Because the transformations are calculated lazily, lifecycle-related behavior is implicitly + * passed down without requiring additional explicit calls or dependencies. */ @SuppressWarnings("WeakerAccess") public class Transformations { @@ -34,6 +40,18 @@ public class Transformations { * LiveData and returns LiveData, which emits resulting values. * <p> * The given function {@code func} will be executed on the main thread. + * <p> + * Suppose that you have a LiveData, named {@code userLiveData}, that contains user data and you + * need to display the user name, created by concatenating the first and the last + * name of the user. You can define a function that handles the name creation, that will be + * applied to every value emitted by {@code useLiveData}. + * + * <pre> + * LiveData<User> userLiveData = ...; + * LiveData<String> userName = Transformations.map(userLiveData, user -> { + * return user.firstName + " " + user.lastName + * }); + * </pre> * * @param source a {@code LiveData} to listen to * @param func a function to apply @@ -63,9 +81,39 @@ public class Transformations { * <p> * If the given function returns null, then {@code swLiveData} is not "backed" by any other * LiveData. + * * <p> * The given function {@code func} will be executed on the main thread. * + * <p> + * Consider the case where you have a LiveData containing a user id. Every time there's a new + * user id emitted, you want to trigger a request to get the user object corresponding to that + * id, from a repository that also returns a LiveData. + * <p> + * The {@code userIdLiveData} is the trigger and the LiveData returned by the {@code + * repository.getUserById} is the "backing" LiveData. + * <p> + * In a scenario where the repository contains User(1, "Jane") and User(2, "John"), when the + * userIdLiveData value is set to "1", the {@code switchMap} will call {@code getUser(1)}, + * that will return a LiveData containing the value User(1, "Jane"). So now, the userLiveData + * will emit User(1, "Jane"). When the user in the repository gets updated to User(1, "Sarah"), + * the {@code userLiveData} gets automatically notified and will emit User(1, "Sarah"). + * <p> + * When the {@code setUserId} method is called with userId = "2", the value of the {@code + * userIdLiveData} changes and automatically triggers a request for getting the user with id + * "2" from the repository. So, the {@code userLiveData} emits User(2, "John"). The LiveData + * returned by {@code repository.getUserById(1)} is removed as a source. + * + * <pre> + * MutableLiveData<String> userIdLiveData = ...; + * LiveData<User> userLiveData = Transformations.switchMap(userIdLiveData, id -> + * repository.getUserById(id)); + * + * void setUserId(String userId) { + * this.userIdLiveData.setValue(userId); + * } + * </pre> + * * @param trigger a {@code LiveData} to listen to * @param func a function which creates "backing" LiveData * @param <X> a type of {@code source} LiveData diff --git a/android/arch/lifecycle/TransformationsTest.java b/android/arch/lifecycle/TransformationsTest.java index e92ecca3..940a3e86 100644 --- a/android/arch/lifecycle/TransformationsTest.java +++ b/android/arch/lifecycle/TransformationsTest.java @@ -25,7 +25,7 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.util.Function; import android.arch.lifecycle.util.InstantTaskExecutor; @@ -42,7 +42,7 @@ public class TransformationsTest { @Before public void swapExecutorDelegate() { - AppToolkitTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor()); + ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor()); } @Before diff --git a/android/arch/lifecycle/ViewModelProviderTest.java b/android/arch/lifecycle/ViewModelProviderTest.java index 61760fc3..8877357a 100644 --- a/android/arch/lifecycle/ViewModelProviderTest.java +++ b/android/arch/lifecycle/ViewModelProviderTest.java @@ -21,6 +21,8 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import android.arch.lifecycle.ViewModelProvider.NewInstanceFactory; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; import org.junit.Assert; import org.junit.Before; @@ -82,6 +84,18 @@ public class ViewModelProviderTest { assertThat(viewModel, is(provider.get(ViewModel1.class))); } + @Test(expected = IllegalStateException.class) + public void testNotAttachedActivity() { + // This is similar to call ViewModelProviders.of in Activity's constructor + ViewModelProviders.of(new FragmentActivity()); + } + + @Test(expected = IllegalStateException.class) + public void testNotAttachedFragment() { + // This is similar to call ViewModelProviders.of in Activity's constructor + ViewModelProviders.of(new Fragment()); + } + public static class ViewModel1 extends ViewModel { boolean mCleared; diff --git a/android/arch/lifecycle/ViewModelProviders.java b/android/arch/lifecycle/ViewModelProviders.java index f64365b6..746162a9 100644 --- a/android/arch/lifecycle/ViewModelProviders.java +++ b/android/arch/lifecycle/ViewModelProviders.java @@ -17,6 +17,7 @@ package android.arch.lifecycle; import android.annotation.SuppressLint; +import android.app.Activity; import android.app.Application; import android.arch.lifecycle.ViewModelProvider.Factory; import android.support.annotation.MainThread; @@ -40,6 +41,23 @@ public class ViewModelProviders { } } + private static Application checkApplication(Activity activity) { + Application application = activity.getApplication(); + if (application == null) { + throw new IllegalStateException("Your activity/fragment is not yet attached to " + + "Application. You can't request ViewModel before onCreate call."); + } + return application; + } + + private static Activity checkActivity(Fragment fragment) { + Activity activity = fragment.getActivity(); + if (activity == null) { + throw new IllegalStateException("Can't create ViewModelProvider for detached fragment"); + } + return activity; + } + /** * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given * {@code fragment} is alive. More detailed explanation is in {@link ViewModel}. @@ -51,12 +69,7 @@ public class ViewModelProviders { */ @MainThread public static ViewModelProvider of(@NonNull Fragment fragment) { - FragmentActivity activity = fragment.getActivity(); - if (activity == null) { - throw new IllegalArgumentException( - "Can't create ViewModelProvider for detached fragment"); - } - initializeFactoryIfNeeded(activity.getApplication()); + initializeFactoryIfNeeded(checkApplication(checkActivity(fragment))); return new ViewModelProvider(ViewModelStores.of(fragment), sDefaultFactory); } @@ -71,7 +84,7 @@ public class ViewModelProviders { */ @MainThread public static ViewModelProvider of(@NonNull FragmentActivity activity) { - initializeFactoryIfNeeded(activity.getApplication()); + initializeFactoryIfNeeded(checkApplication(activity)); return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory); } @@ -87,6 +100,7 @@ public class ViewModelProviders { */ @MainThread public static ViewModelProvider of(@NonNull Fragment fragment, @NonNull Factory factory) { + checkApplication(checkActivity(fragment)); return new ViewModelProvider(ViewModelStores.of(fragment), factory); } @@ -103,6 +117,7 @@ public class ViewModelProviders { @MainThread public static ViewModelProvider of(@NonNull FragmentActivity activity, @NonNull Factory factory) { + checkApplication(activity); return new ViewModelProvider(ViewModelStores.of(activity), factory); } diff --git a/android/arch/lifecycle/ViewModelTest.java b/android/arch/lifecycle/ViewModelTest.java index 98ce0278..03ebdf36 100644 --- a/android/arch/lifecycle/ViewModelTest.java +++ b/android/arch/lifecycle/ViewModelTest.java @@ -32,6 +32,7 @@ import android.support.test.annotation.UiThreadTest; import android.support.test.filters.MediumTest; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; @@ -120,7 +121,7 @@ public class ViewModelTest { void onResume() { try { final FragmentManager manager = activity.getSupportFragmentManager(); - LifecycleFragment fragment = new LifecycleFragment(); + Fragment fragment = new Fragment(); manager.beginTransaction().add(fragment, "temp").commitNow(); ViewModel1 vm = ViewModelProviders.of(fragment).get(ViewModel1.class); assertThat(vm.mCleared, is(false)); diff --git a/android/arch/lifecycle/activity/EmptyActivity.java b/android/arch/lifecycle/activity/EmptyActivity.java index 017fff48..c32c8985 100644 --- a/android/arch/lifecycle/activity/EmptyActivity.java +++ b/android/arch/lifecycle/activity/EmptyActivity.java @@ -16,13 +16,12 @@ package android.arch.lifecycle.activity; +import android.arch.lifecycle.extensions.test.R; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; -import android.arch.lifecycle.LifecycleActivity; -import android.arch.lifecycle.extensions.test.R; - -public class EmptyActivity extends LifecycleActivity { +public class EmptyActivity extends FragmentActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/android/arch/lifecycle/activity/FragmentLifecycleActivity.java b/android/arch/lifecycle/activity/FragmentLifecycleActivity.java index f4485e8f..2eb1cc2e 100644 --- a/android/arch/lifecycle/activity/FragmentLifecycleActivity.java +++ b/android/arch/lifecycle/activity/FragmentLifecycleActivity.java @@ -17,7 +17,6 @@ package android.arch.lifecycle.activity; import android.arch.lifecycle.Lifecycle; -import android.arch.lifecycle.LifecycleFragment; import android.arch.lifecycle.LifecycleObserver; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.OnLifecycleEvent; @@ -26,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import java.util.ArrayList; @@ -70,9 +70,9 @@ public class FragmentLifecycleActivity extends AppCompatActivity { mLoggedEvents.clear(); } - public static class MainFragment extends LifecycleFragment { + public static class MainFragment extends Fragment { @Nullable - LifecycleFragment mNestedFragment; + Fragment mNestedFragment; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -85,7 +85,7 @@ public class FragmentLifecycleActivity extends AppCompatActivity { } } - public static class NestedFragment extends LifecycleFragment { + public static class NestedFragment extends Fragment { } public static Intent intentFor(Context context, boolean nested) { @@ -98,7 +98,8 @@ public class FragmentLifecycleActivity extends AppCompatActivity { mObservedOwner = provider; provider.getLifecycle().addObserver(new LifecycleObserver() { @OnLifecycleEvent(Lifecycle.Event.ON_ANY) - public void anyEvent(LifecycleOwner owner, Lifecycle.Event event) { + public void anyEvent(@SuppressWarnings("unused") LifecycleOwner owner, + Lifecycle.Event event) { mLoggedEvents.add(event); } }); diff --git a/android/arch/lifecycle/observers/Base.java b/android/arch/lifecycle/observers/Base.java new file mode 100644 index 00000000..08919d46 --- /dev/null +++ b/android/arch/lifecycle/observers/Base.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleObserver; +import android.arch.lifecycle.OnLifecycleEvent; + +public class Base implements LifecycleObserver { + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + public void onCreate() { + } +} diff --git a/android/arch/lifecycle/observers/Base_LifecycleAdapter.java b/android/arch/lifecycle/observers/Base_LifecycleAdapter.java new file mode 100644 index 00000000..4218b963 --- /dev/null +++ b/android/arch/lifecycle/observers/Base_LifecycleAdapter.java @@ -0,0 +1,34 @@ +/* + * 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.observers; + +import android.arch.lifecycle.GeneratedAdapter; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.MethodCallsLogger; + +public class Base_LifecycleAdapter implements GeneratedAdapter { + + public Base_LifecycleAdapter(Base base) { + } + + @Override + public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny, + MethodCallsLogger logger) { + + } +} diff --git a/android/arch/lifecycle/observers/DerivedSequence1.java b/android/arch/lifecycle/observers/DerivedSequence1.java new file mode 100644 index 00000000..9db37f17 --- /dev/null +++ b/android/arch/lifecycle/observers/DerivedSequence1.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +public class DerivedSequence1 extends Base { + + public void something() { + } +} diff --git a/android/arch/lifecycle/observers/DerivedSequence2.java b/android/arch/lifecycle/observers/DerivedSequence2.java new file mode 100644 index 00000000..f2ef943e --- /dev/null +++ b/android/arch/lifecycle/observers/DerivedSequence2.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.OnLifecycleEvent; + +public class DerivedSequence2 extends DerivedSequence1 { + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + void onStop() { + + } +} diff --git a/android/arch/lifecycle/observers/DerivedWithNewMethods.java b/android/arch/lifecycle/observers/DerivedWithNewMethods.java new file mode 100644 index 00000000..b1eaef0e --- /dev/null +++ b/android/arch/lifecycle/observers/DerivedWithNewMethods.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.OnLifecycleEvent; + +public class DerivedWithNewMethods extends Base { + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + void onStop() { + + } +} diff --git a/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java b/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java new file mode 100644 index 00000000..cb1afb8c --- /dev/null +++ b/android/arch/lifecycle/observers/DerivedWithNoNewMethods.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +public class DerivedWithNoNewMethods extends Base { +} diff --git a/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java b/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java new file mode 100644 index 00000000..40c7c9ac --- /dev/null +++ b/android/arch/lifecycle/observers/DerivedWithOverridenMethodsWithLfAnnotation.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.OnLifecycleEvent; + +public class DerivedWithOverridenMethodsWithLfAnnotation extends Base { + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + @Override + public void onCreate() { + super.onCreate(); + } +} diff --git a/android/arch/lifecycle/observers/Interface1.java b/android/arch/lifecycle/observers/Interface1.java new file mode 100644 index 00000000..e193de98 --- /dev/null +++ b/android/arch/lifecycle/observers/Interface1.java @@ -0,0 +1,27 @@ +/* + * 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.observers; + +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleObserver; +import android.arch.lifecycle.OnLifecycleEvent; + +public interface Interface1 extends LifecycleObserver { + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + void onCreate(); +} diff --git a/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java b/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java new file mode 100644 index 00000000..c597b1c8 --- /dev/null +++ b/android/arch/lifecycle/observers/Interface1_LifecycleAdapter.java @@ -0,0 +1,34 @@ +/* + * 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.observers; + +import android.arch.lifecycle.GeneratedAdapter; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.MethodCallsLogger; + +public class Interface1_LifecycleAdapter implements GeneratedAdapter { + + public Interface1_LifecycleAdapter(Interface1 base) { + } + + @Override + public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny, + MethodCallsLogger logger) { + + } +} diff --git a/android/arch/lifecycle/observers/Interface2.java b/android/arch/lifecycle/observers/Interface2.java new file mode 100644 index 00000000..1056fcbe --- /dev/null +++ b/android/arch/lifecycle/observers/Interface2.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleObserver; +import android.arch.lifecycle.OnLifecycleEvent; + +public interface Interface2 extends LifecycleObserver { + + @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) + void onCreate(); + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + void onDestroy(); +} diff --git a/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java b/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java new file mode 100644 index 00000000..b05b41a1 --- /dev/null +++ b/android/arch/lifecycle/observers/Interface2_LifecycleAdapter.java @@ -0,0 +1,34 @@ +/* + * 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.observers; + +import android.arch.lifecycle.GeneratedAdapter; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.MethodCallsLogger; + +public class Interface2_LifecycleAdapter implements GeneratedAdapter { + + public Interface2_LifecycleAdapter(Interface2 base) { + } + + @Override + public void callMethods(LifecycleOwner source, Lifecycle.Event event, boolean onAny, + MethodCallsLogger logger) { + + } +} diff --git a/android/arch/lifecycle/observers/InterfaceImpl1.java b/android/arch/lifecycle/observers/InterfaceImpl1.java new file mode 100644 index 00000000..2f033931 --- /dev/null +++ b/android/arch/lifecycle/observers/InterfaceImpl1.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +public class InterfaceImpl1 implements Interface1 { + @Override + public void onCreate() { + } +} diff --git a/android/arch/lifecycle/observers/InterfaceImpl2.java b/android/arch/lifecycle/observers/InterfaceImpl2.java new file mode 100644 index 00000000..eef8ce4f --- /dev/null +++ b/android/arch/lifecycle/observers/InterfaceImpl2.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +public class InterfaceImpl2 implements Interface1, Interface2 { + @Override + public void onCreate() { + } + + @Override + public void onDestroy() { + + } +} diff --git a/android/arch/lifecycle/observers/InterfaceImpl3.java b/android/arch/lifecycle/observers/InterfaceImpl3.java new file mode 100644 index 00000000..8f31808a --- /dev/null +++ b/android/arch/lifecycle/observers/InterfaceImpl3.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.lifecycle.observers; + +public class InterfaceImpl3 extends Base implements Interface1 { + @Override + public void onCreate() { + } +} diff --git a/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java b/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java index 5972b16c..5f33c282 100644 --- a/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java +++ b/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java @@ -19,8 +19,8 @@ package android.arch.lifecycle.testapp; import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; import android.arch.lifecycle.Lifecycle; -import android.arch.lifecycle.LifecycleActivity; import android.os.Bundle; +import android.support.v4.app.FragmentActivity; import android.util.Pair; import java.util.ArrayList; @@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit; /** * Activity for testing full lifecycle */ -public class FullLifecycleTestActivity extends LifecycleActivity implements CollectingActivity { +public class FullLifecycleTestActivity extends FragmentActivity implements CollectingActivity { private List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents = new ArrayList<>(); private TestObserver mTestObserver = new TestObserver(mCollectedEvents); diff --git a/android/arch/lifecycle/testapp/LifecycleTestActivity.java b/android/arch/lifecycle/testapp/LifecycleTestActivity.java index 093ec7f8..cf07aeeb 100644 --- a/android/arch/lifecycle/testapp/LifecycleTestActivity.java +++ b/android/arch/lifecycle/testapp/LifecycleTestActivity.java @@ -16,13 +16,13 @@ package android.arch.lifecycle.testapp; -import android.arch.lifecycle.LifecycleActivity; import android.os.Bundle; +import android.support.v4.app.FragmentActivity; /** * Activity for testing events by themselves */ -public class LifecycleTestActivity extends LifecycleActivity { +public class LifecycleTestActivity extends FragmentActivity { /** * identifies that diff --git a/android/arch/lifecycle/testapp/NavigationDialogActivity.java b/android/arch/lifecycle/testapp/NavigationDialogActivity.java index 709bd8dc..0ae94033 100644 --- a/android/arch/lifecycle/testapp/NavigationDialogActivity.java +++ b/android/arch/lifecycle/testapp/NavigationDialogActivity.java @@ -16,10 +16,10 @@ package android.arch.lifecycle.testapp; -import android.arch.lifecycle.LifecycleActivity; +import android.support.v4.app.FragmentActivity; /** * an activity with Dialog theme. */ -public class NavigationDialogActivity extends LifecycleActivity { +public class NavigationDialogActivity extends FragmentActivity { } diff --git a/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java b/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java index f1847c9f..69fd478f 100644 --- a/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java +++ b/android/arch/lifecycle/testapp/NavigationTestActivityFirst.java @@ -16,10 +16,10 @@ package android.arch.lifecycle.testapp; -import android.arch.lifecycle.LifecycleActivity; +import android.support.v4.app.FragmentActivity; /** * Activity for ProcessOwnerTest */ -public class NavigationTestActivityFirst extends LifecycleActivity { +public class NavigationTestActivityFirst extends FragmentActivity { } diff --git a/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java b/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java index 221e9275..0f9a4c9f 100644 --- a/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java +++ b/android/arch/lifecycle/testapp/NavigationTestActivitySecond.java @@ -16,10 +16,10 @@ package android.arch.lifecycle.testapp; -import android.arch.lifecycle.LifecycleActivity; +import android.support.v4.app.FragmentActivity; /** * Activity for ProcessOwnerTest */ -public class NavigationTestActivitySecond extends LifecycleActivity { +public class NavigationTestActivitySecond extends FragmentActivity { } diff --git a/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java b/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java index 6d61c5e7..77bd99f3 100644 --- a/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java +++ b/android/arch/lifecycle/testapp/SimpleAppLifecycleTestActivity.java @@ -17,12 +17,12 @@ package android.arch.lifecycle.testapp; import android.arch.lifecycle.Lifecycle; -import android.arch.lifecycle.LifecycleActivity; import android.arch.lifecycle.LifecycleObserver; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.OnLifecycleEvent; import android.arch.lifecycle.ProcessLifecycleOwner; import android.os.Bundle; +import android.support.v4.app.FragmentActivity; import android.util.Pair; import java.util.ArrayList; @@ -33,7 +33,7 @@ import java.util.concurrent.TimeUnit; /** * Activity for SimpleAppFullLifecycleTest */ -public class SimpleAppLifecycleTestActivity extends LifecycleActivity { +public class SimpleAppLifecycleTestActivity extends FragmentActivity { public enum TestEventType { PROCESS_EVENT, diff --git a/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java b/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java index 5ef9f161..1f9f100c 100644 --- a/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java +++ b/android/arch/lifecycle/viewmodeltest/ViewModelActivity.java @@ -16,15 +16,14 @@ package android.arch.lifecycle.viewmodeltest; +import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.extensions.test.R; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; -import android.arch.lifecycle.LifecycleActivity; -import android.arch.lifecycle.LifecycleFragment; -import android.arch.lifecycle.ViewModelProviders; - -public class ViewModelActivity extends LifecycleActivity { +public class ViewModelActivity extends FragmentActivity { public static final String KEY_FRAGMENT_MODEL = "fragment-model"; public static final String KEY_ACTIVITY_MODEL = "activity-model"; public static final String FRAGMENT_TAG_1 = "f1"; @@ -47,7 +46,7 @@ public class ViewModelActivity extends LifecycleActivity { defaultActivityModel = ViewModelProviders.of(this).get(TestViewModel.class); } - public static class ViewModelFragment extends LifecycleFragment { + public static class ViewModelFragment extends Fragment { public TestViewModel fragmentModel; public TestViewModel activityModel; public TestViewModel defaultActivityModel; diff --git a/android/arch/paging/BoundedDataSource.java b/android/arch/paging/BoundedDataSource.java index 96c23fc5..664ab16c 100644 --- a/android/arch/paging/BoundedDataSource.java +++ b/android/arch/paging/BoundedDataSource.java @@ -18,6 +18,7 @@ package android.arch.paging; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; +import android.support.annotation.WorkerThread; import java.util.ArrayList; import java.util.Collections; @@ -49,15 +50,18 @@ public abstract class BoundedDataSource<Value> extends PositionalDataSource<Valu * @return List of loaded items. Null if the BoundedDataSource is no longer valid, and should * not be queried again. */ + @WorkerThread @Nullable public abstract List<Value> loadRange(int startPosition, int loadCount); + @WorkerThread @Nullable @Override public List<Value> loadAfter(int startIndex, int pageSize) { return loadRange(startIndex, pageSize); } + @WorkerThread @Nullable @Override public List<Value> loadBefore(int startIndex, int pageSize) { diff --git a/android/arch/paging/ContiguousDataSource.java b/android/arch/paging/ContiguousDataSource.java index 9ff11173..afcc208c 100644 --- a/android/arch/paging/ContiguousDataSource.java +++ b/android/arch/paging/ContiguousDataSource.java @@ -41,6 +41,8 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V return true; } + /** @hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable public abstract NullPaddedList<Value> loadInitial( @@ -58,7 +60,10 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V * @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 */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable public final List<Value> loadAfter(int currentEndIndex, @@ -88,7 +93,10 @@ public abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, V * on item contents. * @param pageSize Suggested number of items to load. * @return List of items, in descending order, starting at position currentBeginIndex - 1. + * + * @hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @WorkerThread @Nullable public final List<Value> loadBefore(int currentBeginIndex, diff --git a/android/arch/paging/DataSource.java b/android/arch/paging/DataSource.java index 1e815690..48fbec5f 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.WorkerThread; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -64,6 +65,7 @@ public abstract class DataSource<Key, Value> { * @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(); /** @@ -143,6 +145,7 @@ public abstract class DataSource<Key, Value> { * * @return True if the data source is invalid, and can no longer return data. */ + @WorkerThread public boolean isInvalid() { return mInvalid.get(); } diff --git a/android/arch/paging/KeyedDataSource.java b/android/arch/paging/KeyedDataSource.java index 057eb7f5..8cf6829c 100644 --- a/android/arch/paging/KeyedDataSource.java +++ b/android/arch/paging/KeyedDataSource.java @@ -16,6 +16,7 @@ package android.arch.paging; +import android.support.annotation.AnyThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; @@ -54,7 +55,7 @@ import java.util.List; * {@literal @}SuppressWarnings("FieldCanBeLocal") * private final InvalidationTracker.Observer mObserver; * - * public OffsetUserQueryDataSource(MyDatabase db) { + * public KeyedUserQueryDataSource(MyDatabase db) { * mDb = db; * mUserDao = db.getUserDao(); * mObserver = new InvalidationTracker.Observer("user") { @@ -85,11 +86,15 @@ import java.util.List; * * {@literal @}Override * public List<User> loadBefore({@literal @}NonNull String userName, int pageSize) { + * // Return items adjacent to 'userName' in reverse order + * // it's valid to return a different-sized list of items than pageSize, if it's easier * return mUserDao.userNameLoadBefore(userName, pageSize); * } * * {@literal @}Override * public List<User> loadAfter({@literal @}Nullable String userName, int pageSize) { + * // Return items adjacent to 'userName' + * // it's valid to return a different-sized list of items than pageSize, if it's easier * return mUserDao.userNameLoadAfter(userName, pageSize); * } * }</pre> @@ -198,13 +203,64 @@ public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<K return list; } + /** + * Return a key associated with the given item. + * <p> + * If your KeyedDataSource is loading from a source that is sorted and loaded by a unique + * integer ID, you would return {@code item.getID()} here. This key can then be passed to + * {@link #loadBefore(Key, int)} or {@link #loadAfter(Key, int)} to load additional items + * adjacent to the item passed to this function. + * <p> + * If your key is more complex, such as when you're sorting by name, then resolving collisions + * with integer ID, you'll need to return both. In such a case you would use a wrapper class, + * such as {@code Pair<String, Integer>} or, in Kotlin, + * {@code data class Key(val name: String, val id: Int)} + * + * @param item Item to get the key from. + * @return Key associated with given item. + */ @NonNull + @AnyThread public abstract Key getKey(@NonNull Value item); + /** + * Return the number of items that occur before the item uniquely identified by {@code key} in + * the data set. + * <p> + * For example, if you're loading items sorted by ID, then this would return the total number of + * items with ID less than {@code key}. + * <p> + * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsAfter(Key)}, your + * data source will not present placeholder null items in place of unloaded data. + * + * @param key A unique identifier of an item in the data set. + * @return Number of items in the data set before the item identified by {@code key}, or + * {@link #COUNT_UNDEFINED}. + * + * @see #countItemsAfter(Key) + */ + @WorkerThread public int countItemsBefore(@NonNull Key key) { return COUNT_UNDEFINED; } + /** + * Return the number of items that occur after the item uniquely identified by {@code key} in + * the data set. + * <p> + * For example, if you're loading items sorted by ID, then this would return the total number of + * items with ID greater than {@code key}. + * <p> + * If you return {@link #COUNT_UNDEFINED} here, or from {@link #countItemsBefore(Key)}, your + * data source will not present placeholder null items in place of unloaded data. + * + * @param key A unique identifier of an item in the data set. + * @return Number of items in the data set after the item identified by {@code key}, or + * {@link #COUNT_UNDEFINED}. + * + * @see #countItemsBefore(Key) + */ + @WorkerThread public int countItemsAfter(@NonNull Key key) { return COUNT_UNDEFINED; } @@ -231,10 +287,17 @@ public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<K public abstract List<Value> loadAfter(@NonNull Key currentEndKey, int pageSize); /** - * Load data before the currently loaded content, starting at the provided index. + * Load data before the currently loaded content, starting at the provided index, + * in reverse-display order. * <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 the number loaded than reduce. + * <p class="note"><strong>Note:</strong> Items returned from loadBefore <em>must</em> be in + * reverse order from how they will be presented in the list. The first item in the return list + * will be prepended immediately before the current beginning of the list. This is so that the + * KeyedDataSource may return a different number of items from the requested {@code pageSize} by + * shortening or lengthening the return list as it desires. + * <p> * * @param currentBeginKey Load items before this key. * @param pageSize Suggested number of items to load. diff --git a/android/arch/paging/PagedList.java b/android/arch/paging/PagedList.java index 0d5313dd..6a31b689 100644 --- a/android/arch/paging/PagedList.java +++ b/android/arch/paging/PagedList.java @@ -27,16 +27,65 @@ import java.util.concurrent.Executor; /** * Lazy loading list that pages in content from a {@link DataSource}. * <p> - * A PagedList is a lazy loaded list, which presents data from a {@link DataSource}. If the - * DataSource is counted (returns a valid number from its count method(s)), the PagedList will - * present {@code null} items in place of not-yet-loaded content to serve as placeholders. + * A PagedList is a {@link List} which loads its data in chunks (pages) from a {@link DataSource}. + * Items can be accessed with {@link #get(int)}, and further loading can be triggered with + * {@link #loadAround(int)}. See {@link PagedListAdapter}, which enables the binding of a PagedList + * to a {@link android.support.v7.widget.RecyclerView}. + * <h4>Loading Data</h4> * <p> - * When {@link #loadAround} is called, items will be loaded in near the passed position. If - * placeholder {@code null}s are present in the list, they will be replaced as content is loaded. + * All data in a PagedList is loaded from its {@link DataSource}. Creating a PagedList loads data + * from the DataSource immediately, and should for this reason be done on a background thread. The + * constructed PagedList may then be passed to and used on the UI thread. This is done to prevent + * passing a list with no loaded content to the UI thread, which should generally not be presented + * to the user. * <p> - * In this way, PagedList can present data for an unbounded, infinite scrolling list, or a very - * large but countable list. See {@link PagedListAdapter}, which enables the binding of a PagedList - * to a RecyclerView. Use {@link Config} to control how many items a PagedList loads, and when. + * When {@link #loadAround} is called, items will be loaded in near the passed list index. If + * placeholder {@code null}s are present in the list, they will be replaced as content is + * loaded. If not, newly loaded items will be inserted at the beginning or end of the list. + * <p> + * PagedList can present data for an unbounded, infinite scrolling list, or a very large but + * countable list. Use {@link Config} to control how many items a PagedList loads, and when. + * <p> + * If you use {@link LivePagedListProvider} to get a + * {@link android.arch.lifecycle.LiveData}<PagedList>, it will initialize PagedLists on a + * background thread for you. + * <h4>Placeholders</h4> + * <p> + * There are two ways that PagedList can represent its not-yet-loaded data - with or without + * {@code null} placeholders. + * <p> + * With placeholders, the PagedList is always the full size of the data set. {@code get(N)} returns + * the {@code N}th item in the data set, or {@code null} if its not yet loaded. + * <p> + * Without {@code null} placeholders, the PagedList is the sublist of data that has already been + * loaded. The size of the PagedList is the number of currently loaded items, and {@code get(N)} + * returns the {@code N}th <em>loaded</em> item. This is not necessarily the {@code N}th item in the + * data set. + * <p> + * Placeholders have several benefits: + * <ul> + * <li>They express the full sized list to the presentation layer (often a + * {@link PagedListAdapter}), and so can support scrollbars (without jumping as pages are + * loaded) and fast-scrolling to any position, whether loaded or not. + * <li>They avoid the need for a loading spinner at the end of the loaded list, since the list + * is always full sized. + * </ul> + * <p> + * They also have drawbacks: + * <ul> + * <li>Your Adapter (or other presentation mechanism) needs to account for {@code null} items. + * This often means providing default values in data you bind to a + * {@link android.support.v7.widget.RecyclerView.ViewHolder}. + * <li>They don't work well if your item views are of different sizes, as this will prevent + * loading items from cross-fading nicely. + * <li>They require you to count your data set, which can be expensive or impossible, depending + * on where your data comes from. + * </ul> + * <p> + * Placeholders are enabled by default, but can be disabled in two ways. They are disabled if the + * DataSource returns {@link DataSource#COUNT_UNDEFINED} from any item counting method, or if + * {@code false} is passed to {@link Config.Builder#setEnablePlaceholders(boolean)} when building a + * {@link Config}. * * @param <T> The type of the entries in the list. */ @@ -92,6 +141,18 @@ public abstract class PagedList<T> extends AbstractList<T> { * Builder class for PagedList. * <p> * DataSource, main thread and background executor, and Config must all be provided. + * <p> + * A valid PagedList may not be constructed without data, so building a PagedList queries + * initial data from the data source. This is done because it's generally undesired to present a + * PagedList with no data in it to the UI. It's better to present initial data, so that the UI + * doesn't show an empty list, or placeholders for a few frames, just before showing initial + * content. + * <p> + * Because PagedLists are initialized with data, PagedLists must be built on a background + * thread. + * <p> + * {@link LivePagedListProvider} does this creation on a background thread automatically, if you + * want to receive a {@code LiveData<PagedList<...>>}. * * @param <Key> Type of key used to load data from the DataSource. * @param <Value> Type of items held and loaded by the PagedList. @@ -174,11 +235,17 @@ public abstract class PagedList<T> extends AbstractList<T> { * <p> * This call will initial data and perform any counting needed to initialize the PagedList, * therefore it should only be called on a worker thread. + * <p> + * While build() will always return a PagedList, it's important to note that the PagedList + * initial load may fail to acquire data from the DataSource. This can happen for example if + * the DataSource is invalidated during its initial load. If this happens, the PagedList + * will be immediately {@link PagedList#isDetached() detached}, and you can retry + * construction (including setting a new DataSource). * * @return The newly constructed PagedList */ - @NonNull @WorkerThread + @NonNull public PagedList<Value> build() { if (mDataSource == null) { throw new IllegalArgumentException("DataSource required"); @@ -403,6 +470,16 @@ public abstract class PagedList<T> extends AbstractList<T> { * Defines the number of items loaded at once from the DataSource. * <p> * Should be several times the number of visible items onscreen. + * <p> + * Configuring your page size depends on how your data is being loaded and used. Smaller + * page sizes improve memory usage, latency, and avoid GC churn. Larger pages generally + * improve loading throughput, to a point + * (avoid loading more than 2MB from SQLite at once, since it incurs extra cost). + * <p> + * If you're loading data for very large, social-media style cards that take up most of + * a screen, and your database isn't a bottleneck, 10-20 may make sense. If you're + * displaying dozens of items in a tiled grid, which can present items during a scroll + * much more quickly, consider closer to 100. * * @param pageSize Number of items loaded at once from the DataSource. * @return this @@ -414,12 +491,15 @@ public abstract class PagedList<T> extends AbstractList<T> { /** * Defines how far from the edge of loaded content an access must be to trigger further - * loading. Defaults to page size. - * <p> - * A value of 0 indicates that no list items will be loaded before they are first - * requested. + * loading. * <p> * Should be several times the number of visible items onscreen. + * <p> + * If not set, defaults to page size. + * <p> + * A value of 0 indicates that no list items will be loaded until they are specifically + * requested. This is generally not recommended, so that users don't observe a + * placeholder item (with placeholders) or end of list (without) while scrolling. * * @param prefetchDistance Distance the PagedList should prefetch. * @return this @@ -432,8 +512,10 @@ public abstract class PagedList<T> extends AbstractList<T> { /** * Pass false to disable null placeholders in PagedLists using this Config. * <p> - * A PagedList will present null placeholders for not yet loaded content if two - * contitions are met: + * If not set, defaults to true. + * <p> + * A PagedList will present null placeholders for not-yet-loaded content if two + * conditions are met: * <p> * 1) Its DataSource can count all unloaded items (so that the number of nulls to * present is known). @@ -442,6 +524,13 @@ public abstract class PagedList<T> extends AbstractList<T> { * <p> * Call {@code setEnablePlaceholders(false)} to ensure the receiver of the PagedList * (often a {@link PagedListAdapter}) doesn't need to account for null items. + * <p> + * If placeholders are disabled, not-yet-loaded content will not be present in the list. + * Paging will still occur, but as items are loaded or removed, they will be signaled + * as inserts to the {@link PagedList.Callback}. + * {@link PagedList.Callback#onChanged(int, int)} will not be issued as part of loading, + * though a {@link PagedListAdapter} may still receive change events as a result of + * PagedList diffing. * * @param enablePlaceholders False if null placeholders should be disabled. * @return this diff --git a/android/arch/paging/PagedListAdapter.java b/android/arch/paging/PagedListAdapter.java index 19a0c558..93c02ea3 100644 --- a/android/arch/paging/PagedListAdapter.java +++ b/android/arch/paging/PagedListAdapter.java @@ -51,6 +51,7 @@ import android.support.v7.widget.RecyclerView; * public final LiveData<PagedList<User>> usersList; * public MyViewModel(UserDao userDao) { * usersList = userDao.usersByLastName().create( + * /* initial load position {@literal *}/ 0, * new PagedList.Config.Builder() * .setPageSize(50) * .setPrefetchDistance(50) @@ -72,7 +73,7 @@ import android.support.v7.widget.RecyclerView; * * class UserAdapter extends PagedListAdapter<User, UserViewHolder> { * public UserAdapter() { - * super(User.DIFF_CALLBACK); + * super(DIFF_CALLBACK); * } * {@literal @}Override * public void onBindViewHolder(UserViewHolder holder, int position) { @@ -85,27 +86,21 @@ import android.support.v7.widget.RecyclerView; * holder.clear(); * } * } - * } - * - * {@literal @}Entity - * class User { - * // ... simple POJO code omitted ... - * - * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<Customer>() { - * {@literal @}Override - * public boolean areItemsTheSame( - * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { - * // User properties may have changed if reloaded from the DB, but ID is fixed - * return oldUser.getId() == newUser.getId(); - * } - * {@literal @}Override - * public boolean areContentsTheSame( - * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { - * // NOTE: if you use equals, your object must properly override Object#equals() - * // Incorrectly returning false here will result in too many animations. - * return oldUser.equals(newUser); - * } - * } + * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() { + * {@literal @}Override + * public boolean areItemsTheSame( + * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { + * // User properties may have changed if reloaded from the DB, but ID is fixed + * return oldUser.getId() == newUser.getId(); + * } + * {@literal @}Override + * public boolean areContentsTheSame( + * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { + * // NOTE: if you use equals, your object must properly override Object#equals() + * // Incorrectly returning false here will result in too many animations. + * return oldUser.equals(newUser); + * } + * } * }</pre> * * Advanced users that wish for more control over adapter behavior, or to provide a specific base diff --git a/android/arch/paging/PagedListAdapterHelper.java b/android/arch/paging/PagedListAdapterHelper.java index 4bff5fcf..c7b61d9f 100644 --- a/android/arch/paging/PagedListAdapterHelper.java +++ b/android/arch/paging/PagedListAdapterHelper.java @@ -57,6 +57,7 @@ import java.util.List; * public final LiveData<PagedList<User>> usersList; * public MyViewModel(UserDao userDao) { * usersList = userDao.usersByLastName().create( + * /* initial load position {@literal *}/ 0, * new PagedList.Config.Builder() * .setPageSize(50) * .setPrefetchDistance(50) @@ -79,7 +80,7 @@ import java.util.List; * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> { * private final PagedListAdapterHelper<User> mHelper; * public UserAdapter(PagedListAdapterHelper.Builder<User> builder) { - * mHelper = new PagedListAdapterHelper(this, User.DIFF_CALLBACK); + * mHelper = new PagedListAdapterHelper(this, DIFF_CALLBACK); * } * {@literal @}Override * public int getItemCount() { @@ -99,13 +100,7 @@ import java.util.List; * holder.clear(); * } * } - * } - * - * {@literal @}Entity - * class User { - * // ... simple POJO code omitted ... - * - * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<Customer>() { + * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() { * {@literal @}Override * public boolean areItemsTheSame( * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { diff --git a/android/arch/paging/TestExecutor.java b/android/arch/paging/TestExecutor.java index 976f7df5..30809c3e 100644 --- a/android/arch/paging/TestExecutor.java +++ b/android/arch/paging/TestExecutor.java @@ -30,7 +30,7 @@ public class TestExecutor implements Executor { mTasks.add(command); } - public boolean executeAll() { + boolean executeAll() { boolean consumed = !mTasks.isEmpty(); Runnable task; while ((task = mTasks.poll()) != null) { diff --git a/android/arch/paging/TiledDataSource.java b/android/arch/paging/TiledDataSource.java index 56556cd4..36be423d 100644 --- a/android/arch/paging/TiledDataSource.java +++ b/android/arch/paging/TiledDataSource.java @@ -17,6 +17,7 @@ package android.arch.paging; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; import java.util.List; @@ -90,6 +91,7 @@ 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(); @@ -100,14 +102,20 @@ public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> { /** * Called to load items at from the specified position range. + * <p> + * This method must return a list of requested size, unless at the end of list. Fixed size pages + * enable TiledDataSource to navigate tiles efficiently, and quickly accesss any position in the + * data set. + * <p> + * If a list of a different size is returned, but it is not the last list in the data set based + * on the return value from {@link #countItems()}, an exception will be thrown. * * @param startPosition Index of first item to load. - * @param count Exact number of items to load. Returning a different number will cause - * an exception to be thrown. - * @return List of loaded items. Null if the DataSource is no longer valid, and should - * not be queried again. + * @param count Number of items to load. + * @return List of loaded items, of the requested length unless at end of list. Null if the + * DataSource is no longer valid, and should not be queried again. */ - @SuppressWarnings("WeakerAccess") + @WorkerThread public abstract List<Type> loadRange(int startPosition, int count); final List<Type> loadRangeWrapper(int startPosition, int count) { @@ -132,6 +140,7 @@ public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> { mTiledDataSource = tiledDataSource; } + @WorkerThread @Nullable @Override public List<Value> loadRange(int startPosition, int loadCount) { diff --git a/android/arch/persistence/db/SupportSQLiteOpenHelper.java b/android/arch/persistence/db/SupportSQLiteOpenHelper.java index 5a96e5ac..02e4e7dc 100644 --- a/android/arch/persistence/db/SupportSQLiteOpenHelper.java +++ b/android/arch/persistence/db/SupportSQLiteOpenHelper.java @@ -17,13 +17,18 @@ package android.arch.persistence.db; import android.content.Context; -import android.database.DatabaseErrorHandler; -import android.database.DefaultDatabaseErrorHandler; +import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; +import android.util.Log; +import android.util.Pair; + +import java.io.File; +import java.io.IOException; +import java.util.List; /** * An interface to map the behavior of {@link android.database.sqlite.SQLiteOpenHelper}. @@ -99,10 +104,29 @@ public interface SupportSQLiteOpenHelper { void close(); /** - * Matching callback methods from {@link android.database.sqlite.SQLiteOpenHelper}. + * Handles various lifecycle events for the SQLite connection, similar to + * {@link android.database.sqlite.SQLiteOpenHelper}. */ @SuppressWarnings({"unused", "WeakerAccess"}) abstract class Callback { + private static final String TAG = "SupportSQLite"; + /** + * Version number of the database (starting at 1); if the database is older, + * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)} + * will be used to upgrade the database; if the database is newer, + * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)} + * will be used to downgrade the database. + */ + public final int version; + + /** + * Creates a new Callback to get database lifecycle events. + * @param version The version for the database instance. See {@link #version}. + */ + public Callback(int version) { + this.version = version; + } + /** * Called when the database connection is being configured, to enable features such as * write-ahead logging or foreign key support. @@ -193,6 +217,81 @@ public interface SupportSQLiteOpenHelper { public void onOpen(SupportSQLiteDatabase db) { } + + /** + * The method invoked when database corruption is detected. Default implementation will + * delete the database file. + * + * @param db the {@link SupportSQLiteDatabase} object representing the database on which + * corruption is detected. + */ + public void onCorruption(SupportSQLiteDatabase db) { + // the following implementation is taken from {@link DefaultDatabaseErrorHandler}. + + Log.e(TAG, "Corruption reported by sqlite on database: " + db.getPath()); + // is the corruption detected even before database could be 'opened'? + if (!db.isOpen()) { + // database files are not even openable. delete this database file. + // NOTE if the database has attached databases, then any of them could be corrupt. + // and not deleting all of them could cause corrupted database file to remain and + // make the application crash on database open operation. To avoid this problem, + // the application should provide its own {@link DatabaseErrorHandler} impl class + // to delete ALL files of the database (including the attached databases). + deleteDatabaseFile(db.getPath()); + return; + } + + List<Pair<String, String>> attachedDbs = null; + try { + // Close the database, which will cause subsequent operations to fail. + // before that, get the attached database list first. + try { + attachedDbs = db.getAttachedDbs(); + } catch (SQLiteException e) { + /* ignore */ + } + try { + db.close(); + } catch (IOException e) { + /* ignore */ + } + } finally { + // Delete all files of this corrupt database and/or attached databases + if (attachedDbs != null) { + for (Pair<String, String> p : attachedDbs) { + deleteDatabaseFile(p.second); + } + } else { + // attachedDbs = null is possible when the database is so corrupt that even + // "PRAGMA database_list;" also fails. delete the main database file + deleteDatabaseFile(db.getPath()); + } + } + } + + private void deleteDatabaseFile(String fileName) { + if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { + return; + } + Log.w(TAG, "deleting the database file: " + fileName); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + SQLiteDatabase.deleteDatabase(new File(fileName)); + } else { + try { + final boolean deleted = new File(fileName).delete(); + if (!deleted) { + Log.e(TAG, "Could not delete the database file " + fileName); + } + } catch (Exception error) { + Log.e(TAG, "error while deleting corrupted database file", error); + } + } + } catch (Exception e) { + /* print warning and ignore exception */ + Log.w(TAG, "delete failed: ", e); + } + } } /** @@ -211,33 +310,15 @@ public interface SupportSQLiteOpenHelper { @Nullable public final String name; /** - * Version number of the database (starting at 1); if the database is older, - * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)} - * will be used to upgrade the database; if the database is newer, - * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)} - * will be used to downgrade the database. - */ - public final int version; - /** * The callback class to handle creation, upgrade and downgrade. */ @NonNull public final SupportSQLiteOpenHelper.Callback callback; - /** - * The {@link DatabaseErrorHandler} to be used when sqlite reports database - * corruption, or null to use the default error handler. - */ - @Nullable - public final DatabaseErrorHandler errorHandler; - Configuration(@NonNull Context context, @Nullable String name, - int version, @Nullable DatabaseErrorHandler errorHandler, - @NonNull Callback callback) { + Configuration(@NonNull Context context, @Nullable String name, @NonNull Callback callback) { this.context = context; this.name = name; - this.version = version; this.callback = callback; - this.errorHandler = errorHandler; } /** @@ -255,9 +336,7 @@ public interface SupportSQLiteOpenHelper { public static class Builder { Context mContext; String mName; - int mVersion = 1; SupportSQLiteOpenHelper.Callback mCallback; - DatabaseErrorHandler mErrorHandler; public Configuration build() { if (mCallback == null) { @@ -268,11 +347,7 @@ public interface SupportSQLiteOpenHelper { throw new IllegalArgumentException("Must set a non-null context to create" + " the configuration."); } - if (mErrorHandler == null) { - mErrorHandler = new DefaultDatabaseErrorHandler(); - } - return new Configuration(mContext, mName, mVersion, mErrorHandler, - mCallback); + return new Configuration(mContext, mName, mCallback); } Builder(@NonNull Context context) { @@ -280,17 +355,6 @@ public interface SupportSQLiteOpenHelper { } /** - * @param errorHandler The {@link DatabaseErrorHandler} to be used when sqlite - * reports database corruption, or null to use the default error - * handler. - * @return This - */ - public Builder errorHandler(@Nullable DatabaseErrorHandler errorHandler) { - mErrorHandler = errorHandler; - return this; - } - - /** * @param name Name of the database file, or null for an in-memory database. * @return This */ @@ -307,20 +371,6 @@ public interface SupportSQLiteOpenHelper { mCallback = callback; return this; } - - /** - * @param version Version number of the database (starting at 1); if the database is - * older, - * {@link SupportSQLiteOpenHelper.Callback#onUpgrade(SupportSQLiteDatabase, int, int)} - * will be used to upgrade the database; if the database is newer, - * {@link SupportSQLiteOpenHelper.Callback#onDowngrade(SupportSQLiteDatabase, int, int)} - * will be used to downgrade the database. - * @return this - */ - public Builder version(int version) { - mVersion = version; - return this; - } } } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java index 92a58205..e9c2b741 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java @@ -53,8 +53,7 @@ class FrameworkSQLiteDatabase implements SupportSQLiteDatabase { * * @param delegate The delegate to receive all calls. */ - @SuppressWarnings("WeakerAccess") - public FrameworkSQLiteDatabase(SQLiteDatabase delegate) { + FrameworkSQLiteDatabase(SQLiteDatabase delegate) { mDelegate = delegate; } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java index aa08fa42..a1690f40 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelper.java @@ -28,42 +28,14 @@ import android.support.annotation.RequiresApi; class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper { private final OpenHelper mDelegate; - FrameworkSQLiteOpenHelper(Context context, String name, int version, - DatabaseErrorHandler errorHandler, - SupportSQLiteOpenHelper.Callback callback) { - mDelegate = createDelegate(context, name, version, errorHandler, callback); + FrameworkSQLiteOpenHelper(Context context, String name, + Callback callback) { + mDelegate = createDelegate(context, name, callback); } - private OpenHelper createDelegate(Context context, String name, - int version, DatabaseErrorHandler errorHandler, - final Callback callback) { - return new OpenHelper(context, name, null, version, errorHandler) { - @Override - public void onCreate(SQLiteDatabase sqLiteDatabase) { - mWrappedDb = new FrameworkSQLiteDatabase(sqLiteDatabase); - callback.onCreate(mWrappedDb); - } - - @Override - public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { - callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion); - } - - @Override - public void onConfigure(SQLiteDatabase db) { - callback.onConfigure(getWrappedDb(db)); - } - - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion); - } - - @Override - public void onOpen(SQLiteDatabase db) { - callback.onOpen(getWrappedDb(db)); - } - }; + private OpenHelper createDelegate(Context context, String name, Callback callback) { + final FrameworkSQLiteDatabase[] dbRef = new FrameworkSQLiteDatabase[1]; + return new OpenHelper(context, name, dbRef, callback); } @Override @@ -92,14 +64,29 @@ class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper { mDelegate.close(); } - abstract static class OpenHelper extends SQLiteOpenHelper { - - FrameworkSQLiteDatabase mWrappedDb; - - OpenHelper(Context context, String name, - SQLiteDatabase.CursorFactory factory, int version, - DatabaseErrorHandler errorHandler) { - super(context, name, factory, version, errorHandler); + static class OpenHelper extends SQLiteOpenHelper { + /** + * This is used as an Object reference so that we can access the wrapped database inside + * the constructor. SQLiteOpenHelper requires the error handler to be passed in the + * constructor. + */ + final FrameworkSQLiteDatabase[] mDbRef; + final Callback mCallback; + + OpenHelper(Context context, String name, final FrameworkSQLiteDatabase[] dbRef, + final Callback callback) { + super(context, name, null, callback.version, + new DatabaseErrorHandler() { + @Override + public void onCorruption(SQLiteDatabase dbObj) { + FrameworkSQLiteDatabase db = dbRef[0]; + if (db != null) { + callback.onCorruption(db); + } + } + }); + mCallback = callback; + mDbRef = dbRef; } SupportSQLiteDatabase getWritableSupportDatabase() { @@ -113,16 +100,43 @@ class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper { } FrameworkSQLiteDatabase getWrappedDb(SQLiteDatabase sqLiteDatabase) { - if (mWrappedDb == null) { - mWrappedDb = new FrameworkSQLiteDatabase(sqLiteDatabase); + FrameworkSQLiteDatabase dbRef = mDbRef[0]; + if (dbRef == null) { + dbRef = new FrameworkSQLiteDatabase(sqLiteDatabase); + mDbRef[0] = dbRef; } - return mWrappedDb; + return mDbRef[0]; + } + + @Override + public void onCreate(SQLiteDatabase sqLiteDatabase) { + mCallback.onCreate(getWrappedDb(sqLiteDatabase)); + } + + @Override + public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { + mCallback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion); + } + + @Override + public void onConfigure(SQLiteDatabase db) { + mCallback.onConfigure(getWrappedDb(db)); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + mCallback.onDowngrade(getWrappedDb(db), oldVersion, newVersion); + } + + @Override + public void onOpen(SQLiteDatabase db) { + mCallback.onOpen(getWrappedDb(db)); } @Override public synchronized void close() { super.close(); - mWrappedDb = null; + mDbRef[0] = null; } } } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java index 2268f45f..ab11d490 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteOpenHelperFactory.java @@ -27,8 +27,6 @@ public final class FrameworkSQLiteOpenHelperFactory implements SupportSQLiteOpen @Override public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) { return new FrameworkSQLiteOpenHelper( - configuration.context, configuration.name, - configuration.version, configuration.errorHandler, configuration.callback - ); + configuration.context, configuration.name, configuration.callback); } } diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java index a2daf12e..53a04bd6 100644 --- a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java +++ b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java @@ -30,8 +30,7 @@ class FrameworkSQLiteStatement implements SupportSQLiteStatement { * * @param delegate The SQLiteStatement to delegate calls to. */ - @SuppressWarnings("WeakerAccess") - public FrameworkSQLiteStatement(SQLiteStatement delegate) { + FrameworkSQLiteStatement(SQLiteStatement delegate) { mDelegate = delegate; } diff --git a/android/arch/persistence/room/Entity.java b/android/arch/persistence/room/Entity.java index f54f0f80..94ca3bfc 100644 --- a/android/arch/persistence/room/Entity.java +++ b/android/arch/persistence/room/Entity.java @@ -36,6 +36,9 @@ import java.lang.annotation.Target; * When a class is marked as an Entity, all of its fields are persisted. If you would like to * exclude some of its fields, you can mark them with {@link Ignore}. * <p> + * If a field is {@code transient}, it is automatically ignored <b>unless</b> it is annotated with + * {@link ColumnInfo}, {@link Embedded} or {@link Relation}. + * <p> * Example: * <pre> * {@literal @}Entity diff --git a/android/arch/persistence/room/ForeignKey.java b/android/arch/persistence/room/ForeignKey.java index 4ba0fb3f..3ba632b5 100644 --- a/android/arch/persistence/room/ForeignKey.java +++ b/android/arch/persistence/room/ForeignKey.java @@ -40,7 +40,7 @@ import android.support.annotation.IntDef; * <a href="https://sqlite.org/pragma.html#pragma_defer_foreign_keys">defer_foreign_keys</a> PRAGMA * to defer them depending on your transaction. * <p> - * Please refer to the SQLite <a href="https://sqlite.org/foreignkeys.html>foreign keys</a> + * Please refer to the SQLite <a href="https://sqlite.org/foreignkeys.html">foreign keys</a> * documentation for details. */ public @interface ForeignKey { diff --git a/android/arch/persistence/room/InvalidationTracker.java b/android/arch/persistence/room/InvalidationTracker.java index 33bc4ed6..45ec0289 100644 --- a/android/arch/persistence/room/InvalidationTracker.java +++ b/android/arch/persistence/room/InvalidationTracker.java @@ -16,7 +16,7 @@ package android.arch.persistence.room; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.internal.SafeIterableMap; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteStatement; @@ -166,10 +166,12 @@ public class InvalidationTracker { private static void appendTriggerName(StringBuilder builder, String tableName, String triggerType) { - builder.append("room_table_modification_trigger_") + builder.append("`") + .append("room_table_modification_trigger_") .append(tableName) .append("_") - .append(triggerType); + .append(triggerType) + .append("`"); } private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) { @@ -192,9 +194,9 @@ public class InvalidationTracker { appendTriggerName(stringBuilder, tableName, trigger); stringBuilder.append(" AFTER ") .append(trigger) - .append(" ON ") + .append(" ON `") .append(tableName) - .append(" BEGIN INSERT OR REPLACE INTO ") + .append("` BEGIN INSERT OR REPLACE INTO ") .append(UPDATE_TABLE_NAME) .append(" VALUES(null, ") .append(tableId) @@ -238,7 +240,7 @@ public class InvalidationTracker { currentObserver = mObserverMap.putIfAbsent(observer, wrapper); } if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) { - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers); + ArchTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers); } } @@ -269,7 +271,7 @@ public class InvalidationTracker { wrapper = mObserverMap.remove(observer); } if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) { - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers); + ArchTaskExecutor.getInstance().executeOnDiskIO(mSyncTriggers); } } @@ -350,11 +352,18 @@ public class InvalidationTracker { return; } - if (mDatabase.inTransaction() - || !mPendingRefresh.compareAndSet(true, false)) { + if (!mPendingRefresh.compareAndSet(true, false)) { // no pending refresh return; } + + if (mDatabase.inTransaction()) { + // current thread is in a transaction. when it ends, it will invoke + // refreshRunnable again. mPendingRefresh is left as false on purpose + // so that the last transaction can flip it on again. + return; + } + mCleanupStatement.executeUpdateDelete(); mQueryArgs[0] = mMaxVersion; Cursor cursor = mDatabase.query(SELECT_UPDATED_TABLES_SQL, mQueryArgs); @@ -400,7 +409,7 @@ public class InvalidationTracker { public void refreshVersionsAsync() { // TODO we should consider doing this sync instead of async. if (mPendingRefresh.compareAndSet(false, true)) { - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); + ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); } } diff --git a/android/arch/persistence/room/InvalidationTrackerTest.java b/android/arch/persistence/room/InvalidationTrackerTest.java index f0b730ad..d7474fd1 100644 --- a/android/arch/persistence/room/InvalidationTrackerTest.java +++ b/android/arch/persistence/room/InvalidationTrackerTest.java @@ -247,7 +247,7 @@ public class InvalidationTrackerTest { mTracker.mRefreshRunnable.run(); } - @Test + // @Test - disabled due to flakiness b/65257997 public void closedDbAfterOpen() throws InterruptedException { setVersions(3, 1); mTracker.addObserver(new LatchObserver(1, "a", "b")); diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java index e64f2d61..cdad868d 100644 --- a/android/arch/persistence/room/RoomDatabase.java +++ b/android/arch/persistence/room/RoomDatabase.java @@ -16,7 +16,7 @@ package android.arch.persistence.room; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.persistence.db.SimpleSQLiteQuery; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteOpenHelper; @@ -158,7 +158,7 @@ public abstract class RoomDatabase { if (mAllowMainThreadQueries) { return; } - if (AppToolkitTaskExecutor.getInstance().isMainThread()) { + if (ArchTaskExecutor.getInstance().isMainThread()) { throw new IllegalStateException("Cannot access database on the main thread since" + " it may potentially lock the UI for a long period of time."); } @@ -216,7 +216,11 @@ public abstract class RoomDatabase { */ public void endTransaction() { mOpenHelper.getWritableDatabase().endTransaction(); - mInvalidationTracker.refreshVersionsAsync(); + if (!inTransaction()) { + // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last + // endTransaction call to do it. + mInvalidationTracker.refreshVersionsAsync(); + } } /** @@ -311,7 +315,6 @@ public abstract class RoomDatabase { private ArrayList<Callback> mCallbacks; private SupportSQLiteOpenHelper.Factory mFactory; - private boolean mInMemory; private boolean mAllowMainThreadQueries; private boolean mRequireMigration; /** @@ -381,6 +384,9 @@ public abstract class RoomDatabase { } /** + * Allows Room to destructively recreate database tables if {@link Migration}s that would + * migrate old database schemas to the latest schema version are not found. + * <p> * When the database version on the device does not match the latest schema version, Room * runs necessary {@link Migration}s on the database. * <p> diff --git a/android/arch/persistence/room/RoomOpenHelper.java b/android/arch/persistence/room/RoomOpenHelper.java index 8767f065..47279d60 100644 --- a/android/arch/persistence/room/RoomOpenHelper.java +++ b/android/arch/persistence/room/RoomOpenHelper.java @@ -44,6 +44,7 @@ public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback { public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate, @NonNull String identityHash) { + super(delegate.version); mConfiguration = configuration; mDelegate = delegate; mIdentityHash = identityHash; @@ -135,6 +136,12 @@ public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback { */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract static class Delegate { + public final int version; + + public Delegate(int version) { + this.version = version; + } + protected abstract void dropAllTables(SupportSQLiteDatabase database); protected abstract void createAllTables(SupportSQLiteDatabase database); diff --git a/android/arch/persistence/room/RoomWarnings.java b/android/arch/persistence/room/RoomWarnings.java index 91f32e45..c64be967 100644 --- a/android/arch/persistence/room/RoomWarnings.java +++ b/android/arch/persistence/room/RoomWarnings.java @@ -117,4 +117,12 @@ public class RoomWarnings { */ public static final String MISSING_INDEX_ON_FOREIGN_KEY_CHILD = "ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX"; + + /** + * Reported when a Pojo has multiple constructors, one of which is a no-arg constructor. Room + * will pick that one by default but will print this warning in case the constructor choice is + * important. You can always guide Room to use the right constructor using the @Ignore + * annotation. + */ + public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR"; } diff --git a/android/arch/persistence/room/RxRoom.java b/android/arch/persistence/room/RxRoom.java index adfca27b..285b3f89 100644 --- a/android/arch/persistence/room/RxRoom.java +++ b/android/arch/persistence/room/RxRoom.java @@ -16,7 +16,7 @@ package android.arch.persistence.room; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; @@ -133,7 +133,7 @@ public class RxRoom { public Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { DisposableRunnable disposable = new DisposableRunnable(run, mDisposed); - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(run); + ArchTaskExecutor.getInstance().executeOnDiskIO(run); return disposable; } diff --git a/android/arch/persistence/room/Transaction.java b/android/arch/persistence/room/Transaction.java new file mode 100644 index 00000000..914e4f41 --- /dev/null +++ b/android/arch/persistence/room/Transaction.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.persistence.room; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method in an abstract {@link Dao} class as a transaction method. + * <p> + * 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> + * Example: + * <pre> + * {@literal @}Dao + * public abstract class ProductDao { + * {@literal @}Insert + * public abstract void insert(Product product); + * {@literal @}Delete + * public abstract void delete(Product product); + * {@literal @}Transaction + * public void insertAndDeleteInTransaction(Product newProduct, Product oldProduct) { + * // Anything inside this method runs in a single transaction. + * insert(newProduct); + * delete(oldProduct); + * } + * } + * </pre> + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface Transaction { +} diff --git a/android/arch/persistence/room/integration/testapp/CustomerViewModel.java b/android/arch/persistence/room/integration/testapp/CustomerViewModel.java index 1f434ad6..320b2cdd 100644 --- a/android/arch/persistence/room/integration/testapp/CustomerViewModel.java +++ b/android/arch/persistence/room/integration/testapp/CustomerViewModel.java @@ -17,7 +17,7 @@ package android.arch.persistence.room.integration.testapp; import android.app.Application; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.lifecycle.AndroidViewModel; import android.arch.lifecycle.LiveData; import android.arch.paging.DataSource; @@ -47,7 +47,7 @@ public class CustomerViewModel extends AndroidViewModel { mDatabase = Room.databaseBuilder(this.getApplication(), SampleDatabase.class, "customerDatabase").build(); - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(new Runnable() { + ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() { @Override public void run() { // fill with some simple data @@ -73,7 +73,7 @@ public class CustomerViewModel extends AndroidViewModel { } void insertCustomer() { - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(new Runnable() { + ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() { @Override public void run() { mDatabase.getCustomerDao().insert(createCustomer()); diff --git a/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java b/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java index e61d808c..63b95072 100644 --- a/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java +++ b/android/arch/persistence/room/integration/testapp/PKeyTestDatabase.java @@ -23,13 +23,18 @@ import android.arch.persistence.room.Query; import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.integration.testapp.vo.IntAutoIncPKeyEntity; import android.arch.persistence.room.integration.testapp.vo.IntegerAutoIncPKeyEntity; +import android.arch.persistence.room.integration.testapp.vo.IntegerPKeyEntity; +import android.arch.persistence.room.integration.testapp.vo.ObjectPKeyEntity; import java.util.List; -@Database(entities = {IntAutoIncPKeyEntity.class, IntegerAutoIncPKeyEntity.class}, version = 1, +@Database(entities = {IntAutoIncPKeyEntity.class, IntegerAutoIncPKeyEntity.class, + ObjectPKeyEntity.class, IntegerPKeyEntity.class}, version = 1, exportSchema = false) public abstract class PKeyTestDatabase extends RoomDatabase { public abstract IntPKeyDao intPKeyDao(); + public abstract IntegerAutoIncPKeyDao integerAutoIncPKeyDao(); + public abstract ObjectPKeyDao objectPKeyDao(); public abstract IntegerPKeyDao integerPKeyDao(); @Dao @@ -50,9 +55,10 @@ public abstract class PKeyTestDatabase extends RoomDatabase { } @Dao - public interface IntegerPKeyDao { + public interface IntegerAutoIncPKeyDao { @Insert - void insertMe(IntegerAutoIncPKeyEntity items); + void insertMe(IntegerAutoIncPKeyEntity item); + @Query("select * from IntegerAutoIncPKeyEntity WHERE pKey = :key") IntegerAutoIncPKeyEntity getMe(int key); @@ -65,4 +71,19 @@ public abstract class PKeyTestDatabase extends RoomDatabase { @Query("select data from IntegerAutoIncPKeyEntity WHERE pKey IN(:ids)") List<String> loadDataById(long... ids); } + + @Dao + public interface ObjectPKeyDao { + @Insert + void insertMe(ObjectPKeyEntity item); + } + + @Dao + public interface IntegerPKeyDao { + @Insert + void insertMe(IntegerPKeyEntity item); + + @Query("select * from IntegerPKeyEntity") + List<IntegerPKeyEntity> loadAll(); + } } diff --git a/android/arch/persistence/room/integration/testapp/TestDatabase.java b/android/arch/persistence/room/integration/testapp/TestDatabase.java index 94172965..2fad7b1f 100644 --- a/android/arch/persistence/room/integration/testapp/TestDatabase.java +++ b/android/arch/persistence/room/integration/testapp/TestDatabase.java @@ -21,6 +21,7 @@ import android.arch.persistence.room.RoomDatabase; import android.arch.persistence.room.TypeConverter; import android.arch.persistence.room.TypeConverters; import android.arch.persistence.room.integration.testapp.dao.BlobEntityDao; +import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao; import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao; import android.arch.persistence.room.integration.testapp.dao.PetDao; import android.arch.persistence.room.integration.testapp.dao.ProductDao; @@ -31,6 +32,7 @@ import android.arch.persistence.room.integration.testapp.dao.UserDao; import android.arch.persistence.room.integration.testapp.dao.UserPetDao; import android.arch.persistence.room.integration.testapp.dao.WithClauseDao; import android.arch.persistence.room.integration.testapp.vo.BlobEntity; +import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity; import android.arch.persistence.room.integration.testapp.vo.Pet; import android.arch.persistence.room.integration.testapp.vo.PetCouple; import android.arch.persistence.room.integration.testapp.vo.Product; @@ -41,7 +43,7 @@ import android.arch.persistence.room.integration.testapp.vo.User; import java.util.Date; @Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class, - BlobEntity.class, Product.class}, + BlobEntity.class, Product.class, FunnyNamedEntity.class}, version = 1, exportSchema = false) @TypeConverters(TestDatabase.Converters.class) public abstract class TestDatabase extends RoomDatabase { @@ -55,6 +57,7 @@ public abstract class TestDatabase extends RoomDatabase { public abstract ProductDao getProductDao(); public abstract SpecificDogDao getSpecificDogDao(); public abstract WithClauseDao getWithClauseDao(); + public abstract FunnyNamedDao getFunnyNamedDao(); @SuppressWarnings("unused") public static class Converters { diff --git a/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java b/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java new file mode 100644 index 00000000..93b5e72e --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/dao/FunnyNamedDao.java @@ -0,0 +1,50 @@ +/* + * 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.dao; + +import static android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity.COLUMN_ID; +import static android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity.TABLE_NAME; + +import android.arch.lifecycle.LiveData; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Delete; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Update; +import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity; + +import java.util.List; + +@Dao +public interface FunnyNamedDao { + String SELECT_ONE = "select * from \"" + TABLE_NAME + "\" WHERE \"" + COLUMN_ID + "\" = :id"; + @Insert + void insert(FunnyNamedEntity... entities); + @Delete + void delete(FunnyNamedEntity... entities); + @Update + void update(FunnyNamedEntity... entities); + + @Query("select * from \"" + TABLE_NAME + "\" WHERE \"" + COLUMN_ID + "\" IN (:ids)") + List<FunnyNamedEntity> loadAll(int... ids); + + @Query(SELECT_ONE) + LiveData<FunnyNamedEntity> observableOne(int id); + + @Query(SELECT_ONE) + FunnyNamedEntity load(int id); +} diff --git a/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java b/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java index 7bb137fe..18e8d93e 100644 --- a/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/SchoolDao.java @@ -35,16 +35,16 @@ public abstract class SchoolDao { @Query("SELECT * from School WHERE address_street LIKE '%' || :street || '%'") public abstract List<School> findByStreet(String street); - @Query("SELECT mName, manager_mName FROM School") + @Query("SELECT mId, mName, manager_mName FROM School") public abstract List<School> schoolAndManagerNames(); - @Query("SELECT mName, manager_mName FROM School") + @Query("SELECT mId, mName, manager_mName FROM School") @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) public abstract List<SchoolRef> schoolAndManagerNamesAsPojo(); @Query("SELECT address_lat as lat, address_lng as lng FROM School WHERE mId = :schoolId") public abstract Coordinates loadCoordinates(int schoolId); - @Query("SELECT address_lat, address_lng FROM School WHERE mId = :schoolId") + @Query("SELECT mId, address_lat, address_lng FROM School WHERE mId = :schoolId") public abstract School loadCoordinatesAsSchool(int schoolId); } diff --git a/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/android/arch/persistence/room/integration/testapp/dao/UserDao.java index 337c233f..665a1aeb 100644 --- a/android/arch/persistence/room/integration/testapp/dao/UserDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/UserDao.java @@ -24,6 +24,7 @@ import android.arch.persistence.room.Delete; import android.arch.persistence.room.Insert; import android.arch.persistence.room.OnConflictStrategy; import android.arch.persistence.room.Query; +import android.arch.persistence.room.Transaction; import android.arch.persistence.room.Update; import android.arch.persistence.room.integration.testapp.TestDatabase; import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge; @@ -259,4 +260,10 @@ public abstract class UserDao { + " WHERE mLastName > :lastName or (mLastName = :lastName and (mName < :name or (mName = :name and mId > :id)))" + " ORDER BY mLastName ASC, mName DESC, mId ASC") public abstract int userComplexCountBefore(String lastName, String name, int id); + + @Transaction + public void insertBothByAnnotation(final User a, final User b) { + insert(a); + insert(b); + } } diff --git a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java index 3507aeea..eb159014 100644 --- a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java @@ -33,6 +33,8 @@ import android.arch.persistence.room.integration.testapp.vo.UserWithPetsAndToys; import java.util.List; +import io.reactivex.Flowable; + @Dao public interface UserPetDao { @Query("SELECT * FROM User u, Pet p WHERE u.mId = p.mUserId") @@ -62,6 +64,9 @@ public interface UserPetDao { @Query("SELECT * FROM User u where u.mId = :userId") LiveData<UserAndAllPets> liveUserWithPets(int userId); + @Query("SELECT * FROM User u where u.mId = :userId") + Flowable<UserAndAllPets> flowableUserWithPets(int userId); + @Query("SELECT * FROM User u where u.mId = :uid") EmbeddedUserAndAllPets loadUserAndPetsAsEmbedded(int uid); diff --git a/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java b/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java index b1c38eda..40098ed4 100644 --- a/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java +++ b/android/arch/persistence/room/integration/testapp/dao/WithClauseDao.java @@ -16,13 +16,16 @@ package android.arch.persistence.room.integration.testapp.dao; +import android.annotation.TargetApi; import android.arch.lifecycle.LiveData; import android.arch.persistence.room.Dao; import android.arch.persistence.room.Query; +import android.os.Build; import java.util.List; @Dao +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public interface WithClauseDao { @Query("WITH RECURSIVE factorial(n, fact) AS \n" + "(SELECT 0, 1 \n" diff --git a/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java b/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java index eec59f6a..9020eb16 100644 --- a/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java +++ b/android/arch/persistence/room/integration/testapp/database/SampleDatabase.java @@ -18,10 +18,6 @@ package android.arch.persistence.room.integration.testapp.database; import android.arch.persistence.room.Database; import android.arch.persistence.room.RoomDatabase; -import android.arch.persistence.room.TypeConverter; -import android.arch.persistence.room.TypeConverters; - -import java.util.Date; /** * Sample database of customers. diff --git a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java index 725d53f8..7fe2bc94 100644 --- a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java +++ b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java @@ -318,6 +318,8 @@ public class MigrationTest { + " (`id` INTEGER NOT NULL, `name` TEXT COLLATE NOCASE, PRIMARY KEY(`id`)," + " FOREIGN KEY(`name`) REFERENCES `Entity1`(`name`)" + " ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)"); + database.execSQL("CREATE UNIQUE INDEX `index_entity1` ON " + + MigrationDb.Entity1.TABLE_NAME + " (`name`)"); } }; diff --git a/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java b/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java index 4c9d73e1..df70a170 100644 --- a/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java +++ b/android/arch/persistence/room/integration/testapp/paging/LivePagedListProviderTest.java @@ -21,17 +21,17 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.testing.CountingTaskExecutorRule; import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.LifecycleRegistry; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.Observer; +import android.arch.paging.PagedList; import android.arch.persistence.room.integration.testapp.test.TestDatabaseTest; import android.arch.persistence.room.integration.testapp.test.TestUtil; import android.arch.persistence.room.integration.testapp.vo.User; -import android.arch.paging.PagedList; import android.support.annotation.Nullable; import android.support.test.filters.LargeTest; import android.support.test.runner.AndroidJUnit4; @@ -131,7 +131,7 @@ public class LivePagedListProviderTest extends TestDatabaseTest { return null; } }); - AppToolkitTaskExecutor.getInstance().executeOnMainThread(futureTask); + ArchTaskExecutor.getInstance().executeOnMainThread(futureTask); futureTask.get(); } @@ -155,7 +155,7 @@ public class LivePagedListProviderTest extends TestDatabaseTest { private static class PagedListObserver<T> implements Observer<PagedList<T>> { private PagedList<T> mList; - public void reset() { + void reset() { mList = null; } diff --git a/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java b/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java index 353c2e39..6f44546b 100644 --- a/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java +++ b/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java @@ -55,7 +55,6 @@ public class CustomDatabaseTest { Customer customer = new Customer(); for (int i = 0; i < 100; i++) { SampleDatabase db = builder.build(); - customer.setId(i); db.getCustomerDao().insert(customer); // Give InvalidationTracker enough time to start #mRefreshRunnable and pass the // initialization check. diff --git a/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java b/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java new file mode 100644 index 00000000..f4fca7f2 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/test/FunnyNamedDaoTest.java @@ -0,0 +1,96 @@ +/* + * 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.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import android.arch.core.executor.testing.CountingTaskExecutorRule; +import android.arch.lifecycle.Observer; +import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity; +import android.support.annotation.Nullable; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class FunnyNamedDaoTest extends TestDatabaseTest { + @Rule + public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule(); + + @Test + public void readWrite() { + FunnyNamedEntity entity = new FunnyNamedEntity(1, "a"); + mFunnyNamedDao.insert(entity); + FunnyNamedEntity loaded = mFunnyNamedDao.load(1); + assertThat(loaded, is(entity)); + } + + @Test + public void update() { + FunnyNamedEntity entity = new FunnyNamedEntity(1, "a"); + mFunnyNamedDao.insert(entity); + entity.setValue("b"); + mFunnyNamedDao.update(entity); + FunnyNamedEntity loaded = mFunnyNamedDao.load(1); + assertThat(loaded.getValue(), is("b")); + } + + @Test + public void delete() { + FunnyNamedEntity entity = new FunnyNamedEntity(1, "a"); + mFunnyNamedDao.insert(entity); + assertThat(mFunnyNamedDao.load(1), notNullValue()); + mFunnyNamedDao.delete(entity); + assertThat(mFunnyNamedDao.load(1), nullValue()); + } + + @Test + public void observe() throws TimeoutException, InterruptedException { + final FunnyNamedEntity[] item = new FunnyNamedEntity[1]; + mFunnyNamedDao.observableOne(2).observeForever(new Observer<FunnyNamedEntity>() { + @Override + public void onChanged(@Nullable FunnyNamedEntity funnyNamedEntity) { + item[0] = funnyNamedEntity; + } + }); + + FunnyNamedEntity entity = new FunnyNamedEntity(1, "a"); + mFunnyNamedDao.insert(entity); + mExecutorRule.drainTasks(1, TimeUnit.MINUTES); + assertThat(item[0], nullValue()); + + final FunnyNamedEntity entity2 = new FunnyNamedEntity(2, "b"); + mFunnyNamedDao.insert(entity2); + mExecutorRule.drainTasks(1, TimeUnit.MINUTES); + assertThat(item[0], is(entity2)); + + final FunnyNamedEntity entity3 = new FunnyNamedEntity(2, "c"); + mFunnyNamedDao.update(entity3); + mExecutorRule.drainTasks(1, TimeUnit.MINUTES); + assertThat(item[0], is(entity3)); + } +} diff --git a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java index 4787ce52..84f20ec5 100644 --- a/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java +++ b/android/arch/persistence/room/integration/testapp/test/InvalidationTest.java @@ -21,7 +21,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.TaskExecutor; import android.arch.persistence.room.InvalidationTracker; import android.arch.persistence.room.Room; @@ -68,7 +68,7 @@ public class InvalidationTest { @Before public void setSingleThreadedIO() { - AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() { + ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { ExecutorService mIOExecutor = Executors.newSingleThreadExecutor(); Handler mHandler = new Handler(Looper.getMainLooper()); @@ -91,7 +91,7 @@ public class InvalidationTest { @After public void clearExecutor() { - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } private void waitUntilIOThreadIsIdle() { @@ -101,7 +101,7 @@ public class InvalidationTest { return null; } }); - AppToolkitTaskExecutor.getInstance().executeOnDiskIO(future); + ArchTaskExecutor.getInstance().executeOnDiskIO(future); //noinspection TryWithIdenticalCatches try { future.get(); diff --git a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java index cae8445b..d78411f8 100644 --- a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java +++ b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java @@ -21,7 +21,7 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.testing.CountingTaskExecutorRule; import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleOwner; @@ -35,9 +35,11 @@ import android.arch.persistence.room.integration.testapp.vo.PetsToys; import android.arch.persistence.room.integration.testapp.vo.Toy; import android.arch.persistence.room.integration.testapp.vo.User; import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets; +import android.os.Build; import android.support.annotation.Nullable; import android.support.test.InstrumentationRegistry; import android.support.test.filters.MediumTest; +import android.support.test.filters.SdkSuppress; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -235,6 +237,7 @@ public class LiveDataQueryTest extends TestDatabaseTest { } @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) public void withWithClause() throws ExecutionException, InterruptedException, TimeoutException { LiveData<List<String>> actual = @@ -322,7 +325,7 @@ public class LiveDataQueryTest extends TestDatabaseTest { return null; } }); - AppToolkitTaskExecutor.getInstance().executeOnMainThread(futureTask); + ArchTaskExecutor.getInstance().executeOnMainThread(futureTask); futureTask.get(); } diff --git a/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java b/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java index 97ce10c2..fda43732 100644 --- a/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java +++ b/android/arch/persistence/room/integration/testapp/test/PrimaryKeyTest.java @@ -16,29 +16,35 @@ package android.arch.persistence.room.integration.testapp.test; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; - -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import static org.junit.Assert.assertNotNull; import android.arch.persistence.room.Room; import android.arch.persistence.room.integration.testapp.PKeyTestDatabase; import android.arch.persistence.room.integration.testapp.vo.IntAutoIncPKeyEntity; import android.arch.persistence.room.integration.testapp.vo.IntegerAutoIncPKeyEntity; +import android.arch.persistence.room.integration.testapp.vo.IntegerPKeyEntity; +import android.arch.persistence.room.integration.testapp.vo.ObjectPKeyEntity; +import android.database.sqlite.SQLiteConstraintException; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Arrays; +import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest public class PrimaryKeyTest { private PKeyTestDatabase mDatabase; + @Before public void setup() { mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), @@ -49,8 +55,8 @@ public class PrimaryKeyTest { public void integerTest() { IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity(); entity.data = "foo"; - mDatabase.integerPKeyDao().insertMe(entity); - IntegerAutoIncPKeyEntity loaded = mDatabase.integerPKeyDao().getMe(1); + mDatabase.integerAutoIncPKeyDao().insertMe(entity); + IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(1); assertThat(loaded, notNullValue()); assertThat(loaded.data, is(entity.data)); } @@ -60,8 +66,8 @@ public class PrimaryKeyTest { IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity(); entity.pKey = 0; entity.data = "foo"; - mDatabase.integerPKeyDao().insertMe(entity); - IntegerAutoIncPKeyEntity loaded = mDatabase.integerPKeyDao().getMe(0); + mDatabase.integerAutoIncPKeyDao().insertMe(entity); + IntegerAutoIncPKeyEntity loaded = mDatabase.integerAutoIncPKeyDao().getMe(0); assertThat(loaded, notNullValue()); assertThat(loaded.data, is(entity.data)); } @@ -98,8 +104,8 @@ public class PrimaryKeyTest { public void getInsertedIdFromInteger() { IntegerAutoIncPKeyEntity entity = new IntegerAutoIncPKeyEntity(); entity.data = "foo"; - final long id = mDatabase.integerPKeyDao().insertAndGetId(entity); - assertThat(mDatabase.integerPKeyDao().getMe((int) id).data, is("foo")); + final long id = mDatabase.integerAutoIncPKeyDao().insertAndGetId(entity); + assertThat(mDatabase.integerAutoIncPKeyDao().getMe((int) id).data, is("foo")); } @Test @@ -108,7 +114,34 @@ public class PrimaryKeyTest { entity.data = "foo"; IntegerAutoIncPKeyEntity entity2 = new IntegerAutoIncPKeyEntity(); entity2.data = "foo2"; - final long[] ids = mDatabase.integerPKeyDao().insertAndGetIds(entity, entity2); - assertThat(mDatabase.integerPKeyDao().loadDataById(ids), is(Arrays.asList("foo", "foo2"))); + final long[] ids = mDatabase.integerAutoIncPKeyDao().insertAndGetIds(entity, entity2); + assertThat(mDatabase.integerAutoIncPKeyDao().loadDataById(ids), + is(Arrays.asList("foo", "foo2"))); + } + + @Test + public void insertNullPrimaryKey() throws Exception { + ObjectPKeyEntity o1 = new ObjectPKeyEntity(null, "1"); + + Throwable throwable = null; + try { + mDatabase.objectPKeyDao().insertMe(o1); + } catch (Throwable t) { + throwable = t; + } + assertNotNull("Was expecting an exception", throwable); + assertThat(throwable, instanceOf(SQLiteConstraintException.class)); + } + + @Test + public void insertNullPrimaryKeyForInteger() throws Exception { + IntegerPKeyEntity entity = new IntegerPKeyEntity(); + entity.data = "data"; + mDatabase.integerPKeyDao().insertMe(entity); + + List<IntegerPKeyEntity> list = mDatabase.integerPKeyDao().loadAll(); + assertThat(list.size(), is(1)); + assertThat(list.get(0).data, is("data")); + assertNotNull(list.get(0).pKey); } } diff --git a/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java b/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java index 1bbc1406..01d071e7 100644 --- a/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java +++ b/android/arch/persistence/room/integration/testapp/test/RxJava2Test.java @@ -19,10 +19,12 @@ package android.arch.persistence.room.integration.testapp.test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import android.arch.core.executor.TaskExecutor; import android.arch.persistence.room.EmptyResultSetException; +import android.arch.persistence.room.integration.testapp.vo.Pet; import android.arch.persistence.room.integration.testapp.vo.User; +import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets; import android.support.test.filters.MediumTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -38,6 +40,7 @@ import java.util.Collections; import java.util.List; import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Predicate; import io.reactivex.observers.TestObserver; import io.reactivex.schedulers.TestScheduler; import io.reactivex.subscribers.TestSubscriber; @@ -52,7 +55,7 @@ public class RxJava2Test extends TestDatabaseTest { public void setupSchedulers() { mTestScheduler = new TestScheduler(); mTestScheduler.start(); - AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() { + ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() { @Override public void executeOnDiskIO(Runnable runnable) { mTestScheduler.scheduleDirect(runnable); @@ -73,7 +76,7 @@ public class RxJava2Test extends TestDatabaseTest { @After public void clearSchedulers() { mTestScheduler.shutdown(); - AppToolkitTaskExecutor.getInstance().setDelegate(null); + ArchTaskExecutor.getInstance().setDelegate(null); } private void drain() throws InterruptedException { @@ -269,4 +272,60 @@ public class RxJava2Test extends TestDatabaseTest { subscriber.cancel(); subscriber.assertNoErrors(); } + + @Test + public void flowableWithRelation() throws InterruptedException { + final TestSubscriber<UserAndAllPets> subscriber = new TestSubscriber<>(); + + mUserPetDao.flowableUserWithPets(3).subscribe(subscriber); + drain(); + subscriber.assertSubscribed(); + + drain(); + subscriber.assertNoValues(); + + final User user = TestUtil.createUser(3); + mUserDao.insert(user); + drain(); + subscriber.assertValue(new Predicate<UserAndAllPets>() { + @Override + public boolean test(UserAndAllPets userAndAllPets) throws Exception { + return userAndAllPets.user.equals(user); + } + }); + subscriber.assertValueCount(1); + final Pet[] pets = TestUtil.createPetsForUser(3, 1, 2); + mPetDao.insertAll(pets); + drain(); + subscriber.assertValueAt(1, new Predicate<UserAndAllPets>() { + @Override + public boolean test(UserAndAllPets userAndAllPets) throws Exception { + return userAndAllPets.user.equals(user) + && userAndAllPets.pets.equals(Arrays.asList(pets)); + } + }); + } + + @Test + public void flowable_updateInTransaction() throws InterruptedException { + // When subscribing to the emissions of the user + final TestSubscriber<User> userTestSubscriber = mUserDao + .flowableUserById(3) + .observeOn(mTestScheduler) + .test(); + drain(); + userTestSubscriber.assertValueCount(0); + + // When inserting a new user in the data source + mDatabase.beginTransaction(); + try { + mUserDao.insert(TestUtil.createUser(3)); + mDatabase.setTransactionSuccessful(); + + } finally { + mDatabase.endTransaction(); + } + drain(); + userTestSubscriber.assertValueCount(1); + } } diff --git a/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java b/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java new file mode 100644 index 00000000..fcd0b004 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/test/RxJava2WithInstantTaskExecutorTest.java @@ -0,0 +1,70 @@ +/* + * 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 android.arch.core.executor.testing.InstantTaskExecutorRule; +import android.arch.persistence.room.Room; +import android.arch.persistence.room.integration.testapp.TestDatabase; +import android.arch.persistence.room.integration.testapp.vo.User; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.reactivex.subscribers.TestSubscriber; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RxJava2WithInstantTaskExecutorTest { + @Rule + public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule(); + + private TestDatabase mDatabase; + + @Before + public void initDb() throws Exception { + // using an in-memory database because the information stored here disappears when the + // process is killed + mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), + TestDatabase.class) + // allowing main thread queries, just for testing + .allowMainThreadQueries() + .build(); + } + + @Test + public void testFlowableInTransaction() { + // When subscribing to the emissions of the user + TestSubscriber<User> subscriber = mDatabase.getUserDao().flowableUserById(3).test(); + subscriber.assertValueCount(0); + + // When inserting a new user in the data source + mDatabase.beginTransaction(); + try { + mDatabase.getUserDao().insert(TestUtil.createUser(3)); + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + + subscriber.assertValueCount(1); + } +} diff --git a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java index 8861adbc..f8049f35 100644 --- a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java +++ b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java @@ -502,4 +502,26 @@ public class SimpleEntityReadWriteTest { assertThat(mUserDao.updateByAgeAndIds(3f, 30, Arrays.asList(3, 5)), is(1)); assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(3f)); } + + @Test + public void transactionByAnnotation() { + User a = TestUtil.createUser(3); + User b = TestUtil.createUser(5); + mUserDao.insertBothByAnnotation(a, b); + assertThat(mUserDao.count(), is(2)); + } + + @Test + public void transactionByAnnotation_failure() { + User a = TestUtil.createUser(3); + User b = TestUtil.createUser(3); + boolean caught = false; + try { + mUserDao.insertBothByAnnotation(a, b); + } catch (SQLiteConstraintException e) { + caught = true; + } + assertTrue("SQLiteConstraintException expected", caught); + assertThat(mUserDao.count(), is(0)); + } } diff --git a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java index 51d5bb33..ec775617 100644 --- a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java +++ b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java @@ -18,6 +18,7 @@ package android.arch.persistence.room.integration.testapp.test; import android.arch.persistence.room.Room; import android.arch.persistence.room.integration.testapp.TestDatabase; +import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao; import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao; import android.arch.persistence.room.integration.testapp.dao.PetDao; import android.arch.persistence.room.integration.testapp.dao.SchoolDao; @@ -42,6 +43,7 @@ public abstract class TestDatabaseTest { protected ToyDao mToyDao; protected SpecificDogDao mSpecificDogDao; protected WithClauseDao mWithClauseDao; + protected FunnyNamedDao mFunnyNamedDao; @Before public void createDb() { @@ -55,5 +57,6 @@ public abstract class TestDatabaseTest { mToyDao = mDatabase.getToyDao(); mSpecificDogDao = mDatabase.getSpecificDogDao(); mWithClauseDao = mDatabase.getWithClauseDao(); + mFunnyNamedDao = mDatabase.getFunnyNamedDao(); } } diff --git a/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java b/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java index 10897da1..92096380 100644 --- a/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java +++ b/android/arch/persistence/room/integration/testapp/test/WithClauseTest.java @@ -20,6 +20,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import android.arch.persistence.room.integration.testapp.vo.User; +import android.os.Build; +import android.support.test.filters.SdkSuppress; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -32,6 +34,7 @@ import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) public class WithClauseTest extends TestDatabaseTest{ @Test public void noSourceOfData() { diff --git a/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java b/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java new file mode 100644 index 00000000..20f3c216 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/vo/FunnyNamedEntity.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.arch.persistence.room.integration.testapp.vo; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; + +/** + * An entity that was weird names + */ +@Entity(tableName = FunnyNamedEntity.TABLE_NAME) +public class FunnyNamedEntity { + public static final String TABLE_NAME = "funny but not so funny"; + public static final String COLUMN_ID = "_this $is id$"; + public static final String COLUMN_VALUE = "unlikely-Ωşå¨ıünames"; + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COLUMN_ID) + private int mId; + @ColumnInfo(name = COLUMN_VALUE) + private String mValue; + + public FunnyNamedEntity(int id, String value) { + mId = id; + mValue = value; + } + + public int getId() { + return mId; + } + + public void setId(int id) { + mId = id; + } + + public String getValue() { + return mValue; + } + + public void setValue(String value) { + mValue = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FunnyNamedEntity entity = (FunnyNamedEntity) o; + + if (mId != entity.mId) return false; + return mValue != null ? mValue.equals(entity.mValue) : entity.mValue == null; + } + + @Override + public int hashCode() { + int result = mId; + result = 31 * result + (mValue != null ? mValue.hashCode() : 0); + return result; + } +} diff --git a/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java b/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java new file mode 100644 index 00000000..cae1843e --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/vo/IntegerPKeyEntity.java @@ -0,0 +1,27 @@ +/* + * 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.vo; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; + +@Entity +public class IntegerPKeyEntity { + @PrimaryKey + public Integer pKey; + public String data; +} diff --git a/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java b/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java new file mode 100644 index 00000000..895a35a2 --- /dev/null +++ b/android/arch/persistence/room/integration/testapp/vo/ObjectPKeyEntity.java @@ -0,0 +1,34 @@ +/* + * 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.vo; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; +import android.support.annotation.NonNull; + +@Entity +public class ObjectPKeyEntity { + @PrimaryKey + @NonNull + public String pKey; + public String data; + + public ObjectPKeyEntity(String pKey, String data) { + this.pKey = pKey; + this.data = data; + } +} diff --git a/android/arch/persistence/room/integration/testapp/vo/PetCouple.java b/android/arch/persistence/room/integration/testapp/vo/PetCouple.java index f27b1313..e5208ed7 100644 --- a/android/arch/persistence/room/integration/testapp/vo/PetCouple.java +++ b/android/arch/persistence/room/integration/testapp/vo/PetCouple.java @@ -20,11 +20,13 @@ import android.arch.persistence.room.Embedded; import android.arch.persistence.room.Entity; import android.arch.persistence.room.PrimaryKey; import android.arch.persistence.room.RoomWarnings; +import android.support.annotation.NonNull; @Entity @SuppressWarnings(RoomWarnings.PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED) public class PetCouple { @PrimaryKey + @NonNull public String id; @Embedded(prefix = "male_") public Pet male; diff --git a/android/arch/persistence/room/migration/TableInfoTest.java b/android/arch/persistence/room/migration/TableInfoTest.java index c6eade55..d88c02fd 100644 --- a/android/arch/persistence/room/migration/TableInfoTest.java +++ b/android/arch/persistence/room/migration/TableInfoTest.java @@ -37,6 +37,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -179,6 +180,35 @@ public class TableInfoTest { Collections.<TableInfo.ForeignKey>emptySet()))); } + @Test + public void readIndices() { + mDb = createDatabase( + "CREATE TABLE foo (n INTEGER, indexed TEXT, unique_indexed TEXT," + + "a INTEGER, b INTEGER);", + "CREATE INDEX foo_indexed ON foo(indexed);", + "CREATE UNIQUE INDEX foo_unique_indexed ON foo(unique_indexed COLLATE NOCASE" + + " DESC);", + "CREATE INDEX " + TableInfo.Index.DEFAULT_PREFIX + "foo_composite_indexed" + + " ON foo(a, b);" + ); + TableInfo info = TableInfo.read(mDb, "foo"); + assertThat(info, is(new TableInfo( + "foo", + toMap(new TableInfo.Column("n", "INTEGER", false, 0), + new TableInfo.Column("indexed", "TEXT", false, 0), + new TableInfo.Column("unique_indexed", "TEXT", false, 0), + new TableInfo.Column("a", "INTEGER", false, 0), + new TableInfo.Column("b", "INTEGER", false, 0)), + Collections.<TableInfo.ForeignKey>emptySet(), + toSet(new TableInfo.Index("index_foo_blahblah", false, + Arrays.asList("a", "b")), + new TableInfo.Index("foo_unique_indexed", true, + Arrays.asList("unique_indexed")), + new TableInfo.Index("foo_indexed", false, + Arrays.asList("indexed")))) + )); + } + private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) { Map<String, TableInfo.Column> result = new HashMap<>(); for (TableInfo.Column column : columns) { @@ -187,6 +217,14 @@ public class TableInfoTest { return result; } + private static <T> Set<T> toSet(T... ts) { + final HashSet<T> result = new HashSet<T>(); + for (T t : ts) { + result.add(t); + } + return result; + } + @After public void closeDb() throws IOException { if (mDb != null && mDb.isOpen()) { @@ -199,8 +237,7 @@ public class TableInfoTest { SupportSQLiteOpenHelper.Configuration .builder(InstrumentationRegistry.getTargetContext()) .name(null) - .version(1) - .callback(new SupportSQLiteOpenHelper.Callback() { + .callback(new SupportSQLiteOpenHelper.Callback(1) { @Override public void onCreate(SupportSQLiteDatabase db) { for (String query : queries) { diff --git a/android/arch/persistence/room/package-info.java b/android/arch/persistence/room/package-info.java index faaa952b..1dafc1b2 100644 --- a/android/arch/persistence/room/package-info.java +++ b/android/arch/persistence/room/package-info.java @@ -39,8 +39,8 @@ * database row. For each {@link android.arch.persistence.room.Entity Entity}, a database table * is created to hold the items. The Entity class must be referenced in the * {@link android.arch.persistence.room.Database#entities() Database#entities} array. Each field - * of the Entity is persisted in the database unless it is annotated with - * {@link android.arch.persistence.room.Ignore Ignore}. Entities must have no-arg constructors. + * of the Entity (and its super class) is persisted in the database unless it is denoted + * otherwise (see {@link android.arch.persistence.room.Entity Entity} docs for details). * </li> * <li>{@link android.arch.persistence.room.Dao Dao}: This annotation marks a class or interface * as a Data Access Object. Data access objects are the main component of Room that are diff --git a/android/arch/persistence/room/testing/MigrationTestHelper.java b/android/arch/persistence/room/testing/MigrationTestHelper.java index aea3e96e..18e0a146 100644 --- a/android/arch/persistence/room/testing/MigrationTestHelper.java +++ b/android/arch/persistence/room/testing/MigrationTestHelper.java @@ -29,6 +29,7 @@ import android.arch.persistence.room.migration.bundle.DatabaseBundle; import android.arch.persistence.room.migration.bundle.EntityBundle; import android.arch.persistence.room.migration.bundle.FieldBundle; import android.arch.persistence.room.migration.bundle.ForeignKeyBundle; +import android.arch.persistence.room.migration.bundle.IndexBundle; import android.arch.persistence.room.migration.bundle.SchemaBundle; import android.arch.persistence.room.util.TableInfo; import android.content.Context; @@ -146,7 +147,7 @@ public class MigrationTestHelper extends TestWatcher { RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration, new CreatingDelegate(schemaBundle.getDatabase()), schemaBundle.getDatabase().getIdentityHash()); - return openDatabase(name, version, roomOpenHelper); + return openDatabase(name, roomOpenHelper); } /** @@ -189,17 +190,15 @@ public class MigrationTestHelper extends TestWatcher { RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration, new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables), schemaBundle.getDatabase().getIdentityHash()); - return openDatabase(name, version, roomOpenHelper); + return openDatabase(name, roomOpenHelper); } - private SupportSQLiteDatabase openDatabase(String name, int version, - RoomOpenHelper roomOpenHelper) { + private SupportSQLiteDatabase openDatabase(String name, RoomOpenHelper roomOpenHelper) { SupportSQLiteOpenHelper.Configuration config = SupportSQLiteOpenHelper.Configuration .builder(mInstrumentation.getTargetContext()) .callback(roomOpenHelper) .name(name) - .version(version) .build(); SupportSQLiteDatabase db = mOpenFactory.create(config).getWritableDatabase(); mManagedDatabases.add(new WeakReference<>(db)); @@ -287,7 +286,19 @@ public class MigrationTestHelper extends TestWatcher { private static TableInfo toTableInfo(EntityBundle entityBundle) { return new TableInfo(entityBundle.getTableName(), toColumnMap(entityBundle), - toForeignKeys(entityBundle.getForeignKeys())); + toForeignKeys(entityBundle.getForeignKeys()), toIndices(entityBundle.getIndices())); + } + + private static Set<TableInfo.Index> toIndices(List<IndexBundle> indices) { + if (indices == null) { + return Collections.emptySet(); + } + Set<TableInfo.Index> result = new HashSet<>(); + for (IndexBundle bundle : indices) { + result.add(new TableInfo.Index(bundle.getName(), bundle.isUnique(), + bundle.getColumnNames())); + } + return result; } private static Set<TableInfo.ForeignKey> toForeignKeys( @@ -401,6 +412,7 @@ public class MigrationTestHelper extends TestWatcher { final DatabaseBundle mDatabaseBundle; RoomOpenHelperDelegate(DatabaseBundle databaseBundle) { + super(databaseBundle.getVersion()); mDatabaseBundle = databaseBundle; } diff --git a/android/arch/persistence/room/util/TableInfo.java b/android/arch/persistence/room/util/TableInfo.java index bcd2e9ef..a115147d 100644 --- a/android/arch/persistence/room/util/TableInfo.java +++ b/android/arch/persistence/room/util/TableInfo.java @@ -20,6 +20,7 @@ import android.arch.persistence.db.SupportSQLiteDatabase; import android.database.Cursor; import android.os.Build; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import java.util.ArrayList; @@ -29,6 +30,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; /** * A data class that holds the information about a table. @@ -56,11 +58,70 @@ public class TableInfo { public final Set<ForeignKey> foreignKeys; + /** + * Sometimes, Index information is not available (older versions). If so, we skip their + * verification. + */ + @Nullable + public final Set<Index> indices; + @SuppressWarnings("unused") - public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) { + public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys, + Set<Index> indices) { this.name = name; this.columns = Collections.unmodifiableMap(columns); this.foreignKeys = Collections.unmodifiableSet(foreignKeys); + this.indices = indices == null ? null : Collections.unmodifiableSet(indices); + } + + /** + * For backward compatibility with dbs created with older versions. + */ + @SuppressWarnings("unused") + public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) { + this(name, columns, foreignKeys, Collections.<Index>emptySet()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TableInfo tableInfo = (TableInfo) o; + + if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false; + if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) { + return false; + } + if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys) + : tableInfo.foreignKeys != null) { + return false; + } + if (indices == null || tableInfo.indices == null) { + // if one us is missing index information, seems like we couldn't acquire the + // information so we better skip. + return true; + } + return indices.equals(tableInfo.indices); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (columns != null ? columns.hashCode() : 0); + result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0); + // skip index, it is not reliable for comparison. + return result; + } + + @Override + public String toString() { + return "TableInfo{" + + "name='" + name + '\'' + + ", columns=" + columns + + ", foreignKeys=" + foreignKeys + + ", indices=" + indices + + '}'; } /** @@ -74,7 +135,8 @@ public class TableInfo { public static TableInfo read(SupportSQLiteDatabase database, String tableName) { Map<String, Column> columns = readColumns(database, tableName); Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName); - return new TableInfo(tableName, columns, foreignKeys); + Set<Index> indices = readIndices(database, tableName); + return new TableInfo(tableName, columns, foreignKeys, indices); } private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database, @@ -167,34 +229,74 @@ public class TableInfo { return columns; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - TableInfo tableInfo = (TableInfo) o; - - if (!name.equals(tableInfo.name)) return false; - //noinspection SimplifiableIfStatement - if (!columns.equals(tableInfo.columns)) return false; - return foreignKeys.equals(tableInfo.foreignKeys); + /** + * @return null if we cannot read the indices due to older sqlite implementations. + */ + @Nullable + private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) { + Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)"); + try { + final int nameColumnIndex = cursor.getColumnIndex("name"); + final int originColumnIndex = cursor.getColumnIndex("origin"); + final int uniqueIndex = cursor.getColumnIndex("unique"); + if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) { + // we cannot read them so better not validate any index. + return null; + } + HashSet<Index> indices = new HashSet<>(); + while (cursor.moveToNext()) { + String origin = cursor.getString(originColumnIndex); + if (!"c".equals(origin)) { + // Ignore auto-created indices + continue; + } + String name = cursor.getString(nameColumnIndex); + boolean unique = cursor.getInt(uniqueIndex) == 1; + Index index = readIndex(database, name, unique); + if (index == null) { + // we cannot read it properly so better not read it + return null; + } + indices.add(index); + } + return indices; + } finally { + cursor.close(); + } } - @Override - public int hashCode() { - int result = name.hashCode(); - result = 31 * result + columns.hashCode(); - result = 31 * result + foreignKeys.hashCode(); - return result; - } + /** + * @return null if we cannot read the index due to older sqlite implementations. + */ + @Nullable + private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) { + Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)"); + try { + final int seqnoColumnIndex = cursor.getColumnIndex("seqno"); + final int cidColumnIndex = cursor.getColumnIndex("cid"); + final int nameColumnIndex = cursor.getColumnIndex("name"); + if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) { + // we cannot read them so better not validate any index. + return null; + } + final TreeMap<Integer, String> results = new TreeMap<>(); - @Override - public String toString() { - return "TableInfo{" - + "name='" + name + '\'' - + ", columns=" + columns - + ", foreignKeys=" + foreignKeys - + '}'; + while (cursor.moveToNext()) { + int cid = cursor.getInt(cidColumnIndex); + if (cid < 0) { + // Ignore SQLite row ID + continue; + } + int seq = cursor.getInt(seqnoColumnIndex); + String columnName = cursor.getString(nameColumnIndex); + results.put(seq, columnName); + } + final List<String> columns = new ArrayList<>(results.size()); + columns.addAll(results.values()); + return new Index(name, unique, columns); + } finally { + cursor.close(); + } } /** @@ -379,4 +481,65 @@ public class TableInfo { } } } + + /** + * Holds the information about an SQLite index + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static class Index { + // should match the value in Index.kt + public static final String DEFAULT_PREFIX = "index_"; + public final String name; + public final boolean unique; + public final List<String> columns; + + public Index(String name, boolean unique, List<String> columns) { + this.name = name; + this.unique = unique; + this.columns = columns; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Index index = (Index) o; + if (unique != index.unique) { + return false; + } + if (!columns.equals(index.columns)) { + return false; + } + if (name.startsWith(Index.DEFAULT_PREFIX)) { + return index.name.startsWith(Index.DEFAULT_PREFIX); + } else { + return name.equals(index.name); + } + } + + @Override + public int hashCode() { + int result; + if (name.startsWith(DEFAULT_PREFIX)) { + result = DEFAULT_PREFIX.hashCode(); + } else { + result = name.hashCode(); + } + result = 31 * result + (unique ? 1 : 0); + result = 31 * result + columns.hashCode(); + return result; + } + + @Override + public String toString() { + return "Index{" + + "name='" + name + '\'' + + ", unique=" + unique + + ", columns=" + columns + + '}'; + } + } } diff --git a/android/bluetooth/BluetoothAdapter.java b/android/bluetooth/BluetoothAdapter.java index 70591d4d..84765f6d 100644 --- a/android/bluetooth/BluetoothAdapter.java +++ b/android/bluetooth/BluetoothAdapter.java @@ -1134,6 +1134,29 @@ public final class BluetoothAdapter { } /** + * Sets the {@link BluetoothClass} Bluetooth Class of Device (CoD) of + * the local Bluetooth adapter. + * + * @param bluetoothClass {@link BluetoothClass} to set the local Bluetooth adapter to. + * @return true if successful, false if unsuccessful. + * + * @hide + */ + @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) + public boolean setBluetoothClass(BluetoothClass bluetoothClass) { + if (getState() != STATE_ON) return false; + try { + mServiceLock.readLock().lock(); + if (mService != null) return mService.setBluetoothClass(bluetoothClass); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } finally { + mServiceLock.readLock().unlock(); + } + return false; + } + + /** * Get the current Bluetooth scan mode of the local Bluetooth adapter. * <p>The Bluetooth scan mode determines if the local adapter is * connectable and/or discoverable from remote Bluetooth devices. diff --git a/android/bluetooth/BluetoothClass.java b/android/bluetooth/BluetoothClass.java index 57e4abb1..f22ea6e8 100644 --- a/android/bluetooth/BluetoothClass.java +++ b/android/bluetooth/BluetoothClass.java @@ -19,6 +19,10 @@ package android.bluetooth; import android.os.Parcel; import android.os.Parcelable; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + /** * Represents a Bluetooth class, which describes general characteristics * and capabilities of a device. For example, a Bluetooth class will @@ -275,6 +279,48 @@ public final class BluetoothClass implements Parcelable { return (mClass & Device.BITMASK); } + /** + * Return the Bluetooth Class of Device (CoD) value including the + * {@link BluetoothClass.Service}, {@link BluetoothClass.Device.Major} and + * minor device fields. + * + * <p>This value is an integer representation of Bluetooth CoD as in + * Bluetooth specification. + * + * @see <a href="Bluetooth CoD">https://www.bluetooth.com/specifications/assigned-numbers/baseband</a> + * + * @hide + */ + public int getClassOfDevice() { + return mClass; + } + + /** + * Return the Bluetooth Class of Device (CoD) value including the + * {@link BluetoothClass.Service}, {@link BluetoothClass.Device.Major} and + * minor device fields. + * + * <p>This value is a byte array representation of Bluetooth CoD as in + * Bluetooth specification. + * + * <p>Bluetooth COD information is 3 bytes, but stored as an int. Hence the + * MSB is useless and needs to be thrown away. The lower 3 bytes are + * converted into a byte array MSB to LSB. Hence, using BIG_ENDIAN. + * + * @see <a href="Bluetooth CoD">https://www.bluetooth.com/specifications/assigned-numbers/baseband</a> + * + * @hide + */ + public byte[] getClassOfDeviceBytes() { + byte[] bytes = ByteBuffer.allocate(4) + .order(ByteOrder.BIG_ENDIAN) + .putInt(mClass) + .array(); + + // Discard the top byte + return Arrays.copyOfRange(bytes, 1, bytes.length); + } + /** @hide */ public static final int PROFILE_HEADSET = 0; /** @hide */ diff --git a/android/bluetooth/BluetoothInputHost.java b/android/bluetooth/BluetoothInputHost.java index 37f04278..e18d9d1b 100644 --- a/android/bluetooth/BluetoothInputHost.java +++ b/android/bluetooth/BluetoothInputHost.java @@ -74,7 +74,7 @@ public final class BluetoothInputHost implements BluetoothProfile { public static final byte SUBCLASS2_GAMEPAD = (byte) 0x02; public static final byte SUBCLASS2_REMOTE_CONTROL = (byte) 0x03; public static final byte SUBCLASS2_SENSING_DEVICE = (byte) 0x04; - public static final byte SUBCLASS2_DIGITIZER_TABLED = (byte) 0x05; + public static final byte SUBCLASS2_DIGITIZER_TABLET = (byte) 0x05; public static final byte SUBCLASS2_CARD_READER = (byte) 0x06; /** diff --git a/android/bluetooth/BluetoothPbap.java b/android/bluetooth/BluetoothPbap.java index 19f5198c..a1a9347d 100644 --- a/android/bluetooth/BluetoothPbap.java +++ b/android/bluetooth/BluetoothPbap.java @@ -16,6 +16,7 @@ package android.bluetooth; +import android.annotation.SdkConstant; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -53,35 +54,32 @@ public class BluetoothPbap { private static final boolean DBG = true; private static final boolean VDBG = false; - /** int extra for PBAP_STATE_CHANGED_ACTION */ - public static final String PBAP_STATE = - "android.bluetooth.pbap.intent.PBAP_STATE"; - /** int extra for PBAP_STATE_CHANGED_ACTION */ - public static final String PBAP_PREVIOUS_STATE = - "android.bluetooth.pbap.intent.PBAP_PREVIOUS_STATE"; - /** - * Indicates the state of a pbap connection state has changed. - * This intent will always contain PBAP_STATE, PBAP_PREVIOUS_STATE and - * BluetoothIntent.ADDRESS extras. + * Intent used to broadcast the change in connection state of the PBAP + * profile. + * + * <p>This intent will have 3 extras: + * <ul> + * <li> {@link BluetoothProfile#EXTRA_STATE} - The current state of the profile. </li> + * <li> {@link BluetoothProfile#EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li> + * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> + * </ul> + * <p>{@link BluetoothProfile#EXTRA_STATE} or {@link BluetoothProfile#EXTRA_PREVIOUS_STATE} + * can be any of {@link BluetoothProfile#STATE_DISCONNECTED}, + * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, + * {@link BluetoothProfile#STATE_DISCONNECTING}. + * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to + * receive. */ - public static final String PBAP_STATE_CHANGED_ACTION = - "android.bluetooth.pbap.intent.action.PBAP_STATE_CHANGED"; + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CONNECTION_STATE_CHANGED = + "android.bluetooth.pbap.profile.action.CONNECTION_STATE_CHANGED"; private volatile IBluetoothPbap mService; private final Context mContext; private ServiceListener mServiceListener; private BluetoothAdapter mAdapter; - /** There was an error trying to obtain the state */ - public static final int STATE_ERROR = -1; - /** No client currently connected */ - public static final int STATE_DISCONNECTED = 0; - /** Connection attempt in progress */ - public static final int STATE_CONNECTING = 1; - /** Client is currently connected */ - public static final int STATE_CONNECTED = 2; - public static final int RESULT_FAILURE = 0; public static final int RESULT_SUCCESS = 1; /** Connection canceled before completion. */ @@ -209,8 +207,8 @@ public class BluetoothPbap { /** * Get the current state of the BluetoothPbap service. * - * @return One of the STATE_ return codes, or STATE_ERROR if this proxy object is currently not - * connected to the Pbap service. + * @return One of the STATE_ return codes, or {@link BluetoothProfile#STATE_DISCONNECTED} + * if this proxy object is currently not connected to the Pbap service. */ public int getState() { if (VDBG) log("getState()"); @@ -225,7 +223,7 @@ public class BluetoothPbap { Log.w(TAG, "Proxy not attached to service"); if (DBG) log(Log.getStackTraceString(new Throwable())); } - return BluetoothPbap.STATE_ERROR; + return BluetoothProfile.STATE_DISCONNECTED; } /** diff --git a/android/bluetooth/BluetoothPbapClient.java b/android/bluetooth/BluetoothPbapClient.java index 00a15f3f..01b3f6e0 100644 --- a/android/bluetooth/BluetoothPbapClient.java +++ b/android/bluetooth/BluetoothPbapClient.java @@ -40,7 +40,7 @@ public final class BluetoothPbapClient implements BluetoothProfile { private static final boolean VDBG = false; public static final String ACTION_CONNECTION_STATE_CHANGED = - "android.bluetooth.pbap.profile.action.CONNECTION_STATE_CHANGED"; + "android.bluetooth.pbapclient.profile.action.CONNECTION_STATE_CHANGED"; private volatile IBluetoothPbapClient mService; private final Context mContext; diff --git a/android/bluetooth/BluetoothUuid.java b/android/bluetooth/BluetoothUuid.java index 5bfc54d2..76cb3f5b 100644 --- a/android/bluetooth/BluetoothUuid.java +++ b/android/bluetooth/BluetoothUuid.java @@ -232,7 +232,7 @@ public final class BluetoothUuid { */ public static int getServiceIdentifierFromParcelUuid(ParcelUuid parcelUuid) { UUID uuid = parcelUuid.getUuid(); - long value = (uuid.getMostSignificantBits() & 0x0000FFFF00000000L) >>> 32; + long value = (uuid.getMostSignificantBits() & 0xFFFFFFFF00000000L) >>> 32; return (int) value; } diff --git a/android/content/ComponentName.java b/android/content/ComponentName.java index ea6b7690..0d36bddc 100644 --- a/android/content/ComponentName.java +++ b/android/content/ComponentName.java @@ -21,9 +21,9 @@ import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; -import java.lang.Comparable; /** * Identifier for a specific application component @@ -33,7 +33,7 @@ import java.lang.Comparable; * pieces of information, encapsulated here, are required to identify * a component: the package (a String) it exists in, and the class (a String) * name inside of that package. - * + * */ public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> { private final String mPackage; @@ -91,7 +91,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co /** * Create a new component identifier. - * + * * @param pkg The name of the package that the component exists in. Can * not be null. * @param cls The name of the class inside of <var>pkg</var> that @@ -106,7 +106,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co /** * Create a new component identifier from a Context and class name. - * + * * @param pkg A Context for the package implementing the component, * from which the actual package name will be retrieved. * @param cls The name of the class inside of <var>pkg</var> that @@ -120,7 +120,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co /** * Create a new component identifier from a Context and Class object. - * + * * @param pkg A Context for the package implementing the component, from * which the actual package name will be retrieved. * @param cls The Class object of the desired component, from which the @@ -141,14 +141,14 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co public @NonNull String getPackageName() { return mPackage; } - + /** * Return the class name of this component. */ public @NonNull String getClassName() { return mClass; } - + /** * Return the class name, either fully qualified or in a shortened form * (with a leading '.') if it is a suffix of the package. @@ -163,7 +163,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co } return mClass; } - + private static void appendShortClassName(StringBuilder sb, String packageName, String className) { if (className.startsWith(packageName)) { @@ -195,26 +195,26 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co * class names contained in the ComponentName. You can later recover * the ComponentName from this string through * {@link #unflattenFromString(String)}. - * + * * @return Returns a new String holding the package and class names. This * is represented as the package name, concatenated with a '/' and then the * class name. - * + * * @see #unflattenFromString(String) */ public @NonNull String flattenToString() { return mPackage + "/" + mClass; } - + /** * The same as {@link #flattenToString()}, but abbreviates the class * name if it is a suffix of the package. The result can still be used * with {@link #unflattenFromString(String)}. - * + * * @return Returns a new String holding the package and class names. This * is represented as the package name, concatenated with a '/' and then the * class name. - * + * * @see #unflattenFromString(String) */ public @NonNull String flattenToShortString() { @@ -250,11 +250,11 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co * followed by a '.' then the final class name will be the concatenation * of the package name with the string following the '/'. Thus * "com.foo/.Blah" becomes package="com.foo" class="com.foo.Blah". - * + * * @param str The String that was returned by flattenToString(). * @return Returns a new ComponentName containing the package and class * names that were encoded in <var>str</var> - * + * * @see #flattenToString() */ public static @Nullable ComponentName unflattenFromString(@NonNull String str) { @@ -269,7 +269,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co } return new ComponentName(pkg, cls); } - + /** * Return string representation of this class without the class's name * as a prefix. @@ -283,6 +283,12 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co return "ComponentInfo{" + mPackage + "/" + mClass + "}"; } + /** Put this here so that individual services don't have to reimplement this. @hide */ + public void toProto(ProtoOutputStream proto) { + proto.write(ComponentNameProto.PACKAGE_NAME, mPackage); + proto.write(ComponentNameProto.CLASS_NAME, mClass); + } + @Override public boolean equals(Object obj) { try { @@ -311,7 +317,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co } return this.mClass.compareTo(that.mClass); } - + public int describeContents() { return 0; } @@ -324,10 +330,10 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co /** * Write a ComponentName to a Parcel, handling null pointers. Must be * read with {@link #readFromParcel(Parcel)}. - * + * * @param c The ComponentName to be written. * @param out The Parcel in which the ComponentName will be placed. - * + * * @see #readFromParcel(Parcel) */ public static void writeToParcel(ComponentName c, Parcel out) { @@ -337,23 +343,23 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co out.writeString(null); } } - + /** * Read a ComponentName from a Parcel that was previously written * with {@link #writeToParcel(ComponentName, Parcel)}, returning either * a null or new object as appropriate. - * + * * @param in The Parcel from which to read the ComponentName * @return Returns a new ComponentName matching the previously written * object, or null if a null had been written. - * + * * @see #writeToParcel(ComponentName, Parcel) */ public static ComponentName readFromParcel(Parcel in) { String pkg = in.readString(); return pkg != null ? new ComponentName(pkg, in) : null; } - + public static final Parcelable.Creator<ComponentName> CREATOR = new Parcelable.Creator<ComponentName>() { public ComponentName createFromParcel(Parcel in) { @@ -371,7 +377,7 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co * must not use this with data written by * {@link #writeToParcel(ComponentName, Parcel)} since it is not possible * to handle a null ComponentObject here. - * + * * @param in The Parcel containing the previously written ComponentName, * positioned at the location in the buffer where it was written. */ diff --git a/android/content/ContentProvider.java b/android/content/ContentProvider.java index cdeaea3e..5b2bf456 100644 --- a/android/content/ContentProvider.java +++ b/android/content/ContentProvider.java @@ -2099,7 +2099,8 @@ 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_CONTENT.equals(uri.getScheme()) + || ContentResolver.SCHEME_SLICE.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 9ccc552f..02e70f55 100644 --- a/android/content/ContentResolver.java +++ b/android/content/ContentResolver.java @@ -47,6 +47,8 @@ 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; @@ -178,6 +180,8 @@ 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"; @@ -1718,6 +1722,36 @@ 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 @@ -1725,7 +1759,7 @@ public abstract class ContentResolver { * @hide */ public final IContentProvider acquireProvider(Uri uri) { - if (!SCHEME_CONTENT.equals(uri.getScheme())) { + if (!SCHEME_CONTENT.equals(uri.getScheme()) && !SCHEME_SLICE.equals(uri.getScheme())) { return null; } final String auth = uri.getAuthority(); diff --git a/android/content/Context.java b/android/content/Context.java index 2d8249ac..20fbf046 100644 --- a/android/content/Context.java +++ b/android/content/Context.java @@ -64,6 +64,7 @@ import android.view.DisplayAdjustments; import android.view.View; import android.view.ViewDebug; import android.view.WindowManager; +import android.view.autofill.AutofillManager.AutofillClient; import android.view.textclassifier.TextClassificationManager; import java.io.File; @@ -2991,6 +2992,7 @@ public abstract class Context { //@hide: CONTEXTHUB_SERVICE, SYSTEM_HEALTH_SERVICE, //@hide: INCIDENT_SERVICE, + //@hide: STATS_COMPANION_SERVICE, COMPANION_DEVICE_SERVICE }) @Retention(RetentionPolicy.SOURCE) @@ -3033,6 +3035,9 @@ public abstract class Context { * <dt> {@link #CONNECTIVITY_SERVICE} ("connection") * <dd> A {@link android.net.ConnectivityManager ConnectivityManager} for * handling management of network connections. + * <dt> {@link #IPSEC_SERVICE} ("ipsec") + * <dd> A {@link android.net.IpSecManager IpSecManager} for managing IPSec on + * sockets and networks. * <dt> {@link #WIFI_SERVICE} ("wifi") * <dd> A {@link android.net.wifi.WifiManager WifiManager} for management of Wi-Fi * connectivity. On releases before NYC, it should only be obtained from an application @@ -3377,7 +3382,6 @@ public abstract class Context { * {@link android.net.IpSecManager} for encrypting Sockets or Networks with * IPSec. * - * @hide * @see #getSystemService */ public static final String IPSEC_SERVICE = "ipsec"; @@ -3464,6 +3468,19 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a {@link + * android.net.wifi.rtt.WifiRttManager} for ranging devices with wifi + * + * Note: this is a replacement for WIFI_RTT_SERVICE above. It will + * be renamed once final implementation in place. + * + * @see #getSystemService + * @see android.net.wifi.rtt.WifiRttManager + * @hide + */ + public static final String WIFI_RTT2_SERVICE = "rttmanager2"; + + /** + * Use with {@link #getSystemService} to retrieve a {@link * android.net.lowpan.LowpanManager} for handling management of * LoWPAN access. * @@ -4020,6 +4037,12 @@ public abstract class Context { public static final String INCIDENT_SERVICE = "incident"; /** + * Service to assist statsd in obtaining general stats. + * @hide + */ + public static final String STATS_COMPANION_SERVICE = "statscompanion"; + + /** * Use with {@link #getSystemService} to retrieve a {@link * android.content.om.OverlayManager} for managing overlay packages. * @@ -4765,6 +4788,19 @@ public abstract class Context { } /** + * @hide + */ + public AutofillClient getAutofillClient() { + return null; + } + + /** + * @hide + */ + public void setAutofillClient(AutofillClient client) { + } + + /** * Throws an exception if the Context is using system resources, * which are non-runtime-overlay-themable and may show inconsistent UI. * @hide diff --git a/android/content/ContextWrapper.java b/android/content/ContextWrapper.java index a9fd58bc..85acdc6b 100644 --- a/android/content/ContextWrapper.java +++ b/android/content/ContextWrapper.java @@ -37,6 +37,7 @@ import android.os.Looper; import android.os.UserHandle; import android.view.Display; import android.view.DisplayAdjustments; +import android.view.autofill.AutofillManager.AutofillClient; import java.io.File; import java.io.FileInputStream; @@ -967,7 +968,24 @@ public class ContextWrapper extends Context { /** * @hide */ + @Override public int getNextAutofillId() { return mBase.getNextAutofillId(); } + + /** + * @hide + */ + @Override + public AutofillClient getAutofillClient() { + return mBase.getAutofillClient(); + } + + /** + * @hide + */ + @Override + public void setAutofillClient(AutofillClient client) { + mBase.setAutofillClient(client); + } } diff --git a/android/content/Intent.java b/android/content/Intent.java index 08acfb65..c9ad9519 100644 --- a/android/content/Intent.java +++ b/android/content/Intent.java @@ -9444,7 +9444,7 @@ public class Intent implements Parcelable, Cloneable { for (int i=0; i<N; i++) { char c = data.charAt(i); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') - || c == '.' || c == '-') { + || (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+') { continue; } if (c == ':' && i > 0) { @@ -10071,6 +10071,27 @@ public class Intent implements Parcelable, Cloneable { return false; } + /** + * Convert the dock state to a human readable format. + * @hide + */ + public static String dockStateToString(int dock) { + switch (dock) { + case EXTRA_DOCK_STATE_HE_DESK: + return "EXTRA_DOCK_STATE_HE_DESK"; + case EXTRA_DOCK_STATE_LE_DESK: + return "EXTRA_DOCK_STATE_LE_DESK"; + case EXTRA_DOCK_STATE_CAR: + return "EXTRA_DOCK_STATE_CAR"; + case EXTRA_DOCK_STATE_DESK: + return "EXTRA_DOCK_STATE_DESK"; + case EXTRA_DOCK_STATE_UNDOCKED: + return "EXTRA_DOCK_STATE_UNDOCKED"; + default: + return Integer.toString(dock); + } + } + private static ClipData.Item makeClipItem(ArrayList<Uri> streams, ArrayList<CharSequence> texts, ArrayList<String> htmlTexts, int which) { Uri uri = streams != null ? streams.get(which) : null; diff --git a/android/content/pm/ActivityInfo.java b/android/content/pm/ActivityInfo.java index 48587b36..41667c4c 100644 --- a/android/content/pm/ActivityInfo.java +++ b/android/content/pm/ActivityInfo.java @@ -17,6 +17,7 @@ package android.content.pm; import android.annotation.IntDef; +import android.annotation.TestApi; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Configuration.NativeConfig; @@ -34,8 +35,7 @@ import java.lang.annotation.RetentionPolicy; * from the AndroidManifest.xml's <activity> and * <receiver> tags. */ -public class ActivityInfo extends ComponentInfo - implements Parcelable { +public class ActivityInfo extends ComponentInfo implements Parcelable { // NOTE: When adding new data members be sure to update the copy-constructor, Parcel // constructor, and writeToParcel. @@ -181,6 +181,7 @@ public class ActivityInfo extends ComponentInfo * Activity explicitly requested to be resizeable. * @hide */ + @TestApi public static final int RESIZE_MODE_RESIZEABLE = 2; /** * Activity is resizeable and supported picture-in-picture mode. This flag is now deprecated @@ -1211,6 +1212,67 @@ public class ActivityInfo extends ComponentInfo return isFloating || isTranslucent || isSwipeToDismiss; } + /** + * Convert the screen orientation constant to a human readable format. + * @hide + */ + public static String screenOrientationToString(int orientation) { + switch (orientation) { + case SCREEN_ORIENTATION_UNSET: + return "SCREEN_ORIENTATION_UNSET"; + case SCREEN_ORIENTATION_UNSPECIFIED: + return "SCREEN_ORIENTATION_UNSPECIFIED"; + case SCREEN_ORIENTATION_LANDSCAPE: + return "SCREEN_ORIENTATION_LANDSCAPE"; + case SCREEN_ORIENTATION_PORTRAIT: + return "SCREEN_ORIENTATION_PORTRAIT"; + case SCREEN_ORIENTATION_USER: + return "SCREEN_ORIENTATION_USER"; + case SCREEN_ORIENTATION_BEHIND: + return "SCREEN_ORIENTATION_BEHIND"; + case SCREEN_ORIENTATION_SENSOR: + return "SCREEN_ORIENTATION_SENSOR"; + case SCREEN_ORIENTATION_NOSENSOR: + return "SCREEN_ORIENTATION_NOSENSOR"; + case SCREEN_ORIENTATION_SENSOR_LANDSCAPE: + return "SCREEN_ORIENTATION_SENSOR_LANDSCAPE"; + case SCREEN_ORIENTATION_SENSOR_PORTRAIT: + return "SCREEN_ORIENTATION_SENSOR_PORTRAIT"; + case SCREEN_ORIENTATION_REVERSE_LANDSCAPE: + return "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"; + case SCREEN_ORIENTATION_REVERSE_PORTRAIT: + return "SCREEN_ORIENTATION_REVERSE_PORTRAIT"; + case SCREEN_ORIENTATION_FULL_SENSOR: + return "SCREEN_ORIENTATION_FULL_SENSOR"; + case SCREEN_ORIENTATION_USER_LANDSCAPE: + return "SCREEN_ORIENTATION_USER_LANDSCAPE"; + case SCREEN_ORIENTATION_USER_PORTRAIT: + return "SCREEN_ORIENTATION_USER_PORTRAIT"; + case SCREEN_ORIENTATION_FULL_USER: + return "SCREEN_ORIENTATION_FULL_USER"; + case SCREEN_ORIENTATION_LOCKED: + return "SCREEN_ORIENTATION_LOCKED"; + default: + return Integer.toString(orientation); + } + } + + /** + * @hide + */ + public static String colorModeToString(@ColorMode int colorMode) { + switch (colorMode) { + case COLOR_MODE_DEFAULT: + return "COLOR_MODE_DEFAULT"; + case COLOR_MODE_WIDE_COLOR_GAMUT: + return "COLOR_MODE_WIDE_COLOR_GAMUT"; + case COLOR_MODE_HDR: + return "COLOR_MODE_HDR"; + default: + return Integer.toString(colorMode); + } + } + public static final Parcelable.Creator<ActivityInfo> CREATOR = new Parcelable.Creator<ActivityInfo>() { public ActivityInfo createFromParcel(Parcel source) { diff --git a/android/content/pm/PackageManager.java b/android/content/pm/PackageManager.java index ef8f84bd..31ca1985 100644 --- a/android/content/pm/PackageManager.java +++ b/android/content/pm/PackageManager.java @@ -2330,6 +2330,16 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: The device supports Wi-Fi RTT (IEEE 802.11mc). + * + * @hide RTT_API + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_WIFI_RTT = "android.hardware.wifi.rtt"; + + + /** + * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device supports LoWPAN networking. * @hide */ diff --git a/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java index 4c981cdb..be7f921e 100644 --- a/android/content/pm/PackageManagerInternal.java +++ b/android/content/pm/PackageManagerInternal.java @@ -16,6 +16,9 @@ package android.content.pm; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager.ApplicationInfoFlags; @@ -25,6 +28,8 @@ import android.content.pm.PackageManager.ResolveInfoFlags; import android.os.Bundle; import android.util.SparseArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -33,6 +38,20 @@ import java.util.List; * @hide Only for use within the system server. */ public abstract class PackageManagerInternal { + public static final int PACKAGE_SYSTEM = 0; + public static final int PACKAGE_SETUP_WIZARD = 1; + public static final int PACKAGE_INSTALLER = 2; + public static final int PACKAGE_VERIFIER = 3; + public static final int PACKAGE_BROWSER = 4; + @IntDef(value = { + PACKAGE_SYSTEM, + PACKAGE_SETUP_WIZARD, + PACKAGE_INSTALLER, + PACKAGE_VERIFIER, + PACKAGE_BROWSER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface KnownPackage {} /** * Provider for package names. @@ -172,6 +191,13 @@ public abstract class PackageManagerInternal { @ResolveInfoFlags int flags, int filterCallingUid, int userId); /** + * Retrieve all services that can be performed for the given intent. + * @see PackageManager#queryIntentServices(Intent, int) + */ + public abstract List<ResolveInfo> queryIntentServices( + Intent intent, int flags, int callingUid, int userId); + + /** * Interface to {@link com.android.server.pm.PackageManagerService#getHomeActivitiesAsUser}. */ public abstract ComponentName getHomeActivitiesAsUser(List<ResolveInfo> allHomeCandidates, @@ -343,14 +369,19 @@ public abstract class PackageManagerInternal { * Resolves an activity intent, allowing instant apps to be resolved. */ public abstract ResolveInfo resolveIntent(Intent intent, String resolvedType, - int flags, int userId); + int flags, int userId, boolean resolveForStart); /** * Resolves a service intent, allowing instant apps to be resolved. */ - public abstract ResolveInfo resolveService(Intent intent, String resolvedType, + public abstract ResolveInfo resolveService(Intent intent, String resolvedType, int flags, int userId, int callingUid); + /** + * Resolves a content provider intent. + */ + public abstract ProviderInfo resolveContentProvider(String name, int flags, int userId); + /** * Track the creator of a new isolated uid. * @param isolatedUid The newly created isolated uid. @@ -383,4 +414,59 @@ public abstract class PackageManagerInternal { * Updates a package last used time. */ public abstract void notifyPackageUse(String packageName, int reason); + + /** + * Returns a package object for the given package name. + */ + public abstract @Nullable PackageParser.Package getPackage(@NonNull String packageName); + + /** + * Returns a package object for the disabled system package name. + */ + public abstract @Nullable PackageParser.Package getDisabledPackage(@NonNull String packageName); + + /** + * Returns whether or not the component is the resolver activity. + */ + public abstract boolean isResolveActivityComponent(@NonNull ComponentInfo component); + + /** + * Returns the package name for a known package. + */ + public abstract @Nullable String getKnownPackageName( + @KnownPackage int knownPackage, int userId); + + /** + * Returns whether the package is an instant app. + */ + public abstract boolean isInstantApp(String packageName, int userId); + + /** + * Returns whether the package is an instant app. + */ + public abstract @Nullable String getInstantAppPackageName(int uid); + + /** + * Returns whether or not access to the application should be filtered. + * <p> + * Access may be limited based upon whether the calling or target applications + * are instant applications. + * + * @see #canAccessInstantApps(int) + */ + public abstract boolean filterAppAccess( + @Nullable PackageParser.Package pkg, int callingUid, int userId); + + /* + * NOTE: The following methods are temporary until permissions are extracted from + * the package manager into a component specifically for handling permissions. + */ + /** Returns the flags for the given permission. */ + public abstract @Nullable int getPermissionFlagsTEMP(@NonNull String permName, + @NonNull String packageName, int userId); + /** Updates the flags for the given permission. */ + public abstract void updatePermissionFlagsTEMP(@NonNull String permName, + @NonNull String packageName, int flagMask, int flagValues, int userId); + /** temporary until mPermissionTrees is moved to PermissionManager */ + public abstract Object enforcePermissionTreeTEMP(@NonNull String permName, int callingUid); } diff --git a/android/content/pm/PermissionInfo.java b/android/content/pm/PermissionInfo.java index 17b4f871..b45c26ce 100644 --- a/android/content/pm/PermissionInfo.java +++ b/android/content/pm/PermissionInfo.java @@ -318,16 +318,19 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { return null; } + @Override public String toString() { return "PermissionInfo{" + Integer.toHexString(System.identityHashCode(this)) + " " + name + "}"; } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel dest, int parcelableFlags) { super.writeToParcel(dest, parcelableFlags); dest.writeInt(protectionLevel); @@ -338,11 +341,25 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { TextUtils.writeToParcel(nonLocalizedDescription, dest, parcelableFlags); } + /** @hide */ + public int calculateFootprint() { + int size = name.length(); + if (nonLocalizedLabel != null) { + size += nonLocalizedLabel.length(); + } + if (nonLocalizedDescription != null) { + size += nonLocalizedDescription.length(); + } + return size; + } + public static final Creator<PermissionInfo> CREATOR = new Creator<PermissionInfo>() { + @Override public PermissionInfo createFromParcel(Parcel source) { return new PermissionInfo(source); } + @Override public PermissionInfo[] newArray(int size) { return new PermissionInfo[size]; } diff --git a/android/content/pm/ShortcutInfo.java b/android/content/pm/ShortcutInfo.java index d3a3560c..6b9c7537 100644 --- a/android/content/pm/ShortcutInfo.java +++ b/android/content/pm/ShortcutInfo.java @@ -1763,21 +1763,43 @@ public final class ShortcutInfo implements Parcelable { return 0; } + /** * Return a string representation, intended for logging. Some fields will be retracted. */ @Override public String toString() { - return toStringInner(/* secure =*/ true, /* includeInternalData =*/ false); + return toStringInner(/* secure =*/ true, /* includeInternalData =*/ false, + /*indent=*/ null); } /** @hide */ public String toInsecureString() { - return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true); + return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true, + /*indent=*/ null); + } + + /** @hide */ + public String toDumpString(String indent) { + return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true, indent); + } + + private void addIndentOrComma(StringBuilder sb, String indent) { + if (indent != null) { + sb.append("\n "); + sb.append(indent); + } else { + sb.append(", "); + } } - private String toStringInner(boolean secure, boolean includeInternalData) { + private String toStringInner(boolean secure, boolean includeInternalData, String indent) { final StringBuilder sb = new StringBuilder(); + + if (indent != null) { + sb.append(indent); + } + sb.append("ShortcutInfo {"); sb.append("id="); @@ -1787,47 +1809,51 @@ public final class ShortcutInfo implements Parcelable { sb.append(Integer.toHexString(mFlags)); sb.append(" ["); if (!isEnabled()) { - sb.append("X"); + sb.append("Dis"); } if (isImmutable()) { sb.append("Im"); } if (isManifestShortcut()) { - sb.append("M"); + sb.append("Man"); } if (isDynamic()) { - sb.append("D"); + sb.append("Dyn"); } if (isPinned()) { - sb.append("P"); + sb.append("Pin"); } if (hasIconFile()) { - sb.append("If"); + sb.append("Ic-f"); } if (isIconPendingSave()) { - sb.append("^"); + sb.append("Pens"); } if (hasIconResource()) { - sb.append("Ir"); + sb.append("Ic-r"); } if (hasKeyFieldsOnly()) { - sb.append("K"); + sb.append("Key"); } if (hasStringResourcesResolved()) { - sb.append("Sr"); + sb.append("Str"); } if (isReturnedByServer()) { - sb.append("V"); + sb.append("Rets"); } sb.append("]"); - sb.append(", packageName="); + addIndentOrComma(sb, indent); + + sb.append("packageName="); sb.append(mPackageName); sb.append(", activity="); sb.append(mActivity); - sb.append(", shortLabel="); + addIndentOrComma(sb, indent); + + sb.append("shortLabel="); sb.append(secure ? "***" : mTitle); sb.append(", resId="); sb.append(mTitleResId); @@ -1835,7 +1861,9 @@ public final class ShortcutInfo implements Parcelable { sb.append(mTitleResName); sb.append("]"); - sb.append(", longLabel="); + addIndentOrComma(sb, indent); + + sb.append("longLabel="); sb.append(secure ? "***" : mText); sb.append(", resId="); sb.append(mTextResId); @@ -1843,7 +1871,9 @@ public final class ShortcutInfo implements Parcelable { sb.append(mTextResName); sb.append("]"); - sb.append(", disabledMessage="); + addIndentOrComma(sb, indent); + + sb.append("disabledMessage="); sb.append(secure ? "***" : mDisabledMessage); sb.append(", resId="); sb.append(mDisabledMessageResId); @@ -1851,19 +1881,27 @@ public final class ShortcutInfo implements Parcelable { sb.append(mDisabledMessageResName); sb.append("]"); - sb.append(", categories="); + addIndentOrComma(sb, indent); + + sb.append("categories="); sb.append(mCategories); - sb.append(", icon="); + addIndentOrComma(sb, indent); + + sb.append("icon="); sb.append(mIcon); - sb.append(", rank="); + addIndentOrComma(sb, indent); + + sb.append("rank="); sb.append(mRank); sb.append(", timestamp="); sb.append(mLastChangedTimestamp); - sb.append(", intents="); + addIndentOrComma(sb, indent); + + sb.append("intents="); if (mIntents == null) { sb.append("null"); } else { @@ -1885,12 +1923,15 @@ public final class ShortcutInfo implements Parcelable { } } - sb.append(", extras="); + addIndentOrComma(sb, indent); + + sb.append("extras="); sb.append(mExtras); if (includeInternalData) { + addIndentOrComma(sb, indent); - sb.append(", iconRes="); + sb.append("iconRes="); sb.append(mIconResId); sb.append("["); sb.append(mIconResName); diff --git a/android/content/res/Configuration.java b/android/content/res/Configuration.java index 780e6f76..dfd3bbf0 100644 --- a/android/content/res/Configuration.java +++ b/android/content/res/Configuration.java @@ -16,9 +16,20 @@ package android.content.res; +import static android.content.ConfigurationProto.DENSITY_DPI; +import static android.content.ConfigurationProto.FONT_SCALE; +import static android.content.ConfigurationProto.ORIENTATION; +import static android.content.ConfigurationProto.SCREEN_HEIGHT_DP; +import static android.content.ConfigurationProto.SCREEN_LAYOUT; +import static android.content.ConfigurationProto.SCREEN_WIDTH_DP; +import static android.content.ConfigurationProto.SMALLEST_SCREEN_WIDTH_DP; +import static android.content.ConfigurationProto.UI_MODE; +import static android.content.ConfigurationProto.WINDOW_CONFIGURATION; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.WindowConfiguration; import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.Config; @@ -27,6 +38,7 @@ import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.proto.ProtoOutputStream; import android.view.View; import com.android.internal.util.XmlUtils; @@ -295,11 +307,12 @@ public final class Configuration implements Parcelable, Comparable<Configuration public int screenLayout; /** - * @hide * Configuration relating to the windowing state of the object associated with this * Configuration. Contents of this field are not intended to affect resources, but need to be * communicated and propagated at the same time as the rest of Configuration. + * @hide */ + @TestApi public final WindowConfiguration windowConfiguration = new WindowConfiguration(); /** @hide */ @@ -1054,6 +1067,55 @@ public final class Configuration implements Parcelable, Comparable<Configuration } /** + * Write to a protocol buffer output stream. + * Protocol buffer message definition at {@link android.content.ConfigurationProto} + * + * @param protoOutputStream Stream to write the Configuration object to. + * @param fieldId Field Id of the Configuration as defined in the parent message + * @hide + */ + public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) { + final long token = protoOutputStream.start(fieldId); + protoOutputStream.write(FONT_SCALE, fontScale); + protoOutputStream.write(SCREEN_LAYOUT, screenLayout); + protoOutputStream.write(ORIENTATION, orientation); + protoOutputStream.write(UI_MODE, uiMode); + protoOutputStream.write(SCREEN_WIDTH_DP, screenWidthDp); + protoOutputStream.write(SCREEN_HEIGHT_DP, screenHeightDp); + protoOutputStream.write(SMALLEST_SCREEN_WIDTH_DP, smallestScreenWidthDp); + protoOutputStream.write(DENSITY_DPI, densityDpi); + windowConfiguration.writeToProto(protoOutputStream, WINDOW_CONFIGURATION); + protoOutputStream.end(token); + } + + /** + * Convert the UI mode to a human readable format. + * @hide + */ + public static String uiModeToString(int uiMode) { + switch (uiMode) { + case UI_MODE_TYPE_UNDEFINED: + return "UI_MODE_TYPE_UNDEFINED"; + case UI_MODE_TYPE_NORMAL: + return "UI_MODE_TYPE_NORMAL"; + case UI_MODE_TYPE_DESK: + return "UI_MODE_TYPE_DESK"; + case UI_MODE_TYPE_CAR: + return "UI_MODE_TYPE_CAR"; + case UI_MODE_TYPE_TELEVISION: + return "UI_MODE_TYPE_TELEVISION"; + case UI_MODE_TYPE_APPLIANCE: + return "UI_MODE_TYPE_APPLIANCE"; + case UI_MODE_TYPE_WATCH: + return "UI_MODE_TYPE_WATCH"; + case UI_MODE_TYPE_VR_HEADSET: + return "UI_MODE_TYPE_VR_HEADSET"; + default: + return Integer.toString(uiMode); + } + } + + /** * Set this object to the system defaults. */ public void setToDefaults() { diff --git a/android/content/res/Resources_Theme_Delegate.java b/android/content/res/Resources_Theme_Delegate.java index f1e8fc21..8aa9216b 100644 --- a/android/content/res/Resources_Theme_Delegate.java +++ b/android/content/res/Resources_Theme_Delegate.java @@ -56,7 +56,8 @@ public class Resources_Theme_Delegate { Resources thisResources, Theme thisTheme, int[] attrs) { boolean changed = setupResources(thisTheme); - BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(attrs); + BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().internalObtainStyledAttributes( + 0, attrs); ta.setTheme(thisTheme); restoreResources(changed); return ta; @@ -68,8 +69,8 @@ public class Resources_Theme_Delegate { int resid, int[] attrs) throws NotFoundException { boolean changed = setupResources(thisTheme); - BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(resid, - attrs); + BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().internalObtainStyledAttributes( + resid, attrs); ta.setTheme(thisTheme); restoreResources(changed); return ta; @@ -80,7 +81,7 @@ public class Resources_Theme_Delegate { Resources thisResources, Theme thisTheme, AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { boolean changed = setupResources(thisTheme); - BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(set, + BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().internalObtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes); ta.setTheme(thisTheme); restoreResources(changed); diff --git a/android/database/sqlite/SQLiteConnection.java b/android/database/sqlite/SQLiteConnection.java index f894f053..c28583ea 100644 --- a/android/database/sqlite/SQLiteConnection.java +++ b/android/database/sqlite/SQLiteConnection.java @@ -104,7 +104,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private PreparedStatement mPreparedStatementPool; // The recent operations log. - private final OperationLog mRecentOperations = new OperationLog(); + private final OperationLog mRecentOperations; // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY) private long mConnectionPtr; @@ -162,6 +162,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen SQLiteDatabaseConfiguration configuration, int connectionId, boolean primaryConnection) { mPool = pool; + mRecentOperations = new OperationLog(mPool); mConfiguration = new SQLiteDatabaseConfiguration(configuration); mConnectionId = connectionId; mIsPrimaryConnection = primaryConnection; @@ -1298,6 +1299,11 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS]; private int mIndex; private int mGeneration; + private final SQLiteConnectionPool mPool; + + OperationLog(SQLiteConnectionPool pool) { + mPool = pool; + } public int beginOperation(String kind, String sql, Object[] bindArgs) { synchronized (mOperations) { @@ -1381,8 +1387,10 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } operation.mEndTime = SystemClock.uptimeMillis(); operation.mFinished = true; + final long execTime = operation.mEndTime - operation.mStartTime; + mPool.onStatementExecuted(execTime); return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery( - operation.mEndTime - operation.mStartTime); + execTime); } return false; } @@ -1426,11 +1434,16 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen int index = mIndex; Operation operation = mOperations[index]; if (operation != null) { + // Note: SimpleDateFormat is not thread-safe, cannot be compile-time created, + // and is relatively expensive to create during preloading. This method is only + // used when dumping a connection, which is a rare (mainly error) case. + SimpleDateFormat opDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); int n = 0; do { StringBuilder msg = new StringBuilder(); msg.append(" ").append(n).append(": ["); - msg.append(operation.getFormattedStartTime()); + String formattedStartTime = opDF.format(new Date(operation.mStartWallTime)); + msg.append(formattedStartTime); msg.append("] "); operation.describe(msg, verbose); printer.println(msg.toString()); @@ -1518,12 +1531,5 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen return methodName; } - private String getFormattedStartTime() { - // Note: SimpleDateFormat is not thread-safe, cannot be compile-time created, and is - // relatively expensive to create during preloading. This method is only used - // when dumping a connection, which is a rare (mainly error) case. So: - // DO NOT CACHE. - return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(mStartWallTime)); - } } } diff --git a/android/database/sqlite/SQLiteConnectionPool.java b/android/database/sqlite/SQLiteConnectionPool.java index b66bf18f..8b0fef4f 100644 --- a/android/database/sqlite/SQLiteConnectionPool.java +++ b/android/database/sqlite/SQLiteConnectionPool.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.LockSupport; /** @@ -102,6 +103,8 @@ public final class SQLiteConnectionPool implements Closeable { @GuardedBy("mLock") private IdleConnectionHandler mIdleConnectionHandler; + private final AtomicLong mTotalExecutionTimeCounter = new AtomicLong(0); + // Describes what should happen to an acquired connection when it is returned to the pool. enum AcquiredConnectionStatus { // The connection should be returned to the pool as usual. @@ -523,6 +526,10 @@ public final class SQLiteConnectionPool implements Closeable { mConnectionLeaked.set(true); } + void onStatementExecuted(long executionTimeMs) { + mTotalExecutionTimeCounter.addAndGet(executionTimeMs); + } + // Can't throw. private void closeAvailableConnectionsAndLogExceptionsLocked() { closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); @@ -1076,6 +1083,7 @@ public final class SQLiteConnectionPool implements Closeable { printer.println("Connection pool for " + mConfiguration.path + ":"); printer.println(" Open: " + mIsOpen); printer.println(" Max connections: " + mMaxConnectionPoolSize); + printer.println(" Total execution time: " + mTotalExecutionTimeCounter); if (mConfiguration.isLookasideConfigSet()) { printer.println(" Lookaside config: sz=" + mConfiguration.lookasideSlotSize + " cnt=" + mConfiguration.lookasideSlotCount); diff --git a/android/ext/services/notification/Assistant.java b/android/ext/services/notification/Assistant.java new file mode 100644 index 00000000..f535368b --- /dev/null +++ b/android/ext/services/notification/Assistant.java @@ -0,0 +1,165 @@ +/** + * 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.ext.services.notification; + +import static android.app.NotificationManager.IMPORTANCE_MIN; +import static android.service.notification.NotificationListenerService.Ranking + .USER_SENTIMENT_NEGATIVE; + +import android.app.INotificationManager; +import android.content.Context; +import android.ext.services.R; +import android.os.Bundle; +import android.service.notification.Adjustment; +import android.service.notification.NotificationAssistantService; +import android.service.notification.NotificationStats; +import android.service.notification.StatusBarNotification; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; + +/** + * Notification assistant that provides guidance on notification channel blocking + */ +public class Assistant extends NotificationAssistantService { + private static final String TAG = "ExtAssistant"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final ArrayList<Integer> DISMISS_WITH_PREJUDICE = new ArrayList<>(); + static { + DISMISS_WITH_PREJUDICE.add(REASON_CANCEL); + DISMISS_WITH_PREJUDICE.add(REASON_LISTENER_CANCEL); + } + + // key : impressions tracker + // TODO: persist across reboots + ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>(); + // SBN key : channel id + ArrayMap<String, String> mLiveNotifications = new ArrayMap<>(); + + private Ranking mFakeRanking = null; + + @Override + public Adjustment onNotificationEnqueued(StatusBarNotification sbn) { + if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey()); + return null; + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { + if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey()); + try { + Ranking ranking = getRanking(sbn.getKey(), rankingMap); + if (ranking != null && ranking.getChannel() != null) { + String key = getKey( + sbn.getPackageName(), sbn.getUserId(), ranking.getChannel().getId()); + ChannelImpressions ci = mkeyToImpressions.getOrDefault(key, + new ChannelImpressions()); + if (ranking.getImportance() > IMPORTANCE_MIN && ci.shouldTriggerBlock()) { + adjustNotification(createNegativeAdjustment( + sbn.getPackageName(), sbn.getKey(), sbn.getUserId())); + } + mkeyToImpressions.put(key, ci); + mLiveNotifications.put(sbn.getKey(), ranking.getChannel().getId()); + } + } catch (Throwable e) { + Log.e(TAG, "Error occurred processing post", e); + } + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, + NotificationStats stats, int reason) { + try { + String channelId = mLiveNotifications.remove(sbn.getKey()); + String key = getKey(sbn.getPackageName(), sbn.getUserId(), channelId); + ChannelImpressions ci = mkeyToImpressions.getOrDefault(key, new ChannelImpressions()); + if (stats.hasSeen()) { + ci.incrementViews(); + } + if (DISMISS_WITH_PREJUDICE.contains(reason) + && !sbn.isAppGroup() + && !sbn.getNotification().isGroupChild() + && !stats.hasInteracted() + && stats.getDismissalSurface() != NotificationStats.DISMISSAL_AOD + && stats.getDismissalSurface() != NotificationStats.DISMISSAL_PEEK + && stats.getDismissalSurface() != NotificationStats.DISMISSAL_OTHER) { + if (DEBUG) Log.i(TAG, "increment dismissals"); + ci.incrementDismissals(); + } else { + if (DEBUG) Slog.i(TAG, "reset streak"); + ci.resetStreak(); + } + mkeyToImpressions.put(key, ci); + } catch (Throwable e) { + Slog.e(TAG, "Error occurred processing removal", e); + } + } + + @Override + public void onNotificationSnoozedUntilContext(StatusBarNotification sbn, + String snoozeCriterionId) { + } + + @Override + public void onListenerConnected() { + if (DEBUG) Log.i(TAG, "CONNECTED"); + try { + for (StatusBarNotification sbn : getActiveNotifications()) { + onNotificationPosted(sbn); + } + } catch (Throwable e) { + Log.e(TAG, "Error occurred on connection", e); + } + } + + private String getKey(String pkg, int userId, String channelId) { + return pkg + "|" + userId + "|" + channelId; + } + + private Ranking getRanking(String key, RankingMap rankingMap) { + if (mFakeRanking != null) { + return mFakeRanking; + } + Ranking ranking = new Ranking(); + rankingMap.getRanking(key, ranking); + return ranking; + } + + private Adjustment createNegativeAdjustment(String packageName, String key, int user) { + if (DEBUG) Log.d(TAG, "User probably doesn't want " + key); + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEGATIVE); + return new Adjustment(packageName, key, signals, + getContext().getString(R.string.prompt_block_reason), user); + } + + // for testing + protected void setFakeRanking(Ranking ranking) { + mFakeRanking = ranking; + } + + protected void setNoMan(INotificationManager noMan) { + mNoMan = noMan; + } + + protected void setContext(Context context) { + mSystemContext = context; + } +}
\ No newline at end of file diff --git a/android/ext/services/notification/ChannelImpressions.java b/android/ext/services/notification/ChannelImpressions.java new file mode 100644 index 00000000..30567ccd --- /dev/null +++ b/android/ext/services/notification/ChannelImpressions.java @@ -0,0 +1,137 @@ +/** + * 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.ext.services.notification; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +public final class ChannelImpressions implements Parcelable { + private static final String TAG = "ExtAssistant.CI"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + static final double DISMISS_TO_VIEW_RATIO_LIMIT = .8; + static final int STREAK_LIMIT = 2; + + private int mDismissals = 0; + private int mViews = 0; + private int mStreak = 0; + + public ChannelImpressions() { + } + + public ChannelImpressions(int dismissals, int views) { + mDismissals = dismissals; + mViews = views; + } + + protected ChannelImpressions(Parcel in) { + mDismissals = in.readInt(); + mViews = in.readInt(); + mStreak = in.readInt(); + } + + public int getStreak() { + return mStreak; + } + + public int getDismissals() { + return mDismissals; + } + + public int getViews() { + return mViews; + } + + public void incrementDismissals() { + mDismissals++; + mStreak++; + } + + public void incrementViews() { + mViews++; + } + + public void resetStreak() { + mStreak = 0; + } + + public boolean shouldTriggerBlock() { + if (getViews() == 0) { + return false; + } + if (DEBUG) { + Log.d(TAG, "should trigger? " + getDismissals() + " " + getViews() + " " + getStreak()); + } + return ((double) getDismissals() / getViews()) > DISMISS_TO_VIEW_RATIO_LIMIT + && getStreak() > STREAK_LIMIT; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mDismissals); + dest.writeInt(mViews); + dest.writeInt(mStreak); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ChannelImpressions> CREATOR = new Creator<ChannelImpressions>() { + @Override + public ChannelImpressions createFromParcel(Parcel in) { + return new ChannelImpressions(in); + } + + @Override + public ChannelImpressions[] newArray(int size) { + return new ChannelImpressions[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ChannelImpressions that = (ChannelImpressions) o; + + if (mDismissals != that.mDismissals) return false; + if (mViews != that.mViews) return false; + return mStreak == that.mStreak; + } + + @Override + public int hashCode() { + int result = mDismissals; + result = 31 * result + mViews; + result = 31 * result + mStreak; + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ChannelImpressions{"); + sb.append("mDismissals=").append(mDismissals); + sb.append(", mViews=").append(mViews); + sb.append(", mStreak=").append(mStreak); + sb.append('}'); + return sb.toString(); + } +} diff --git a/android/graphics/BidiRenderer.java b/android/graphics/BidiRenderer.java index 9664a582..7b7dfa6c 100644 --- a/android/graphics/BidiRenderer.java +++ b/android/graphics/BidiRenderer.java @@ -200,8 +200,7 @@ public class BidiRenderer { private static void logFontWarning() { Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, - "Some fonts could not be loaded. The rendering may not be perfect. " + - "Try running the IDE with JRE 7.", null, null); + "Some fonts could not be loaded. The rendering may not be perfect.", null, null); } /** @@ -288,15 +287,17 @@ public class BidiRenderer { @NonNull private static Font getScriptFont(char[] text, int start, int limit, List<FontInfo> fonts) { for (FontInfo fontInfo : fonts) { - if (fontInfo.mFont == null) { - logFontWarning(); - continue; - } if (fontInfo.mFont.canDisplayUpTo(text, start, limit) == -1) { return fontInfo.mFont; } } + if (fonts.isEmpty()) { + logFontWarning(); + // Fallback font in case no font can be loaded + return Font.getFont(Font.SERIF); + } + return fonts.get(0).mFont; } diff --git a/android/graphics/ImageFormat.java b/android/graphics/ImageFormat.java index e3527e35..43fd2708 100644 --- a/android/graphics/ImageFormat.java +++ b/android/graphics/ImageFormat.java @@ -658,6 +658,14 @@ public class ImageFormat { * float confidence = floatDepthBuffer.get(); * </pre> * + * For camera devices that support the + * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT DEPTH_OUTPUT} + * capability, DEPTH_POINT_CLOUD coordinates have units of meters, and the coordinate system is + * defined by the camera's pose transforms: + * {@link android.hardware.camera2.CameraCharacteristics#LENS_POSE_TRANSLATION} and + * {@link android.hardware.camera2.CameraCharacteristics#LENS_POSE_ROTATION}. That means the origin is + * the optical center of the camera device, and the positive Z axis points along the camera's optical axis, + * toward the scene. */ public static final int DEPTH_POINT_CLOUD = 0x101; diff --git a/android/graphics/NinePatch_Delegate.java b/android/graphics/NinePatch_Delegate.java index 43e5b0f9..ce2c18be 100644 --- a/android/graphics/NinePatch_Delegate.java +++ b/android/graphics/NinePatch_Delegate.java @@ -160,8 +160,12 @@ public final class NinePatch_Delegate { } @LayoutlibDelegate - /*package*/ static void nativeFinalize(long chunk) { - sManager.removeJavaReferenceFor(chunk); + /*package*/ static void nativeFinalize(long nativeNinePatch) { + NinePatch_Delegate delegate = sManager.getDelegate(nativeNinePatch); + if (delegate != null && delegate.chunk != null) { + sChunkCache.remove(delegate.chunk); + } + sManager.removeJavaReferenceFor(nativeNinePatch); } diff --git a/android/graphics/PixelFormat.java b/android/graphics/PixelFormat.java index f93886dc..96d6eeec 100644 --- a/android/graphics/PixelFormat.java +++ b/android/graphics/PixelFormat.java @@ -185,4 +185,52 @@ public class PixelFormat { return false; } + + /** + * @hide + */ + public static String formatToString(@Format int format) { + switch (format) { + case UNKNOWN: + return "UNKNOWN"; + case TRANSLUCENT: + return "TRANSLUCENT"; + case TRANSPARENT: + return "TRANSPARENT"; + case RGBA_8888: + return "RGBA_8888"; + case RGBX_8888: + return "RGBX_8888"; + case RGB_888: + return "RGB_888"; + case RGB_565: + return "RGB_565"; + case RGBA_5551: + return "RGBA_5551"; + case RGBA_4444: + return "RGBA_4444"; + case A_8: + return "A_8"; + case L_8: + return "L_8"; + case LA_88: + return "LA_88"; + case RGB_332: + return "RGB_332"; + case YCbCr_422_SP: + return "YCbCr_422_SP"; + case YCbCr_420_SP: + return "YCbCr_420_SP"; + case YCbCr_422_I: + return "YCbCr_422_I"; + case RGBA_F16: + return "RGBA_F16"; + case RGBA_1010102: + return "RGBA_1010102"; + case JPEG: + return "JPEG"; + default: + return Integer.toString(format); + } + } } diff --git a/android/graphics/RadialGradient_Delegate.java b/android/graphics/RadialGradient_Delegate.java index 1defc901..25521d2c 100644 --- a/android/graphics/RadialGradient_Delegate.java +++ b/android/graphics/RadialGradient_Delegate.java @@ -173,6 +173,7 @@ public class RadialGradient_Delegate extends Gradient_Delegate { int index = 0; float[] pt1 = new float[2]; float[] pt2 = new float[2]; + for (int iy = 0 ; iy < h ; iy++) { for (int ix = 0 ; ix < w ; ix++) { // handle the canvas transform @@ -181,12 +182,12 @@ public class RadialGradient_Delegate extends Gradient_Delegate { mCanvasMatrix.transform(pt1, 0, pt2, 0, 1); // handle the local matrix - pt1[0] = pt2[0] - mX; - pt1[1] = pt2[1] - mY; + pt1[0] = pt2[0]; + pt1[1] = pt2[1]; mLocalMatrix.transform(pt1, 0, pt2, 0, 1); - float _x = pt2[0]; - float _y = pt2[1]; + float _x = pt2[0] - mX; + float _y = pt2[1] - mY; float distance = (float) Math.hypot(_x, _y); data[index++] = getGradientColor(distance / mRadius); diff --git a/android/graphics/Shader.java b/android/graphics/Shader.java index 0209cea4..40288f5e 100644 --- a/android/graphics/Shader.java +++ b/android/graphics/Shader.java @@ -159,8 +159,10 @@ public class Shader { if (mNativeInstance == 0) { mNativeInstance = createNativeInstance(mLocalMatrix == null ? 0 : mLocalMatrix.native_instance); - mCleaner = NoImagePreloadHolder.sRegistry.registerNativeAllocation( - this, mNativeInstance); + if (mNativeInstance != 0) { + mCleaner = NoImagePreloadHolder.sRegistry.registerNativeAllocation( + this, mNativeInstance); + } } return mNativeInstance; } diff --git a/android/graphics/drawable/RippleBackground.java b/android/graphics/drawable/RippleBackground.java index 6bd2646f..3bf4f902 100644 --- a/android/graphics/drawable/RippleBackground.java +++ b/android/graphics/drawable/RippleBackground.java @@ -104,7 +104,7 @@ class RippleBackground extends RippleComponent { final AnimatorSet set = new AnimatorSet(); // Linear exit after enter is completed. - final ObjectAnimator exit = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 0); + final ObjectAnimator exit = ObjectAnimator.ofFloat(this, OPACITY, 0); exit.setInterpolator(LINEAR_INTERPOLATOR); exit.setDuration(OPACITY_EXIT_DURATION); exit.setAutoCancel(true); @@ -115,7 +115,7 @@ class RippleBackground extends RippleComponent { final int fastEnterDuration = mIsBounded ? (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST) : 0; if (fastEnterDuration > 0) { - final ObjectAnimator enter = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 1); + final ObjectAnimator enter = ObjectAnimator.ofFloat(this, OPACITY, 1); enter.setInterpolator(LINEAR_INTERPOLATOR); enter.setDuration(fastEnterDuration); enter.setAutoCancel(true); diff --git a/android/graphics/drawable/RippleDrawable.java b/android/graphics/drawable/RippleDrawable.java index 8f314c9c..1727eca5 100644 --- a/android/graphics/drawable/RippleDrawable.java +++ b/android/graphics/drawable/RippleDrawable.java @@ -266,9 +266,9 @@ public class RippleDrawable extends LayerDrawable { } } - setRippleActive(enabled && pressed); - setBackgroundActive(hovered || focused || (enabled && pressed), focused || hovered); + setRippleActive(focused || (enabled && pressed)); + setBackgroundActive(hovered, hovered); return changed; } @@ -693,7 +693,9 @@ public class RippleDrawable extends LayerDrawable { // have a mask or content and the ripple bounds if we're projecting. final Rect bounds = getDirtyBounds(); final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); - canvas.clipRect(bounds); + if (isBounded()) { + canvas.clipRect(bounds); + } drawContent(canvas); drawBackgroundAndRipples(canvas); diff --git a/android/graphics/drawable/RippleForeground.java b/android/graphics/drawable/RippleForeground.java index 829733e9..a675eaf8 100644 --- a/android/graphics/drawable/RippleForeground.java +++ b/android/graphics/drawable/RippleForeground.java @@ -41,7 +41,6 @@ class RippleForeground extends RippleComponent { // Pixel-based accelerations and velocities. private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024; - private static final float WAVE_TOUCH_UP_ACCELERATION = 3400; private static final float WAVE_OPACITY_DECAY_VELOCITY = 3; // Bounded ripple animation properties. @@ -80,17 +79,18 @@ class RippleForeground extends RippleComponent { private float mTweenX = 0; private float mTweenY = 0; - /** Whether this ripple is bounded. */ - private boolean mIsBounded; - /** Whether this ripple has finished its exit animation. */ private boolean mHasFinishedExit; + /** + * If we have a bound, don't start from 0. Start from 60% of the max out of width and height. + */ + private float mStartRadius = 0; + public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, boolean isBounded, boolean forceSoftware) { super(owner, bounds, forceSoftware); - mIsBounded = isBounded; mStartingX = startingX; mStartingY = startingY; @@ -100,6 +100,8 @@ class RippleForeground extends RippleComponent { } else { mBoundedRadius = 0; } + // Take 60% of the maximum of the width and height, then divided half to get the radius. + mStartRadius = Math.max(bounds.width(), bounds.height()) * 0.3f; } @Override @@ -162,24 +164,18 @@ class RippleForeground extends RippleComponent { @Override protected Animator createSoftwareEnter(boolean fast) { - // Bounded ripples don't have enter animations. - if (mIsBounded) { - return null; - } - - final int duration = (int) - (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5); + final int duration = getRadiusDuration(); final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); tweenRadius.setAutoCancel(true); tweenRadius.setDuration(duration); - tweenRadius.setInterpolator(LINEAR_INTERPOLATOR); + tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY); final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); tweenOrigin.setAutoCancel(true); tweenOrigin.setDuration(duration); - tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR); + tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY); final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); @@ -201,45 +197,29 @@ class RippleForeground extends RippleComponent { return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); } - private int getRadiusExitDuration() { + private int getRadiusDuration() { final float remainingRadius = mTargetRadius - getCurrentRadius(); - return (int) (1000 * Math.sqrt(remainingRadius / (WAVE_TOUCH_UP_ACCELERATION - + WAVE_TOUCH_DOWN_ACCELERATION) * mDensityScale) + 0.5); + return (int) (1000 * Math.sqrt(remainingRadius / WAVE_TOUCH_DOWN_ACCELERATION * + mDensityScale) + 0.5); } private float getCurrentRadius() { - return MathUtils.lerp(0, mTargetRadius, mTweenRadius); + return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius); } private int getOpacityExitDuration() { return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); } - /** - * Compute target values that are dependent on bounding. - */ - private void computeBoundedTargetValues() { - mTargetX = (mClampedStartingX - mBounds.exactCenterX()) * .7f; - mTargetY = (mClampedStartingY - mBounds.exactCenterY()) * .7f; - mTargetRadius = mBoundedRadius; - } - @Override protected Animator createSoftwareExit() { final int radiusDuration; final int originDuration; final int opacityDuration; - if (mIsBounded) { - computeBoundedTargetValues(); - radiusDuration = BOUNDED_RADIUS_EXIT_DURATION; - originDuration = BOUNDED_ORIGIN_EXIT_DURATION; - opacityDuration = BOUNDED_OPACITY_EXIT_DURATION; - } else { - radiusDuration = getRadiusExitDuration(); - originDuration = radiusDuration; - opacityDuration = getOpacityExitDuration(); - } + radiusDuration = getRadiusDuration(); + originDuration = radiusDuration; + opacityDuration = getOpacityExitDuration(); final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); tweenRadius.setAutoCancel(true); @@ -268,17 +248,10 @@ class RippleForeground extends RippleComponent { final int radiusDuration; final int originDuration; final int opacityDuration; - if (mIsBounded) { - computeBoundedTargetValues(); - radiusDuration = BOUNDED_RADIUS_EXIT_DURATION; - originDuration = BOUNDED_ORIGIN_EXIT_DURATION; - opacityDuration = BOUNDED_OPACITY_EXIT_DURATION; - } else { - radiusDuration = getRadiusExitDuration(); - originDuration = radiusDuration; - opacityDuration = getOpacityExitDuration(); - } + radiusDuration = getRadiusDuration(); + originDuration = radiusDuration; + opacityDuration = getOpacityExitDuration(); final float startX = getCurrentX(); final float startY = getCurrentY(); diff --git a/android/graphics/drawable/VectorDrawable_Delegate.java b/android/graphics/drawable/VectorDrawable_Delegate.java index 5fa71021..00630464 100644 --- a/android/graphics/drawable/VectorDrawable_Delegate.java +++ b/android/graphics/drawable/VectorDrawable_Delegate.java @@ -35,6 +35,7 @@ import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.Path_Delegate; import android.graphics.Rect; +import android.graphics.Region; import android.graphics.Region.Op; import android.graphics.Shader_Delegate; import android.util.ArrayMap; @@ -144,6 +145,9 @@ public class VectorDrawable_Delegate { VPathRenderer_Delegate nativePathRenderer = VNativeObject.getDelegate(rendererPtr); Canvas_Delegate.nSave(canvasWrapperPtr, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG); + Canvas_Delegate.nClipRect(canvasWrapperPtr, + bounds.left, bounds.top, bounds.right, bounds.bottom, + Region.Op.INTERSECT.nativeInt); Canvas_Delegate.nTranslate(canvasWrapperPtr, bounds.left, bounds.top); if (needsMirroring) { diff --git a/android/hardware/Camera.java b/android/hardware/Camera.java index aa35a661..931b5c91 100644 --- a/android/hardware/Camera.java +++ b/android/hardware/Camera.java @@ -16,10 +16,11 @@ package android.hardware; -import android.app.ActivityThread; +import static android.system.OsConstants.*; + import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; -import android.app.job.JobInfo; +import android.app.ActivityThread; import android.content.Context; import android.graphics.ImageFormat; import android.graphics.Point; @@ -34,11 +35,11 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.renderscript.Allocation; import android.renderscript.Element; -import android.renderscript.RenderScript; import android.renderscript.RSIllegalArgumentException; +import android.renderscript.RenderScript; import android.renderscript.Type; -import android.util.Log; import android.text.TextUtils; +import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; @@ -48,8 +49,6 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; -import static android.system.OsConstants.*; - /** * The Camera class is used to set image capture settings, start/stop preview, * snap pictures, and retrieve frames for encoding for video. This class is a @@ -243,12 +242,19 @@ public class Camera { /** * Returns the number of physical cameras available on this device. + * + * @return total number of accessible camera devices, or 0 if there are no + * cameras or an error was encountered enumerating them. */ public native static int getNumberOfCameras(); /** * Returns the information about a particular camera. * If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1. + * + * @throws RuntimeException if an invalid ID is provided, or if there is an + * error retrieving the information (generally due to a hardware or other + * low-level failure). */ public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) { _getCameraInfo(cameraId, cameraInfo); @@ -362,7 +368,10 @@ public class Camera { /** * Creates a new Camera object to access the first back-facing camera on the * device. If the device does not have a back-facing camera, this returns - * null. + * null. Otherwise acts like the {@link #open(int)} call. + * + * @return a new Camera object for the first back-facing camera, or null if there is no + * backfacing camera * @see #open(int) */ public static Camera open() { @@ -609,6 +618,8 @@ public class Camera { * * @throws IOException if a connection cannot be re-established (for * example, if the camera is still in use by another process). + * @throws RuntimeException if release() has been called on this Camera + * instance. */ public native final void reconnect() throws IOException; @@ -637,6 +648,8 @@ public class Camera { * or null to remove the preview surface * @throws IOException if the method fails (for example, if the surface * is unavailable or unsuitable). + * @throws RuntimeException if release() has been called on this Camera + * instance. */ public final void setPreviewDisplay(SurfaceHolder holder) throws IOException { if (holder != null) { @@ -684,6 +697,8 @@ public class Camera { * texture * @throws IOException if the method fails (for example, if the surface * texture is unavailable or unsuitable). + * @throws RuntimeException if release() has been called on this Camera + * instance. */ public native final void setPreviewTexture(SurfaceTexture surfaceTexture) throws IOException; @@ -733,12 +748,20 @@ public class Camera { * {@link #setPreviewCallbackWithBuffer(Camera.PreviewCallback)} were * called, {@link Camera.PreviewCallback#onPreviewFrame(byte[], Camera)} * will be called when preview data becomes available. + * + * @throws RuntimeException if starting preview fails; usually this would be + * because of a hardware or other low-level error, or because release() + * has been called on this Camera instance. */ public native final void startPreview(); /** * Stops capturing and drawing preview frames to the surface, and * resets the camera for a future call to {@link #startPreview()}. + * + * @throws RuntimeException if stopping preview fails; usually this would be + * because of a hardware or other low-level error, or because release() + * has been called on this Camera instance. */ public final void stopPreview() { _stopPreview(); @@ -777,6 +800,8 @@ public class Camera { * * @param cb a callback object that receives a copy of each preview frame, * or null to stop receiving callbacks. + * @throws RuntimeException if release() has been called on this Camera + * instance. * @see android.media.MediaActionSound */ public final void setPreviewCallback(PreviewCallback cb) { @@ -803,6 +828,8 @@ public class Camera { * * @param cb a callback object that receives a copy of the next preview frame, * or null to stop receiving callbacks. + * @throws RuntimeException if release() has been called on this Camera + * instance. * @see android.media.MediaActionSound */ public final void setOneShotPreviewCallback(PreviewCallback cb) { @@ -840,6 +867,8 @@ public class Camera { * * @param cb a callback object that receives a copy of the preview frame, * or null to stop receiving callbacks and clear the buffer queue. + * @throws RuntimeException if release() has been called on this Camera + * instance. * @see #addCallbackBuffer(byte[]) * @see android.media.MediaActionSound */ @@ -1259,6 +1288,9 @@ public class Camera { * success sound to the user.</p> * * @param cb the callback to run + * @throws RuntimeException if starting autofocus fails; usually this would + * be because of a hardware or other low-level error, or because + * release() has been called on this Camera instance. * @see #cancelAutoFocus() * @see android.hardware.Camera.Parameters#setAutoExposureLock(boolean) * @see android.hardware.Camera.Parameters#setAutoWhiteBalanceLock(boolean) @@ -1279,6 +1311,9 @@ public class Camera { * this function will return the focus position to the default. * If the camera does not support auto-focus, this is a no-op. * + * @throws RuntimeException if canceling autofocus fails; usually this would + * be because of a hardware or other low-level error, or because + * release() has been called on this Camera instance. * @see #autoFocus(Camera.AutoFocusCallback) */ public final void cancelAutoFocus() @@ -1333,6 +1368,9 @@ public class Camera { * Sets camera auto-focus move callback. * * @param cb the callback to run + * @throws RuntimeException if enabling the focus move callback fails; + * usually this would be because of a hardware or other low-level error, + * or because release() has been called on this Camera instance. */ public void setAutoFocusMoveCallback(AutoFocusMoveCallback cb) { mAutoFocusMoveCallback = cb; @@ -1384,7 +1422,7 @@ public class Camera { }; /** - * Equivalent to takePicture(shutter, raw, null, jpeg). + * Equivalent to <pre>takePicture(Shutter, raw, null, jpeg)</pre>. * * @see #takePicture(ShutterCallback, PictureCallback, PictureCallback, PictureCallback) */ @@ -1422,6 +1460,9 @@ public class Camera { * @param raw the callback for raw (uncompressed) image data, or null * @param postview callback with postview image data, may be null * @param jpeg the callback for JPEG image data, or null + * @throws RuntimeException if starting picture capture fails; usually this + * would be because of a hardware or other low-level error, or because + * release() has been called on this Camera instance. */ public final void takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback postview, PictureCallback jpeg) { @@ -1534,6 +1575,9 @@ public class Camera { * * @param degrees the angle that the picture will be rotated clockwise. * Valid values are 0, 90, 180, and 270. + * @throws RuntimeException if setting orientation fails; usually this would + * be because of a hardware or other low-level error, or because + * release() has been called on this Camera instance. * @see #setPreviewDisplay(SurfaceHolder) */ public native final void setDisplayOrientation(int degrees); @@ -1559,6 +1603,9 @@ public class Camera { * changed. {@code false} if the shutter sound state could not be * changed. {@code true} is also returned if shutter sound playback * is already set to the requested state. + * @throws RuntimeException if the call fails; usually this would be because + * of a hardware or other low-level error, or because release() has been + * called on this Camera instance. * @see #takePicture * @see CameraInfo#canDisableShutterSound * @see ShutterCallback @@ -1903,6 +1950,9 @@ public class Camera { * If modifications are made to the returned Parameters, they must be passed * to {@link #setParameters(Camera.Parameters)} to take effect. * + * @throws RuntimeException if reading parameters fails; usually this would + * be because of a hardware or other low-level error, or because + * release() has been called on this Camera instance. * @see #setParameters(Camera.Parameters) */ public Parameters getParameters() { diff --git a/android/hardware/LegacySensorManager.java b/android/hardware/LegacySensorManager.java index f5cf3f74..098121df 100644 --- a/android/hardware/LegacySensorManager.java +++ b/android/hardware/LegacySensorManager.java @@ -204,7 +204,7 @@ final class LegacySensorManager { } private static final class LegacyListener implements SensorEventListener { - private float mValues[] = new float[6]; + private float[] mValues = new float[6]; private SensorListener mTarget; private int mSensors; private final LmsFilter mYawfilter = new LmsFilter(); @@ -256,7 +256,7 @@ final class LegacySensorManager { } public void onSensorChanged(SensorEvent event) { - final float v[] = mValues; + final float[] v = mValues; v[0] = event.values[0]; v[1] = event.values[1]; v[2] = event.values[2]; @@ -264,10 +264,10 @@ final class LegacySensorManager { int legacyType = getLegacySensorType(type); mapSensorDataToWindow(legacyType, v, LegacySensorManager.getRotation()); if (type == Sensor.TYPE_ORIENTATION) { - if ((mSensors & SensorManager.SENSOR_ORIENTATION_RAW)!=0) { + if ((mSensors & SensorManager.SENSOR_ORIENTATION_RAW) != 0) { mTarget.onSensorChanged(SensorManager.SENSOR_ORIENTATION_RAW, v); } - if ((mSensors & SensorManager.SENSOR_ORIENTATION)!=0) { + if ((mSensors & SensorManager.SENSOR_ORIENTATION) != 0) { v[0] = mYawfilter.filter(event.timestamp, v[0]); mTarget.onSensorChanged(SensorManager.SENSOR_ORIENTATION, v); } @@ -317,7 +317,7 @@ final class LegacySensorManager { switch (sensor) { case SensorManager.SENSOR_ACCELEROMETER: case SensorManager.SENSOR_MAGNETIC_FIELD: - values[0] =-y; + values[0] = -y; values[1] = x; values[2] = z; break; @@ -337,15 +337,15 @@ final class LegacySensorManager { switch (sensor) { case SensorManager.SENSOR_ACCELEROMETER: case SensorManager.SENSOR_MAGNETIC_FIELD: - values[0] =-x; - values[1] =-y; + values[0] = -x; + values[1] = -y; values[2] = z; break; case SensorManager.SENSOR_ORIENTATION: case SensorManager.SENSOR_ORIENTATION_RAW: values[0] = (x >= 180) ? (x - 180) : (x + 180); - values[1] =-y; - values[2] =-z; + values[1] = -y; + values[2] = -z; break; } } @@ -369,10 +369,11 @@ final class LegacySensorManager { private static final class LmsFilter { private static final int SENSORS_RATE_MS = 20; private static final int COUNT = 12; - private static final float PREDICTION_RATIO = 1.0f/3.0f; - private static final float PREDICTION_TIME = (SENSORS_RATE_MS*COUNT/1000.0f)*PREDICTION_RATIO; - private float mV[] = new float[COUNT*2]; - private long mT[] = new long[COUNT*2]; + private static final float PREDICTION_RATIO = 1.0f / 3.0f; + private static final float PREDICTION_TIME = + (SENSORS_RATE_MS * COUNT / 1000.0f) * PREDICTION_RATIO; + private float[] mV = new float[COUNT * 2]; + private long[] mT = new long[COUNT * 2]; private int mIndex; public LmsFilter() { @@ -383,9 +384,9 @@ final class LegacySensorManager { float v = in; final float ns = 1.0f / 1000000000.0f; float v1 = mV[mIndex]; - if ((v-v1) > 180) { + if ((v - v1) > 180) { v -= 360; - } else if ((v1-v) > 180) { + } else if ((v1 - v) > 180) { v += 360; } /* Manage the circular buffer, we write the data twice spaced @@ -393,40 +394,43 @@ final class LegacySensorManager { * when it's full */ mIndex++; - if (mIndex >= COUNT*2) + if (mIndex >= COUNT * 2) { mIndex = COUNT; + } mV[mIndex] = v; mT[mIndex] = time; - mV[mIndex-COUNT] = v; - mT[mIndex-COUNT] = time; + mV[mIndex - COUNT] = v; + mT[mIndex - COUNT] = time; float A, B, C, D, E; float a, b; int i; A = B = C = D = E = 0; - for (i=0 ; i<COUNT-1 ; i++) { + for (i = 0; i < COUNT - 1; i++) { final int j = mIndex - 1 - i; final float Z = mV[j]; - final float T = (mT[j]/2 + mT[j+1]/2 - time)*ns; - float dT = (mT[j] - mT[j+1])*ns; + final float T = (mT[j] / 2 + mT[j + 1] / 2 - time) * ns; + float dT = (mT[j] - mT[j + 1]) * ns; dT *= dT; - A += Z*dT; - B += T*(T*dT); - C += (T*dT); - D += Z*(T*dT); + A += Z * dT; + B += T * (T * dT); + C += (T * dT); + D += Z * (T * dT); E += dT; } - b = (A*B + C*D) / (E*B + C*C); - a = (E*b - A) / C; - float f = b + PREDICTION_TIME*a; + b = (A * B + C * D) / (E * B + C * C); + a = (E * b - A) / C; + float f = b + PREDICTION_TIME * a; // Normalize f *= (1.0f / 360.0f); - if (((f>=0)?f:-f) >= 0.5f) - f = f - (float)Math.ceil(f + 0.5f) + 1.0f; - if (f < 0) + if (((f >= 0) ? f : -f) >= 0.5f) { + f = f - (float) Math.ceil(f + 0.5f) + 1.0f; + } + if (f < 0) { f += 1.0f; + } f *= 360.0f; return f; } diff --git a/android/hardware/Sensor.java b/android/hardware/Sensor.java index f02e4849..7fb0c89e 100644 --- a/android/hardware/Sensor.java +++ b/android/hardware/Sensor.java @@ -794,12 +794,12 @@ public final class Sensor { 1, // SENSOR_TYPE_PICK_UP_GESTURE 1, // SENSOR_TYPE_WRIST_TILT_GESTURE 1, // SENSOR_TYPE_DEVICE_ORIENTATION - 16,// SENSOR_TYPE_POSE_6DOF + 16, // SENSOR_TYPE_POSE_6DOF 1, // SENSOR_TYPE_STATIONARY_DETECT 1, // SENSOR_TYPE_MOTION_DETECT 1, // SENSOR_TYPE_HEART_BEAT 2, // SENSOR_TYPE_DYNAMIC_SENSOR_META - 16,// skip over additional sensor info type + 16, // skip over additional sensor info type 1, // SENSOR_TYPE_LOW_LATENCY_OFFBODY_DETECT 6, // SENSOR_TYPE_ACCELEROMETER_UNCALIBRATED }; @@ -857,8 +857,8 @@ public final class Sensor { static int getMaxLengthValuesArray(Sensor sensor, int sdkLevel) { // RotationVector length has changed to 3 to 5 for API level 18 // Set it to 3 for backward compatibility. - if (sensor.mType == Sensor.TYPE_ROTATION_VECTOR && - sdkLevel <= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (sensor.mType == Sensor.TYPE_ROTATION_VECTOR + && sdkLevel <= Build.VERSION_CODES.JELLY_BEAN_MR1) { return 3; } int offset = sensor.mType; @@ -1033,9 +1033,9 @@ public final class Sensor { * Returns true if the sensor is a wake-up sensor. * <p> * <b>Application Processor Power modes</b> <p> - * Application Processor(AP), is the processor on which applications run. When no wake lock is held - * and the user is not interacting with the device, this processor can enter a “Suspend” mode, - * reducing the power consumption by 10 times or more. + * Application Processor(AP), is the processor on which applications run. When no wake lock is + * held and the user is not interacting with the device, this processor can enter a “Suspend” + * mode, reducing the power consumption by 10 times or more. * </p> * <p> * <b>Non-wake-up sensors</b> <p> @@ -1232,6 +1232,6 @@ public final class Sensor { */ private void setUuid(long msb, long lsb) { // TODO(b/29547335): Rename this method to setId. - mId = (int)msb; + mId = (int) msb; } } diff --git a/android/hardware/SensorAdditionalInfo.java b/android/hardware/SensorAdditionalInfo.java index 0c6a4151..7c876cfc 100644 --- a/android/hardware/SensorAdditionalInfo.java +++ b/android/hardware/SensorAdditionalInfo.java @@ -200,7 +200,7 @@ public class SensorAdditionalInfo { public static final int TYPE_DEBUG_INFO = 0x40000000; SensorAdditionalInfo( - Sensor aSensor, int aType, int aSerial, int [] aIntValues, float [] aFloatValues) { + Sensor aSensor, int aType, int aSerial, int[] aIntValues, float[] aFloatValues) { sensor = aSensor; type = aType; serial = aSerial; @@ -222,10 +222,10 @@ public class SensorAdditionalInfo { null, new float[] { strength, declination, inclination}); } /** @hide */ - public static SensorAdditionalInfo createCustomInfo(Sensor aSensor, int type, float [] data) { + public static SensorAdditionalInfo createCustomInfo(Sensor aSensor, int type, float[] data) { if (type < TYPE_CUSTOM_INFO || type >= TYPE_DEBUG_INFO || aSensor == null) { - throw new IllegalArgumentException("invalid parameter(s): type: " + type + - "; sensor: " + aSensor); + throw new IllegalArgumentException( + "invalid parameter(s): type: " + type + "; sensor: " + aSensor); } return new SensorAdditionalInfo(aSensor, type, 0, null, data); diff --git a/android/hardware/SensorEvent.java b/android/hardware/SensorEvent.java index c0bca97e..bbd04a31 100644 --- a/android/hardware/SensorEvent.java +++ b/android/hardware/SensorEvent.java @@ -207,8 +207,8 @@ public class SensorEvent { * timestamp = event.timestamp; * float[] deltaRotationMatrix = new float[9]; * SensorManager.getRotationMatrixFromVector(deltaRotationMatrix, deltaRotationVector); - * // User code should concatenate the delta rotation we computed with the current rotation - * // in order to get the updated rotation. + * // User code should concatenate the delta rotation we computed with the current + * // rotation in order to get the updated rotation. * // rotationCurrent = rotationCurrent * deltaRotationMatrix; * } * </pre> @@ -244,21 +244,22 @@ public class SensorEvent { * <h4>{@link android.hardware.Sensor#TYPE_GRAVITY Sensor.TYPE_GRAVITY}:</h4> * <p>A three dimensional vector indicating the direction and magnitude of gravity. Units * are m/s^2. The coordinate system is the same as is used by the acceleration sensor.</p> - * <p><b>Note:</b> When the device is at rest, the output of the gravity sensor should be identical - * to that of the accelerometer.</p> - * - * <h4>{@link android.hardware.Sensor#TYPE_LINEAR_ACCELERATION Sensor.TYPE_LINEAR_ACCELERATION}:</h4> - * A three dimensional vector indicating acceleration along each device axis, not including - * gravity. All values have units of m/s^2. The coordinate system is the same as is used by the - * acceleration sensor. + * <p><b>Note:</b> When the device is at rest, the output of the gravity sensor should be + * identical to that of the accelerometer.</p> + * + * <h4> + * {@link android.hardware.Sensor#TYPE_LINEAR_ACCELERATION Sensor.TYPE_LINEAR_ACCELERATION}: + * </h4> A three dimensional vector indicating acceleration along each device axis, not + * including gravity. All values have units of m/s^2. The coordinate system is the same as is + * used by the acceleration sensor. * <p>The output of the accelerometer, gravity and linear-acceleration sensors must obey the * following relation:</p> - * <p><ul>acceleration = gravity + linear-acceleration</ul></p> + * <p><ul>acceleration = gravity + linear-acceleration</ul></p> * * <h4>{@link android.hardware.Sensor#TYPE_ROTATION_VECTOR Sensor.TYPE_ROTATION_VECTOR}:</h4> - * <p>The rotation vector represents the orientation of the device as a combination of an <i>angle</i> - * and an <i>axis</i>, in which the device has rotated through an angle θ around an axis - * <x, y, z>.</p> + * <p>The rotation vector represents the orientation of the device as a combination of an + * <i>angle</i> and an <i>axis</i>, in which the device has rotated through an angle θ + * around an axis <x, y, z>.</p> * <p>The three elements of the rotation vector are * <x*sin(θ/2), y*sin(θ/2), z*sin(θ/2)>, such that the magnitude of the rotation * vector is equal to sin(θ/2), and the direction of the rotation vector is equal to the diff --git a/android/hardware/SensorListener.java b/android/hardware/SensorListener.java index c71e968d..e2033b6d 100644 --- a/android/hardware/SensorListener.java +++ b/android/hardware/SensorListener.java @@ -19,8 +19,8 @@ package android.hardware; /** * Used for receiving notifications from the SensorManager when * sensor values have changed. - * - * @deprecated Use + * + * @deprecated Use * {@link android.hardware.SensorEventListener SensorEventListener} instead. */ @Deprecated @@ -36,7 +36,7 @@ public interface SensorListener { * <p><u>Definition of the coordinate system used below.</u><p> * <p>The X axis refers to the screen's horizontal axis * (the small edge in portrait mode, the long edge in landscape mode) and - * points to the right. + * points to the right. * <p>The Y axis refers to the screen's vertical axis and points towards * the top of the screen (the origin is in the lower-left corner). * <p>The Z axis points toward the sky when the device is lying on its back @@ -44,18 +44,18 @@ public interface SensorListener { * <p> <b>IMPORTANT NOTE:</b> The axis <b><u>are swapped</u></b> when the * device's screen orientation changes. To access the unswapped values, * use indices 3, 4 and 5 in values[]. - * + * * <p>{@link android.hardware.SensorManager#SENSOR_ORIENTATION SENSOR_ORIENTATION}, * {@link android.hardware.SensorManager#SENSOR_ORIENTATION_RAW SENSOR_ORIENTATION_RAW}:<p> * All values are angles in degrees. - * + * * <p>values[0]: Azimuth, rotation around the Z axis (0<=azimuth<360). * 0 = North, 90 = East, 180 = South, 270 = West - * + * * <p>values[1]: Pitch, rotation around X axis (-180<=pitch<=180), with positive * values when the z-axis moves toward the y-axis. * - * <p>values[2]: Roll, rotation around Y axis (-90<=roll<=90), with positive values + * <p>values[2]: Roll, rotation around Y axis (-90<=roll<=90), with positive values * when the z-axis moves toward the x-axis. * * <p>Note that this definition of yaw, pitch and roll is different from the @@ -64,17 +64,17 @@ public interface SensorListener { * * <p>{@link android.hardware.SensorManager#SENSOR_ACCELEROMETER SENSOR_ACCELEROMETER}:<p> * All values are in SI units (m/s^2) and measure contact forces. - * - * <p>values[0]: force applied by the device on the x-axis - * <p>values[1]: force applied by the device on the y-axis + * + * <p>values[0]: force applied by the device on the x-axis + * <p>values[1]: force applied by the device on the y-axis * <p>values[2]: force applied by the device on the z-axis - * + * * <p><u>Examples</u>: * <li>When the device is pushed on its left side toward the right, the * x acceleration value is negative (the device applies a reaction force * to the push toward the left)</li> - * - * <li>When the device lies flat on a table, the acceleration value is + * + * <li>When the device lies flat on a table, the acceleration value is * {@link android.hardware.SensorManager#STANDARD_GRAVITY -STANDARD_GRAVITY}, * which correspond to the force the device applies on the table in reaction * to gravity.</li> @@ -83,7 +83,7 @@ public interface SensorListener { * All values are in micro-Tesla (uT) and measure the ambient magnetic * field in the X, Y and -Z axis. * <p><b><u>Note:</u></b> the magnetic field's Z axis is inverted. - * + * * @param sensor The ID of the sensor being monitored * @param values The new values for the sensor. */ @@ -97,5 +97,5 @@ public interface SensorListener { * @param sensor The ID of the sensor being monitored * @param accuracy The new accuracy of this sensor. */ - public void onAccuracyChanged(int sensor, int accuracy); + public void onAccuracyChanged(int sensor, int accuracy); } diff --git a/android/hardware/SensorManager.java b/android/hardware/SensorManager.java index e1cd451b..35aaf78b 100644 --- a/android/hardware/SensorManager.java +++ b/android/hardware/SensorManager.java @@ -83,7 +83,7 @@ public abstract class SensorManager { /** @hide */ protected static final String TAG = "SensorManager"; - private static final float[] mTempMatrix = new float[16]; + private static final float[] sTempMatrix = new float[16]; // Cached lists of sensors by type. Guarded by mSensorListByType. private final SparseArray<List<Sensor>> mSensorListByType = @@ -188,7 +188,7 @@ public abstract class SensorManager { * @deprecated use {@link android.hardware.Sensor Sensor} instead. */ @Deprecated - public static final int SENSOR_MAX = ((SENSOR_ALL + 1)>>1); + public static final int SENSOR_MAX = ((SENSOR_ALL + 1) >> 1); /** @@ -425,8 +425,9 @@ public abstract class SensorManager { } else { list = new ArrayList<Sensor>(); for (Sensor i : fullList) { - if (i.getType() == type) + if (i.getType() == type) { list.add(i); + } } } list = Collections.unmodifiableList(list); @@ -461,8 +462,9 @@ public abstract class SensorManager { } else { List<Sensor> list = new ArrayList(); for (Sensor i : fullList) { - if (i.getType() == type) + if (i.getType() == type) { list.add(i); + } } return Collections.unmodifiableList(list); } @@ -490,10 +492,11 @@ public abstract class SensorManager { // For the following sensor types, return a wake-up sensor. These types are by default // defined as wake-up sensors. For the rest of the SDK defined sensor types return a // non_wake-up version. - if (type == Sensor.TYPE_PROXIMITY || type == Sensor.TYPE_SIGNIFICANT_MOTION || - type == Sensor.TYPE_TILT_DETECTOR || type == Sensor.TYPE_WAKE_GESTURE || - type == Sensor.TYPE_GLANCE_GESTURE || type == Sensor.TYPE_PICK_UP_GESTURE || - type == Sensor.TYPE_WRIST_TILT_GESTURE || type == Sensor.TYPE_DYNAMIC_SENSOR_META) { + if (type == Sensor.TYPE_PROXIMITY || type == Sensor.TYPE_SIGNIFICANT_MOTION + || type == Sensor.TYPE_TILT_DETECTOR || type == Sensor.TYPE_WAKE_GESTURE + || type == Sensor.TYPE_GLANCE_GESTURE || type == Sensor.TYPE_PICK_UP_GESTURE + || type == Sensor.TYPE_WRIST_TILT_GESTURE + || type == Sensor.TYPE_DYNAMIC_SENSOR_META) { wakeUpSensor = true; } @@ -509,12 +512,12 @@ public abstract class SensorManager { * <p> * For example, * <ul> - * <li>getDefaultSensor({@link Sensor#TYPE_ACCELEROMETER}, true) returns a wake-up accelerometer - * sensor if it exists. </li> - * <li>getDefaultSensor({@link Sensor#TYPE_PROXIMITY}, false) returns a non wake-up proximity - * sensor if it exists. </li> - * <li>getDefaultSensor({@link Sensor#TYPE_PROXIMITY}, true) returns a wake-up proximity sensor - * which is the same as the Sensor returned by {@link #getDefaultSensor(int)}. </li> + * <li>getDefaultSensor({@link Sensor#TYPE_ACCELEROMETER}, true) returns a wake-up + * accelerometer sensor if it exists. </li> + * <li>getDefaultSensor({@link Sensor#TYPE_PROXIMITY}, false) returns a non wake-up + * proximity sensor if it exists. </li> + * <li>getDefaultSensor({@link Sensor#TYPE_PROXIMITY}, true) returns a wake-up proximity + * sensor which is the same as the Sensor returned by {@link #getDefaultSensor(int)}. </li> * </ul> * </p> * <p class="note"> @@ -532,8 +535,9 @@ public abstract class SensorManager { public Sensor getDefaultSensor(int type, boolean wakeUp) { List<Sensor> l = getSensorList(type); for (Sensor sensor : l) { - if (sensor.isWakeUpSensor() == wakeUp) + if (sensor.isWakeUpSensor() == wakeUp) { return sensor; + } } return null; } @@ -842,8 +846,8 @@ public abstract class SensorManager { * @return <code>true</code> if the sensor is supported and successfully enabled. * @see #registerListener(SensorEventListener, Sensor, int, int) */ - public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs, - int maxReportLatencyUs, Handler handler) { + public boolean registerListener(SensorEventListener listener, Sensor sensor, + int samplingPeriodUs, int maxReportLatencyUs, Handler handler) { int delayUs = getDelay(samplingPeriodUs); return registerListenerImpl(listener, sensor, delayUs, handler, maxReportLatencyUs, 0); } @@ -953,7 +957,7 @@ public abstract class SensorManager { * Used for receiving notifications from the SensorManager when dynamic sensors are connected or * disconnected. */ - public static abstract class DynamicSensorCallback { + public abstract static class DynamicSensorCallback { /** * Called when there is a dynamic sensor being connected to the system. * @@ -1180,7 +1184,7 @@ public abstract class SensorManager { float Ay = gravity[1]; float Az = gravity[2]; - final float normsqA = (Ax*Ax + Ay*Ay + Az*Az); + final float normsqA = (Ax * Ax + Ay * Ay + Az * Az); final float g = 9.81f; final float freeFallGravitySquared = 0.01f * g * g; if (normsqA < freeFallGravitySquared) { @@ -1191,10 +1195,10 @@ public abstract class SensorManager { final float Ex = geomagnetic[0]; final float Ey = geomagnetic[1]; final float Ez = geomagnetic[2]; - float Hx = Ey*Az - Ez*Ay; - float Hy = Ez*Ax - Ex*Az; - float Hz = Ex*Ay - Ey*Ax; - final float normH = (float)Math.sqrt(Hx*Hx + Hy*Hy + Hz*Hz); + float Hx = Ey * Az - Ez * Ay; + float Hy = Ez * Ax - Ex * Az; + float Hz = Ex * Ay - Ey * Ax; + final float normH = (float) Math.sqrt(Hx * Hx + Hy * Hy + Hz * Hz); if (normH < 0.1f) { // device is close to free fall (or in space?), or close to @@ -1205,13 +1209,13 @@ public abstract class SensorManager { Hx *= invH; Hy *= invH; Hz *= invH; - final float invA = 1.0f / (float)Math.sqrt(Ax*Ax + Ay*Ay + Az*Az); + final float invA = 1.0f / (float) Math.sqrt(Ax * Ax + Ay * Ay + Az * Az); Ax *= invA; Ay *= invA; Az *= invA; - final float Mx = Ay*Hz - Az*Hy; - final float My = Az*Hx - Ax*Hz; - final float Mz = Ax*Hy - Ay*Hx; + final float Mx = Ay * Hz - Az * Hy; + final float My = Az * Hx - Ax * Hz; + final float Mz = Ax * Hy - Ay * Hx; if (R != null) { if (R.length == 9) { R[0] = Hx; R[1] = Hy; R[2] = Hz; @@ -1228,17 +1232,17 @@ public abstract class SensorManager { // compute the inclination matrix by projecting the geomagnetic // vector onto the Z (gravity) and X (horizontal component // of geomagnetic vector) axes. - final float invE = 1.0f / (float)Math.sqrt(Ex*Ex + Ey*Ey + Ez*Ez); - final float c = (Ex*Mx + Ey*My + Ez*Mz) * invE; - final float s = (Ex*Ax + Ey*Ay + Ez*Az) * invE; + final float invE = 1.0f / (float) Math.sqrt(Ex * Ex + Ey * Ey + Ez * Ez); + final float c = (Ex * Mx + Ey * My + Ez * Mz) * invE; + final float s = (Ex * Ax + Ey * Ay + Ez * Az) * invE; if (I.length == 9) { I[0] = 1; I[1] = 0; I[2] = 0; I[3] = 0; I[4] = c; I[5] = s; - I[6] = 0; I[7] =-s; I[8] = c; + I[6] = 0; I[7] = -s; I[8] = c; } else if (I.length == 16) { I[0] = 1; I[1] = 0; I[2] = 0; I[4] = 0; I[5] = c; I[6] = s; - I[8] = 0; I[9] =-s; I[10]= c; + I[8] = 0; I[9] = -s; I[10] = c; I[3] = I[7] = I[11] = I[12] = I[13] = I[14] = 0; I[15] = 1; } @@ -1262,9 +1266,9 @@ public abstract class SensorManager { */ public static float getInclination(float[] I) { if (I.length == 9) { - return (float)Math.atan2(I[5], I[4]); + return (float) Math.atan2(I[5], I[4]); } else { - return (float)Math.atan2(I[6], I[5]); + return (float) Math.atan2(I[6], I[5]); } } @@ -1343,17 +1347,16 @@ public abstract class SensorManager { * @see #getRotationMatrix(float[], float[], float[], float[]) */ - public static boolean remapCoordinateSystem(float[] inR, int X, int Y, - float[] outR) - { + public static boolean remapCoordinateSystem(float[] inR, int X, int Y, float[] outR) { if (inR == outR) { - final float[] temp = mTempMatrix; - synchronized(temp) { + final float[] temp = sTempMatrix; + synchronized (temp) { // we don't expect to have a lot of contention if (remapCoordinateSystemImpl(inR, X, Y, temp)) { final int size = outR.length; - for (int i=0 ; i<size ; i++) + for (int i = 0; i < size; i++) { outR[i] = temp[i]; + } return true; } } @@ -1361,9 +1364,7 @@ public abstract class SensorManager { return remapCoordinateSystemImpl(inR, X, Y, outR); } - private static boolean remapCoordinateSystemImpl(float[] inR, int X, int Y, - float[] outR) - { + private static boolean remapCoordinateSystemImpl(float[] inR, int X, int Y, float[] outR) { /* * X and Y define a rotation matrix 'r': * @@ -1376,14 +1377,18 @@ public abstract class SensorManager { */ final int length = outR.length; - if (inR.length != length) + if (inR.length != length) { return false; // invalid parameter - if ((X & 0x7C)!=0 || (Y & 0x7C)!=0) + } + if ((X & 0x7C) != 0 || (Y & 0x7C) != 0) { return false; // invalid parameter - if (((X & 0x3)==0) || ((Y & 0x3)==0)) + } + if (((X & 0x3) == 0) || ((Y & 0x3) == 0)) { return false; // no axis specified - if ((X & 0x3) == (Y & 0x3)) + } + if ((X & 0x3) == (Y & 0x3)) { return false; // same axis specified + } // Z is "the other" axis, its sign is either +/- sign(X)*sign(Y) // this can be calculated by exclusive-or'ing X and Y; except for @@ -1391,28 +1396,29 @@ public abstract class SensorManager { int Z = X ^ Y; // extract the axis (remove the sign), offset in the range 0 to 2. - final int x = (X & 0x3)-1; - final int y = (Y & 0x3)-1; - final int z = (Z & 0x3)-1; + final int x = (X & 0x3) - 1; + final int y = (Y & 0x3) - 1; + final int z = (Z & 0x3) - 1; // compute the sign of Z (whether it needs to be inverted) - final int axis_y = (z+1)%3; - final int axis_z = (z+2)%3; - if (((x^axis_y)|(y^axis_z)) != 0) + final int axis_y = (z + 1) % 3; + final int axis_z = (z + 2) % 3; + if (((x ^ axis_y) | (y ^ axis_z)) != 0) { Z ^= 0x80; + } - final boolean sx = (X>=0x80); - final boolean sy = (Y>=0x80); - final boolean sz = (Z>=0x80); + final boolean sx = (X >= 0x80); + final boolean sy = (Y >= 0x80); + final boolean sz = (Z >= 0x80); // Perform R * r, in avoiding actual muls and adds. - final int rowLength = ((length==16)?4:3); - for (int j=0 ; j<3 ; j++) { - final int offset = j*rowLength; - for (int i=0 ; i<3 ; i++) { - if (x==i) outR[offset+i] = sx ? -inR[offset+0] : inR[offset+0]; - if (y==i) outR[offset+i] = sy ? -inR[offset+1] : inR[offset+1]; - if (z==i) outR[offset+i] = sz ? -inR[offset+2] : inR[offset+2]; + final int rowLength = ((length == 16) ? 4 : 3); + for (int j = 0; j < 3; j++) { + final int offset = j * rowLength; + for (int i = 0; i < 3; i++) { + if (x == i) outR[offset + i] = sx ? -inR[offset + 0] : inR[offset + 0]; + if (y == i) outR[offset + i] = sy ? -inR[offset + 1] : inR[offset + 1]; + if (z == i) outR[offset + i] = sz ? -inR[offset + 2] : inR[offset + 2]; } } if (length == 16) { @@ -1466,7 +1472,7 @@ public abstract class SensorManager { * @see #getRotationMatrix(float[], float[], float[], float[]) * @see GeomagneticField */ - public static float[] getOrientation(float[] R, float values[]) { + public static float[] getOrientation(float[] R, float[] values) { /* * 4x4 (length=16) case: * / R[ 0] R[ 1] R[ 2] 0 \ @@ -1481,13 +1487,13 @@ public abstract class SensorManager { * */ if (R.length == 9) { - values[0] = (float)Math.atan2(R[1], R[4]); - values[1] = (float)Math.asin(-R[7]); - values[2] = (float)Math.atan2(-R[6], R[8]); + values[0] = (float) Math.atan2(R[1], R[4]); + values[1] = (float) Math.asin(-R[7]); + values[2] = (float) Math.atan2(-R[6], R[8]); } else { - values[0] = (float)Math.atan2(R[1], R[5]); - values[1] = (float)Math.asin(-R[9]); - values[2] = (float)Math.atan2(-R[8], R[10]); + values[0] = (float) Math.atan2(R[1], R[5]); + values[1] = (float) Math.asin(-R[9]); + values[2] = (float) Math.atan2(-R[8], R[10]); } return values; @@ -1524,7 +1530,7 @@ public abstract class SensorManager { */ public static float getAltitude(float p0, float p) { final float coef = 1.0f / 5.255f; - return 44330.0f * (1.0f - (float)Math.pow(p/p0, coef)); + return 44330.0f * (1.0f - (float) Math.pow(p / p0, coef)); } /** Helper function to compute the angle change between two rotation matrices. @@ -1557,12 +1563,13 @@ public abstract class SensorManager { * (in radians) is stored */ - public static void getAngleChange( float[] angleChange, float[] R, float[] prevR) { - float rd1=0,rd4=0, rd6=0,rd7=0, rd8=0; - float ri0=0,ri1=0,ri2=0,ri3=0,ri4=0,ri5=0,ri6=0,ri7=0,ri8=0; - float pri0=0, pri1=0, pri2=0, pri3=0, pri4=0, pri5=0, pri6=0, pri7=0, pri8=0; + public static void getAngleChange(float[] angleChange, float[] R, float[] prevR) { + float rd1 = 0, rd4 = 0, rd6 = 0, rd7 = 0, rd8 = 0; + float ri0 = 0, ri1 = 0, ri2 = 0, ri3 = 0, ri4 = 0, ri5 = 0, ri6 = 0, ri7 = 0, ri8 = 0; + float pri0 = 0, pri1 = 0, pri2 = 0, pri3 = 0, pri4 = 0; + float pri5 = 0, pri6 = 0, pri7 = 0, pri8 = 0; - if(R.length == 9) { + if (R.length == 9) { ri0 = R[0]; ri1 = R[1]; ri2 = R[2]; @@ -1572,7 +1579,7 @@ public abstract class SensorManager { ri6 = R[6]; ri7 = R[7]; ri8 = R[8]; - } else if(R.length == 16) { + } else if (R.length == 16) { ri0 = R[0]; ri1 = R[1]; ri2 = R[2]; @@ -1584,7 +1591,7 @@ public abstract class SensorManager { ri8 = R[10]; } - if(prevR.length == 9) { + if (prevR.length == 9) { pri0 = prevR[0]; pri1 = prevR[1]; pri2 = prevR[2]; @@ -1594,7 +1601,7 @@ public abstract class SensorManager { pri6 = prevR[6]; pri7 = prevR[7]; pri8 = prevR[8]; - } else if(prevR.length == 16) { + } else if (prevR.length == 16) { pri0 = prevR[0]; pri1 = prevR[1]; pri2 = prevR[2]; @@ -1615,9 +1622,9 @@ public abstract class SensorManager { rd7 = pri2 * ri1 + pri5 * ri4 + pri8 * ri7; //rd[2][1] rd8 = pri2 * ri2 + pri5 * ri5 + pri8 * ri8; //rd[2][2] - angleChange[0] = (float)Math.atan2(rd1, rd4); - angleChange[1] = (float)Math.asin(-rd7); - angleChange[2] = (float)Math.atan2(-rd6, rd8); + angleChange[0] = (float) Math.atan2(rd1, rd4); + angleChange[1] = (float) Math.asin(-rd7); + angleChange[2] = (float) Math.atan2(-rd6, rd8); } @@ -1650,8 +1657,8 @@ public abstract class SensorManager { if (rotationVector.length >= 4) { q0 = rotationVector[3]; } else { - q0 = 1 - q1*q1 - q2*q2 - q3*q3; - q0 = (q0 > 0) ? (float)Math.sqrt(q0) : 0; + q0 = 1 - q1 * q1 - q2 * q2 - q3 * q3; + q0 = (q0 > 0) ? (float) Math.sqrt(q0) : 0; } float sq_q1 = 2 * q1 * q1; @@ -1664,7 +1671,7 @@ public abstract class SensorManager { float q2_q3 = 2 * q2 * q3; float q1_q0 = 2 * q1 * q0; - if(R.length == 9) { + if (R.length == 9) { R[0] = 1 - sq_q2 - sq_q3; R[1] = q1_q2 - q3_q0; R[2] = q1_q3 + q2_q0; @@ -1707,8 +1714,8 @@ public abstract class SensorManager { if (rv.length >= 4) { Q[0] = rv[3]; } else { - Q[0] = 1 - rv[0]*rv[0] - rv[1]*rv[1] - rv[2]*rv[2]; - Q[0] = (Q[0] > 0) ? (float)Math.sqrt(Q[0]) : 0; + Q[0] = 1 - rv[0] * rv[0] - rv[1] * rv[1] - rv[2] * rv[2]; + Q[0] = (Q[0] > 0) ? (float) Math.sqrt(Q[0]) : 0; } Q[1] = rv[0]; Q[2] = rv[1]; @@ -1800,7 +1807,7 @@ public abstract class SensorManager { */ @SystemApi public boolean initDataInjection(boolean enable) { - return initDataInjectionImpl(enable); + return initDataInjectionImpl(enable); } /** @@ -1846,9 +1853,9 @@ public abstract class SensorManager { } int expectedNumValues = Sensor.getMaxLengthValuesArray(sensor, Build.VERSION_CODES.M); if (values.length != expectedNumValues) { - throw new IllegalArgumentException ("Wrong number of values for sensor " + - sensor.getName() + " actual=" + values.length + " expected=" + - expectedNumValues); + throw new IllegalArgumentException("Wrong number of values for sensor " + + sensor.getName() + " actual=" + values.length + " expected=" + + expectedNumValues); } if (accuracy < SENSOR_STATUS_NO_CONTACT || accuracy > SENSOR_STATUS_ACCURACY_HIGH) { throw new IllegalArgumentException("Invalid sensor accuracy"); diff --git a/android/hardware/SystemSensorManager.java b/android/hardware/SystemSensorManager.java index 607788d3..1174cb6a 100644 --- a/android/hardware/SystemSensorManager.java +++ b/android/hardware/SystemSensorManager.java @@ -28,10 +28,11 @@ import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; -import dalvik.system.CloseGuard; import com.android.internal.annotations.GuardedBy; +import dalvik.system.CloseGuard; + import java.io.IOException; import java.io.UncheckedIOException; import java.lang.ref.WeakReference; @@ -40,7 +41,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - /** * Sensor manager implementation that communicates with the built-in * system sensors. @@ -101,7 +101,7 @@ public class SystemSensorManager extends SensorManager { /** {@hide} */ public SystemSensorManager(Context context, Looper mainLooper) { - synchronized(sLock) { + synchronized (sLock) { if (!sNativeClassInited) { sNativeClassInited = true; nativeClassInit(); @@ -114,7 +114,7 @@ public class SystemSensorManager extends SensorManager { mNativeInstance = nativeCreate(context.getOpPackageName()); // initialize the sensor list - for (int index = 0;;++index) { + for (int index = 0;; ++index) { Sensor sensor = new Sensor(); if (!nativeGetSensorAtIndex(mNativeInstance, sensor, index)) break; mFullSensorsList.add(sensor); @@ -157,9 +157,9 @@ public class SystemSensorManager extends SensorManager { return false; } if (mSensorListeners.size() >= MAX_LISTENER_COUNT) { - throw new IllegalStateException("register failed, " + - "the sensor listeners size has exceeded the maximum limit " + - MAX_LISTENER_COUNT); + throw new IllegalStateException("register failed, " + + "the sensor listeners size has exceeded the maximum limit " + + MAX_LISTENER_COUNT); } // Invariants to preserve: @@ -170,9 +170,10 @@ public class SystemSensorManager extends SensorManager { SensorEventQueue queue = mSensorListeners.get(listener); if (queue == null) { Looper looper = (handler != null) ? handler.getLooper() : mMainLooper; - final String fullClassName = listener.getClass().getEnclosingClass() != null ? - listener.getClass().getEnclosingClass().getName() : - listener.getClass().getName(); + final String fullClassName = + listener.getClass().getEnclosingClass() != null + ? listener.getClass().getEnclosingClass().getName() + : listener.getClass().getName(); queue = new SensorEventQueue(listener, looper, this, fullClassName); if (!queue.addSensor(sensor, delayUs, maxBatchReportLatencyUs)) { queue.dispose(); @@ -221,17 +222,18 @@ public class SystemSensorManager extends SensorManager { if (sensor.getReportingMode() != Sensor.REPORTING_MODE_ONE_SHOT) return false; if (mTriggerListeners.size() >= MAX_LISTENER_COUNT) { - throw new IllegalStateException("request failed, " + - "the trigger listeners size has exceeded the maximum limit " + - MAX_LISTENER_COUNT); + throw new IllegalStateException("request failed, " + + "the trigger listeners size has exceeded the maximum limit " + + MAX_LISTENER_COUNT); } synchronized (mTriggerListeners) { TriggerEventQueue queue = mTriggerListeners.get(listener); if (queue == null) { - final String fullClassName = listener.getClass().getEnclosingClass() != null ? - listener.getClass().getEnclosingClass().getName() : - listener.getClass().getName(); + final String fullClassName = + listener.getClass().getEnclosingClass() != null + ? listener.getClass().getEnclosingClass().getName() + : listener.getClass().getName(); queue = new TriggerEventQueue(listener, mMainLooper, this, fullClassName); if (!queue.addSensor(sensor, 0, 0)) { queue.dispose(); @@ -336,27 +338,27 @@ public class SystemSensorManager extends SensorManager { mHandleToSensor.remove(sensor.getHandle()); if (sensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) { - synchronized(mTriggerListeners) { + synchronized (mTriggerListeners) { HashMap<TriggerEventListener, TriggerEventQueue> triggerListeners = - new HashMap<TriggerEventListener, TriggerEventQueue>(mTriggerListeners); + new HashMap<TriggerEventListener, TriggerEventQueue>(mTriggerListeners); - for (TriggerEventListener l: triggerListeners.keySet()) { - if (DEBUG_DYNAMIC_SENSOR){ - Log.i(TAG, "removed trigger listener" + l.toString() + - " due to sensor disconnection"); + for (TriggerEventListener l : triggerListeners.keySet()) { + if (DEBUG_DYNAMIC_SENSOR) { + Log.i(TAG, "removed trigger listener" + l.toString() + + " due to sensor disconnection"); } cancelTriggerSensorImpl(l, sensor, true); } } } else { - synchronized(mSensorListeners) { + synchronized (mSensorListeners) { HashMap<SensorEventListener, SensorEventQueue> sensorListeners = - new HashMap<SensorEventListener, SensorEventQueue>(mSensorListeners); + new HashMap<SensorEventListener, SensorEventQueue>(mSensorListeners); for (SensorEventListener l: sensorListeners.keySet()) { - if (DEBUG_DYNAMIC_SENSOR){ - Log.i(TAG, "removed event listener" + l.toString() + - " due to sensor disconnection"); + if (DEBUG_DYNAMIC_SENSOR) { + Log.i(TAG, "removed event listener" + l.toString() + + " due to sensor disconnection"); } unregisterListenerImpl(l, sensor); } @@ -365,7 +367,7 @@ public class SystemSensorManager extends SensorManager { } private void updateDynamicSensorList() { - synchronized(mFullDynamicSensorsList) { + synchronized (mFullDynamicSensorsList) { if (mDynamicSensorListDirty) { List<Sensor> list = new ArrayList<>(); nativeGetDynamicSensors(mNativeInstance, list); @@ -488,15 +490,15 @@ public class SystemSensorManager extends SensorManager { int i = 0, j = 0; while (true) { - if (j < oldList.size() && ( i >= newList.size() || - newList.get(i).getHandle() > oldList.get(j).getHandle()) ) { + if (j < oldList.size() && (i >= newList.size() + || newList.get(i).getHandle() > oldList.get(j).getHandle())) { changed = true; if (removed != null) { removed.add(oldList.get(j)); } ++j; - } else if (i < newList.size() && ( j >= oldList.size() || - newList.get(i).getHandle() < oldList.get(j).getHandle())) { + } else if (i < newList.size() && (j >= oldList.size() + || newList.get(i).getHandle() < oldList.get(j).getHandle())) { changed = true; if (added != null) { added.add(newList.get(i)); @@ -505,8 +507,8 @@ public class SystemSensorManager extends SensorManager { updated.add(newList.get(i)); } ++i; - } else if (i < newList.size() && j < oldList.size() && - newList.get(i).getHandle() == oldList.get(j).getHandle()) { + } else if (i < newList.size() && j < oldList.size() + && newList.get(i).getHandle() == oldList.get(j).getHandle()) { if (updated != null) { updated.add(oldList.get(j)); } @@ -623,7 +625,7 @@ public class SystemSensorManager extends SensorManager { * associated with any listener and there is one InjectEventQueue associated with a * SensorManager instance. */ - private static abstract class BaseEventQueue { + private abstract static class BaseEventQueue { private static native long nativeInitBaseEventQueue(long nativeManager, WeakReference<BaseEventQueue> eventQWeak, MessageQueue msgQ, String packageName, int mode, String opPackageName); @@ -633,9 +635,9 @@ public class SystemSensorManager extends SensorManager { private static native void nativeDestroySensorEventQueue(long eventQ); private static native int nativeFlushSensor(long eventQ); private static native int nativeInjectSensorData(long eventQ, int handle, - float[] values,int accuracy, long timestamp); + float[] values, int accuracy, long timestamp); - private long nSensorEventQueue; + private long mNativeSensorEventQueue; private final SparseBooleanArray mActiveSensors = new SparseBooleanArray(); protected final SparseIntArray mSensorAccuracies = new SparseIntArray(); private final CloseGuard mCloseGuard = CloseGuard.get(); @@ -646,7 +648,7 @@ public class SystemSensorManager extends SensorManager { BaseEventQueue(Looper looper, SystemSensorManager manager, int mode, String packageName) { if (packageName == null) packageName = ""; - nSensorEventQueue = nativeInitBaseEventQueue(manager.mNativeInstance, + mNativeSensorEventQueue = nativeInitBaseEventQueue(manager.mNativeInstance, new WeakReference<>(this), looper.getQueue(), packageName, mode, manager.mContext.getOpPackageName()); mCloseGuard.open("dispose"); @@ -668,17 +670,17 @@ public class SystemSensorManager extends SensorManager { addSensorEvent(sensor); if (enableSensor(sensor, delayUs, maxBatchReportLatencyUs) != 0) { // Try continuous mode if batching fails. - if (maxBatchReportLatencyUs == 0 || - maxBatchReportLatencyUs > 0 && enableSensor(sensor, delayUs, 0) != 0) { - removeSensor(sensor, false); - return false; + if (maxBatchReportLatencyUs == 0 + || maxBatchReportLatencyUs > 0 && enableSensor(sensor, delayUs, 0) != 0) { + removeSensor(sensor, false); + return false; } } return true; } public boolean removeAllSensors() { - for (int i=0 ; i<mActiveSensors.size(); i++) { + for (int i = 0; i < mActiveSensors.size(); i++) { if (mActiveSensors.valueAt(i) == true) { int handle = mActiveSensors.keyAt(i); Sensor sensor = mManager.mHandleToSensor.get(handle); @@ -706,8 +708,8 @@ public class SystemSensorManager extends SensorManager { } public int flush() { - if (nSensorEventQueue == 0) throw new NullPointerException(); - return nativeFlushSensor(nSensorEventQueue); + if (mNativeSensorEventQueue == 0) throw new NullPointerException(); + return nativeFlushSensor(mNativeSensorEventQueue); } public boolean hasSensors() { @@ -731,29 +733,30 @@ public class SystemSensorManager extends SensorManager { } mCloseGuard.close(); } - if (nSensorEventQueue != 0) { - nativeDestroySensorEventQueue(nSensorEventQueue); - nSensorEventQueue = 0; + if (mNativeSensorEventQueue != 0) { + nativeDestroySensorEventQueue(mNativeSensorEventQueue); + mNativeSensorEventQueue = 0; } } private int enableSensor( Sensor sensor, int rateUs, int maxBatchReportLatencyUs) { - if (nSensorEventQueue == 0) throw new NullPointerException(); + if (mNativeSensorEventQueue == 0) throw new NullPointerException(); if (sensor == null) throw new NullPointerException(); - return nativeEnableSensor(nSensorEventQueue, sensor.getHandle(), rateUs, + return nativeEnableSensor(mNativeSensorEventQueue, sensor.getHandle(), rateUs, maxBatchReportLatencyUs); } protected int injectSensorDataBase(int handle, float[] values, int accuracy, long timestamp) { - return nativeInjectSensorData(nSensorEventQueue, handle, values, accuracy, timestamp); + return nativeInjectSensorData( + mNativeSensorEventQueue, handle, values, accuracy, timestamp); } private int disableSensor(Sensor sensor) { - if (nSensorEventQueue == 0) throw new NullPointerException(); + if (mNativeSensorEventQueue == 0) throw new NullPointerException(); if (sensor == null) throw new NullPointerException(); - return nativeDisableSensor(nSensorEventQueue, sensor.getHandle()); + return nativeDisableSensor(mNativeSensorEventQueue, sensor.getHandle()); } protected abstract void dispatchSensorEvent(int handle, float[] values, int accuracy, long timestamp); @@ -840,7 +843,7 @@ public class SystemSensorManager extends SensorManager { // sensor disconnected return; } - ((SensorEventListener2)mListener).onFlushCompleted(sensor); + ((SensorEventListener2) mListener).onFlushCompleted(sensor); } return; } @@ -858,7 +861,7 @@ public class SystemSensorManager extends SensorManager { } SensorAdditionalInfo info = new SensorAdditionalInfo(sensor, type, serial, intValues, floatValues); - ((SensorEventCallback)mListener).onSensorAdditionalInfo(info); + ((SensorEventCallback) mListener).onSensorAdditionalInfo(info); } } } @@ -930,8 +933,8 @@ public class SystemSensorManager extends SensorManager { super(looper, manager, OPERATING_MODE_DATA_INJECTION, packageName); } - int injectSensorData(int handle, float[] values,int accuracy, long timestamp) { - return injectSensorDataBase(handle, values, accuracy, timestamp); + int injectSensorData(int handle, float[] values, int accuracy, long timestamp) { + return injectSensorDataBase(handle, values, accuracy, timestamp); } @SuppressWarnings("unused") @@ -959,6 +962,7 @@ public class SystemSensorManager extends SensorManager { int handle = -1; if (parameter.sensor != null) handle = parameter.sensor.getHandle(); return nativeSetOperationParameter( - mNativeInstance, handle, parameter.type, parameter.floatValues, parameter.intValues) == 0; + mNativeInstance, handle, + parameter.type, parameter.floatValues, parameter.intValues) == 0; } } diff --git a/android/hardware/camera2/DngCreator.java b/android/hardware/camera2/DngCreator.java index 1a51acd6..cc484eaf 100644 --- a/android/hardware/camera2/DngCreator.java +++ b/android/hardware/camera2/DngCreator.java @@ -18,7 +18,6 @@ package android.hardware.camera2; import android.annotation.IntRange; import android.annotation.NonNull; -import android.annotation.Nullable; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.ImageFormat; @@ -37,6 +36,7 @@ import java.nio.ByteBuffer; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.Locale; import java.util.TimeZone; /** @@ -122,7 +122,7 @@ public final class DngCreator implements AutoCloseable { // Create this fresh each time since the time zone may change while a long-running application // is active. final DateFormat dateTimeStampFormat = - new SimpleDateFormat(TIFF_DATETIME_FORMAT); + new SimpleDateFormat(TIFF_DATETIME_FORMAT, Locale.US); dateTimeStampFormat.setTimeZone(TimeZone.getDefault()); // Format for metadata @@ -472,7 +472,8 @@ public final class DngCreator implements AutoCloseable { private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd"; private static final String TIFF_DATETIME_FORMAT = "yyyy:MM:dd HH:mm:ss"; - private static final DateFormat sExifGPSDateStamp = new SimpleDateFormat(GPS_DATE_FORMAT_STR); + private static final DateFormat sExifGPSDateStamp = + new SimpleDateFormat(GPS_DATE_FORMAT_STR, Locale.US); private final Calendar mGPSTimeStampCalendar = Calendar .getInstance(TimeZone.getTimeZone("UTC")); diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java index 6fbacaf3..b2af44ec 100644 --- a/android/hardware/display/DisplayManager.java +++ b/android/hardware/display/DisplayManager.java @@ -278,6 +278,19 @@ public final class DisplayManager { */ public static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; + /** + * Virtual display flag: Indicates that the contents will be destroyed once + * the display is removed. + * + * Public virtual displays without this flag will move their content to main display + * stack once they're removed. Private vistual displays will always destroy their + * content on removal even without this flag. + * + * @see #createVirtualDisplay + * @hide + */ + public static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; + /** @hide */ public DisplayManager(Context context) { mContext = context; diff --git a/android/hardware/location/NanoAppInstanceInfo.java b/android/hardware/location/NanoAppInstanceInfo.java index ac6d83f6..26238304 100644 --- a/android/hardware/location/NanoAppInstanceInfo.java +++ b/android/hardware/location/NanoAppInstanceInfo.java @@ -287,8 +287,10 @@ public class NanoAppInstanceInfo { mPublisher = in.readString(); mName = in.readString(); + mHandle = in.readInt(); mAppId = in.readLong(); mAppVersion = in.readInt(); + mContexthubId = in.readInt(); mNeededReadMemBytes = in.readInt(); mNeededWriteMemBytes = in.readInt(); mNeededExecMemBytes = in.readInt(); @@ -309,6 +311,8 @@ public class NanoAppInstanceInfo { public void writeToParcel(Parcel out, int flags) { out.writeString(mPublisher); out.writeString(mName); + + out.writeInt(mHandle); out.writeLong(mAppId); out.writeInt(mAppVersion); out.writeInt(mContexthubId); diff --git a/android/media/AmrInputStream.java b/android/media/AmrInputStream.java index fb91bbbb..efaf2244 100644 --- a/android/media/AmrInputStream.java +++ b/android/media/AmrInputStream.java @@ -25,12 +25,12 @@ import android.util.Log; /** - * AmrInputStream + * DO NOT USE * @hide */ public final class AmrInputStream extends InputStream { private final static String TAG = "AmrInputStream"; - + // frame is 20 msec at 8.000 khz private final static int SAMPLES_PER_FRAME = 8000 * 20 / 1000; @@ -51,10 +51,10 @@ public final class AmrInputStream extends InputStream { private byte[] mOneByte = new byte[1]; /** - * Create a new AmrInputStream, which converts 16 bit PCM to AMR - * @param inputStream InputStream containing 16 bit PCM. + * DO NOT USE - use MediaCodec instead */ public AmrInputStream(InputStream inputStream) { + Log.w(TAG, "@@@@ AmrInputStream is not a public API @@@@"); mInputStream = inputStream; MediaFormat format = new MediaFormat(); @@ -83,17 +83,26 @@ public final class AmrInputStream extends InputStream { mInfo = new BufferInfo(); } + /** + * DO NOT USE + */ @Override public int read() throws IOException { int rtn = read(mOneByte, 0, 1); return rtn == 1 ? (0xff & mOneByte[0]) : -1; } + /** + * DO NOT USE + */ @Override public int read(byte[] b) throws IOException { return read(b, 0, b.length); } + /** + * DO NOT USE + */ @Override public int read(byte[] b, int offset, int length) throws IOException { if (mCodec == null) { @@ -131,19 +140,15 @@ public final class AmrInputStream extends InputStream { } } - // now read encoded data from the encoder (blocking, since we just filled up the - // encoder's input with data it should be able to output at least one buffer) - while (true) { - int index = mCodec.dequeueOutputBuffer(mInfo, -1); - if (index >= 0) { - mBufIn = mInfo.size; - ByteBuffer out = mCodec.getOutputBuffer(index); - out.get(mBuf, 0 /* offset */, mBufIn /* length */); - mCodec.releaseOutputBuffer(index, false /* render */); - if ((mInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { - mSawOutputEOS = true; - } - break; + // now read encoded data from the encoder + int index = mCodec.dequeueOutputBuffer(mInfo, 0); + if (index >= 0) { + mBufIn = mInfo.size; + ByteBuffer out = mCodec.getOutputBuffer(index); + out.get(mBuf, 0 /* offset */, mBufIn /* length */); + mCodec.releaseOutputBuffer(index, false /* render */); + if ((mInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + mSawOutputEOS = true; } } } diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java index 3b9a5de0..26ead3d1 100644 --- a/android/media/AudioAttributes.java +++ b/android/media/AudioAttributes.java @@ -19,12 +19,14 @@ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.media.AudioAttributesProto; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; +import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -177,7 +179,7 @@ public final class AudioAttributes implements Parcelable { /** * IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES - * if applicable. + * if applicable, as well as audioattributes.proto. */ /** @@ -850,6 +852,21 @@ public final class AudioAttributes implements Parcelable { } /** @hide */ + public void toProto(ProtoOutputStream proto) { + proto.write(AudioAttributesProto.USAGE, mUsage); + proto.write(AudioAttributesProto.CONTENT_TYPE, mContentType); + proto.write(AudioAttributesProto.FLAGS, mFlags); + // mFormattedTags is never null due to assignment in Builder or unmarshalling. + for (String t : mFormattedTags.split(";")) { + t = t.trim(); + if (t != "") { + proto.write(AudioAttributesProto.TAGS, t); + } + } + // TODO: is the data in mBundle useful for debugging? + } + + /** @hide */ public String usageToString() { return usageToString(mUsage); } diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java index 186b2650..dab7632a 100644 --- a/android/media/AudioManager.java +++ b/android/media/AudioManager.java @@ -4119,7 +4119,15 @@ public class AudioManager { Log.w(TAG, "updateAudioPortCache: listAudioPatches failed"); return status; } - } while (patchGeneration[0] != portGeneration[0]); + // Loop until patch generation is the same as port generation unless audio ports + // and audio patches are not null. + } while (patchGeneration[0] != portGeneration[0] + && (ports == null || patches == null)); + // If the patch generation doesn't equal port generation, return ERROR here in case + // of mismatch between audio ports and audio patches. + if (patchGeneration[0] != portGeneration[0]) { + return ERROR; + } for (int i = 0; i < newPatches.size(); i++) { for (int j = 0; j < newPatches.get(i).sources().length; j++) { diff --git a/android/media/AudioPortEventHandler.java b/android/media/AudioPortEventHandler.java index c152245d..ac3904a2 100644 --- a/android/media/AudioPortEventHandler.java +++ b/android/media/AudioPortEventHandler.java @@ -17,6 +17,7 @@ package android.media; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import java.util.ArrayList; @@ -30,6 +31,7 @@ import java.lang.ref.WeakReference; class AudioPortEventHandler { private Handler mHandler; + private HandlerThread mHandlerThread; private final ArrayList<AudioManager.OnAudioPortUpdateListener> mListeners = new ArrayList<AudioManager.OnAudioPortUpdateListener>(); @@ -40,6 +42,8 @@ class AudioPortEventHandler { private static final int AUDIOPORT_EVENT_SERVICE_DIED = 3; private static final int AUDIOPORT_EVENT_NEW_LISTENER = 4; + private static final long RESCHEDULE_MESSAGE_DELAY_MS = 100; + /** * Accessed by native methods: JNI Callback context. */ @@ -51,11 +55,12 @@ class AudioPortEventHandler { if (mHandler != null) { return; } - // find the looper for our new event handler - Looper looper = Looper.getMainLooper(); + // create a new thread for our new event handler + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); - if (looper != null) { - mHandler = new Handler(looper) { + if (mHandlerThread.getLooper() != null) { + mHandler = new Handler(mHandlerThread.getLooper()) { @Override public void handleMessage(Message msg) { ArrayList<AudioManager.OnAudioPortUpdateListener> listeners; @@ -86,6 +91,12 @@ class AudioPortEventHandler { if (msg.what != AUDIOPORT_EVENT_SERVICE_DIED) { int status = AudioManager.updateAudioPortCache(ports, patches, null); if (status != AudioManager.SUCCESS) { + // Since audio ports and audio patches are not null, the return + // value could be ERROR due to inconsistency between port generation + // and patch generation. In this case, we need to reschedule the + // message to make sure the native callback is done. + sendMessageDelayed(obtainMessage(msg.what, msg.obj), + RESCHEDULE_MESSAGE_DELAY_MS); return; } } @@ -132,6 +143,9 @@ class AudioPortEventHandler { @Override protected void finalize() { native_finalize(); + if (mHandlerThread.isAlive()) { + mHandlerThread.quit(); + } } private native void native_finalize(); @@ -168,6 +182,10 @@ class AudioPortEventHandler { Handler handler = eventHandler.handler(); if (handler != null) { Message m = handler.obtainMessage(what, arg1, arg2, obj); + if (what != AUDIOPORT_EVENT_NEW_LISTENER) { + // Except AUDIOPORT_EVENT_NEW_LISTENER, we can only respect the last message. + handler.removeMessages(what); + } handler.sendMessage(m); } } diff --git a/android/media/ExifInterface.java b/android/media/ExifInterface.java index 1f5edfa0..ba41a7bd 100644 --- a/android/media/ExifInterface.java +++ b/android/media/ExifInterface.java @@ -2584,22 +2584,21 @@ public class ExifInterface { ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder)); } - // Note that the rotation angle from MediaMetadataRetriever for heif images - // are CCW, while rotation in ExifInterface orientations are CW. String rotation = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); if (rotation != null) { int orientation = ExifInterface.ORIENTATION_NORMAL; + // all rotation angles in CW switch (Integer.parseInt(rotation)) { case 90: - orientation = ExifInterface.ORIENTATION_ROTATE_270; + orientation = ExifInterface.ORIENTATION_ROTATE_90; break; case 180: orientation = ExifInterface.ORIENTATION_ROTATE_180; break; case 270: - orientation = ExifInterface.ORIENTATION_ROTATE_90; + orientation = ExifInterface.ORIENTATION_ROTATE_270; break; } diff --git a/android/media/MediaCodecInfo.java b/android/media/MediaCodecInfo.java index f85925d8..f41e33f7 100644 --- a/android/media/MediaCodecInfo.java +++ b/android/media/MediaCodecInfo.java @@ -2749,8 +2749,8 @@ public final class MediaCodecInfo { mQualityRange = Utils .parseIntRange(info.getString("quality-range"), mQualityRange); } - if (info.containsKey("feature-bitrate-control")) { - for (String mode: info.getString("feature-bitrate-control").split(",")) { + if (info.containsKey("feature-bitrate-modes")) { + for (String mode: info.getString("feature-bitrate-modes").split(",")) { mBitControl |= parseBitrateMode(mode); } } diff --git a/android/media/MediaRouter.java b/android/media/MediaRouter.java index fe427a73..70ab8632 100644 --- a/android/media/MediaRouter.java +++ b/android/media/MediaRouter.java @@ -184,13 +184,15 @@ public class MediaRouter { void updateAudioRoutes(AudioRoutesInfo newRoutes) { boolean audioRoutesChanged = false; + boolean forceUseDefaultRoute = false; + if (newRoutes.mainType != mCurAudioRoutesInfo.mainType) { mCurAudioRoutesInfo.mainType = newRoutes.mainType; int name; - if ((newRoutes.mainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0 - || (newRoutes.mainType&AudioRoutesInfo.MAIN_HEADSET) != 0) { + if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0 + || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { name = com.android.internal.R.string.default_audio_route_name_headphones; - } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { name = com.android.internal.R.string.default_audio_route_name_dock_speakers; } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_HDMI) != 0) { name = com.android.internal.R.string.default_audio_route_name_hdmi; @@ -201,11 +203,16 @@ public class MediaRouter { } mDefaultAudioVideo.mNameResId = name; dispatchRouteChanged(mDefaultAudioVideo); + + if ((newRoutes.mainType & (AudioRoutesInfo.MAIN_HEADSET + | AudioRoutesInfo.MAIN_HEADPHONES | AudioRoutesInfo.MAIN_USB)) != 0) { + forceUseDefaultRoute = true; + } audioRoutesChanged = true; } - final int mainType = mCurAudioRoutesInfo.mainType; if (!TextUtils.equals(newRoutes.bluetoothName, mCurAudioRoutesInfo.bluetoothName)) { + forceUseDefaultRoute = false; mCurAudioRoutesInfo.bluetoothName = newRoutes.bluetoothName; if (mCurAudioRoutesInfo.bluetoothName != null) { if (mBluetoothA2dpRoute == null) { @@ -231,30 +238,21 @@ public class MediaRouter { } if (audioRoutesChanged) { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, getDefaultSystemAudioRoute(), false); Log.v(TAG, "Audio routes updated: " + newRoutes + ", a2dp=" + isBluetoothA2dpOn()); + if (mSelectedRoute == null || mSelectedRoute == mDefaultAudioVideo + || mSelectedRoute == mBluetoothA2dpRoute) { + if (forceUseDefaultRoute || mBluetoothA2dpRoute == null) { + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo, false); + } else { + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute, false); + } + } } } - RouteInfo getDefaultSystemAudioRoute() { - boolean globalBluetoothA2doOn = false; - try { - globalBluetoothA2doOn = mMediaRouterService.isGlobalBluetoothA2doOn(); - } catch (RemoteException ex) { - Log.e(TAG, "Unable to call isSystemBluetoothA2doOn.", ex); - } - return (globalBluetoothA2doOn && mBluetoothA2dpRoute != null) - ? mBluetoothA2dpRoute : mDefaultAudioVideo; - } - - RouteInfo getCurrentSystemAudioRoute() { - return (isBluetoothA2dpOn() && mBluetoothA2dpRoute != null) - ? mBluetoothA2dpRoute : mDefaultAudioVideo; - } - boolean isBluetoothA2dpOn() { try { - return mAudioService.isBluetoothA2dpOn(); + return mBluetoothA2dpRoute != null && mAudioService.isBluetoothA2dpOn(); } catch (RemoteException e) { Log.e(TAG, "Error querying Bluetooth A2DP state", e); return false; @@ -602,13 +600,20 @@ public class MediaRouter { @Override public void onRestoreRoute() { - // Skip restoring route if the selected route is not a system audio route, or - // MediaRouter is initializing. - if ((mSelectedRoute != mDefaultAudioVideo && mSelectedRoute != mBluetoothA2dpRoute) - || mSelectedRoute == null) { - return; - } - mSelectedRoute.select(); + mHandler.post(new Runnable() { + @Override + public void run() { + // Skip restoring route if the selected route is not a system audio route, + // MediaRouter is initializing, or mClient was changed. + if (Client.this != mClient || mSelectedRoute == null + || (mSelectedRoute != mDefaultAudioVideo + && mSelectedRoute != mBluetoothA2dpRoute)) { + return; + } + Log.v(TAG, "onRestoreRoute() : route=" + mSelectedRoute); + mSelectedRoute.select(); + } + }); } } } @@ -940,10 +945,12 @@ public class MediaRouter { Log.v(TAG, "Selecting route: " + route); assert(route != null); final RouteInfo oldRoute = sStatic.mSelectedRoute; + final RouteInfo currentSystemRoute = sStatic.isBluetoothA2dpOn() + ? sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo; boolean wasDefaultOrBluetoothRoute = (oldRoute == sStatic.mDefaultAudioVideo || oldRoute == sStatic.mBluetoothA2dpRoute); if (oldRoute == route - && (!wasDefaultOrBluetoothRoute || route == sStatic.getCurrentSystemAudioRoute())) { + && (!wasDefaultOrBluetoothRoute || route == currentSystemRoute)) { return; } if (!route.matchesTypes(types)) { @@ -1014,8 +1021,7 @@ public class MediaRouter { static void selectDefaultRouteStatic() { // TODO: Be smarter about the route types here; this selects for all valid. - if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute - && sStatic.mBluetoothA2dpRoute != null && sStatic.isBluetoothA2dpOn()) { + if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute && sStatic.isBluetoothA2dpOn()) { selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mBluetoothA2dpRoute, false); } else { selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mDefaultAudioVideo, false); diff --git a/android/media/PlayerBase.java b/android/media/PlayerBase.java index 4808d7a5..09449a18 100644 --- a/android/media/PlayerBase.java +++ b/android/media/PlayerBase.java @@ -127,8 +127,9 @@ public abstract class PlayerBase { Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e); } synchronized (mLock) { + boolean attributesChanged = (mAttributes != attr); mAttributes = attr; - updateAppOpsPlayAudio_sync(); + updateAppOpsPlayAudio_sync(attributesChanged); } } @@ -200,16 +201,13 @@ public abstract class PlayerBase { } void baseSetVolume(float leftVolume, float rightVolume) { - final boolean hasAppOpsPlayAudio; + final boolean isRestricted; synchronized (mLock) { mLeftVolume = leftVolume; mRightVolume = rightVolume; - hasAppOpsPlayAudio = mHasAppOpsPlayAudio; - if (isRestricted_sync()) { - return; - } + isRestricted = isRestricted_sync(); } - playerSetVolume(!hasAppOpsPlayAudio/*muting*/, + playerSetVolume(isRestricted/*muting*/, leftVolume * mPanMultiplierL, rightVolume * mPanMultiplierR); } @@ -250,7 +248,7 @@ public abstract class PlayerBase { private void updateAppOpsPlayAudio() { synchronized (mLock) { - updateAppOpsPlayAudio_sync(); + updateAppOpsPlayAudio_sync(false); } } @@ -258,7 +256,7 @@ public abstract class PlayerBase { * To be called whenever a condition that might affect audibility of this player is updated. * Must be called synchronized on mLock. */ - void updateAppOpsPlayAudio_sync() { + void updateAppOpsPlayAudio_sync(boolean attributesChanged) { boolean oldHasAppOpsPlayAudio = mHasAppOpsPlayAudio; try { int mode = AppOpsManager.MODE_IGNORED; @@ -275,9 +273,10 @@ public abstract class PlayerBase { // AppsOps alters a player's volume; when the restriction changes, reflect it on the actual // volume used by the player try { - if (oldHasAppOpsPlayAudio != mHasAppOpsPlayAudio) { + if (oldHasAppOpsPlayAudio != mHasAppOpsPlayAudio || + attributesChanged) { getService().playerHasOpPlayAudio(mPlayerIId, mHasAppOpsPlayAudio); - if (mHasAppOpsPlayAudio) { + if (!isRestricted_sync()) { if (DEBUG_APP_OPS) { Log.v(TAG, "updateAppOpsPlayAudio: unmuting player, vol=" + mLeftVolume + "/" + mRightVolume); diff --git a/android/mtp/MtpDatabase.java b/android/mtp/MtpDatabase.java index 80fd5c03..17b23264 100644 --- a/android/mtp/MtpDatabase.java +++ b/android/mtp/MtpDatabase.java @@ -845,6 +845,33 @@ public class MtpDatabase implements AutoCloseable { return MtpConstants.RESPONSE_OK; } + private int moveObject(int handle, int newParent, String newPath) { + String[] whereArgs = new String[] { Integer.toString(handle) }; + + // do not allow renaming any of the special subdirectories + if (isStorageSubDirectory(newPath)) { + return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; + } + + // update database + ContentValues values = new ContentValues(); + values.put(Files.FileColumns.DATA, newPath); + values.put(Files.FileColumns.PARENT, newParent); + int updated = 0; + try { + // note - we are relying on a special case in MediaProvider.update() to update + // the paths for all children in the case where this is a directory. + updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in mMediaProvider.update", e); + } + if (updated == 0) { + Log.e(TAG, "Unable to update path for " + handle + " to " + newPath); + return MtpConstants.RESPONSE_GENERAL_ERROR; + } + return MtpConstants.RESPONSE_OK; + } + private int setObjectProperty(int handle, int property, long intValue, String stringValue) { switch (property) { diff --git a/android/multiuser/BenchmarkRunner.java b/android/multiuser/BenchmarkRunner.java index c7bebf38..629e6f45 100644 --- a/android/multiuser/BenchmarkRunner.java +++ b/android/multiuser/BenchmarkRunner.java @@ -17,13 +17,16 @@ package android.multiuser; import android.os.Bundle; import android.os.SystemClock; +import android.support.test.InstrumentationRegistry; +import android.support.test.uiautomator.UiDevice; +import java.io.IOException; import java.util.ArrayList; // Based on //platform/frameworks/base/apct-tests/perftests/utils/BenchmarkState.java public class BenchmarkRunner { - private static long COOL_OFF_PERIOD_MS = 2000; + private static final long COOL_OFF_PERIOD_MS = 1000; private static final int NUM_ITERATIONS = 4; @@ -70,9 +73,13 @@ public class BenchmarkRunner { } private void prepareForNextRun() { - // TODO: Once http://b/63115387 is fixed, look into using "am wait-for-broadcast-idle" - // command instead of waiting for a fixed amount of time. SystemClock.sleep(COOL_OFF_PERIOD_MS); + try { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + .executeShellCommand("am wait-for-broadcast-idle"); + } catch (IOException e) { + throw new IllegalStateException("Cannot execute shell command", e); + } mStartTimeNs = System.nanoTime(); mPausedDurationNs = 0; } diff --git a/android/net/ConnectivityManager.java b/android/net/ConnectivityManager.java index 744ee8ed..d7ecc81f 100644 --- a/android/net/ConnectivityManager.java +++ b/android/net/ConnectivityManager.java @@ -2078,16 +2078,30 @@ public class ConnectivityManager { * {@code ro.tether.denied} system property, Settings.TETHER_SUPPORTED or * due to device configuration. * + * <p>If this app does not have permission to use this API, it will always + * return false rather than throw an exception.</p> + * + * <p>If the device has a hotspot provisioning app, the caller is required to hold the + * {@link android.Manifest.permission.TETHER_PRIVILEGED} permission.</p> + * + * <p>Otherwise, this method requires the caller to hold the ability to modify system + * settings as determined by {@link android.provider.Settings.System#canWrite}.</p> + * * @return a boolean - {@code true} indicating Tethering is supported. * * {@hide} */ @SystemApi - @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + @RequiresPermission(anyOf = {android.Manifest.permission.TETHER_PRIVILEGED, + android.Manifest.permission.WRITE_SETTINGS}) public boolean isTetheringSupported() { + String pkgName = mContext.getOpPackageName(); try { - String pkgName = mContext.getOpPackageName(); return mService.isTetheringSupported(pkgName); + } catch (SecurityException e) { + // This API is not available to this caller, but for backward-compatibility + // this will just return false instead of throwing. + return false; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/android/net/IpSecAlgorithm.java b/android/net/IpSecAlgorithm.java index 5ae34003..79310e29 100644 --- a/android/net/IpSecAlgorithm.java +++ b/android/net/IpSecAlgorithm.java @@ -19,15 +19,16 @@ import android.annotation.StringDef; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; + import com.android.internal.util.HexDump; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; /** * IpSecAlgorithm specifies a single algorithm that can be applied to an IpSec Transform. Refer to * RFC 4301. - * - * @hide */ public final class IpSecAlgorithm implements Parcelable { @@ -75,13 +76,7 @@ public final class IpSecAlgorithm implements Parcelable { public static final String AUTH_HMAC_SHA512 = "hmac(sha512)"; /** @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}) @Retention(RetentionPolicy.SOURCE) public @interface AlgorithmName {} @@ -197,4 +192,12 @@ public final class IpSecAlgorithm implements Parcelable { .append("}") .toString(); } + + /** package */ + static boolean equals(IpSecAlgorithm lhs, IpSecAlgorithm rhs) { + if (lhs == null || rhs == null) return (lhs == rhs); + return (lhs.mName.equals(rhs.mName) + && Arrays.equals(lhs.mKey, rhs.mKey) + && lhs.mTruncLenBits == rhs.mTruncLenBits); + } }; diff --git a/android/net/IpSecConfig.java b/android/net/IpSecConfig.java index 5a5c740c..632b7fc0 100644 --- a/android/net/IpSecConfig.java +++ b/android/net/IpSecConfig.java @@ -17,105 +17,170 @@ package android.net; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; -import java.net.InetAddress; -import java.net.UnknownHostException; + +import com.android.internal.annotations.VisibleForTesting; /** @hide */ public final class IpSecConfig implements Parcelable { private static final String TAG = "IpSecConfig"; - //MODE_TRANSPORT or MODE_TUNNEL - int mode; + // MODE_TRANSPORT or MODE_TUNNEL + private int mMode = IpSecTransform.MODE_TRANSPORT; - // For tunnel mode - InetAddress localAddress; + // Needs to be valid only for tunnel mode + // Preventing this from being null simplifies Java->Native binder + private String mLocalAddress = ""; - InetAddress remoteAddress; + // Preventing this from being null simplifies Java->Native binder + private String mRemoteAddress = ""; - // Limit selection by network interface - Network network; + // The underlying Network that represents the "gateway" Network + // for outbound packets. It may also be used to select packets. + private Network mNetwork; public static class Flow { // Minimum requirements for identifying a transform // SPI identifying the IPsec flow in packet processing // and a remote IP address - int spiResourceId; + private int mSpiResourceId = IpSecManager.INVALID_RESOURCE_ID; // Encryption Algorithm - IpSecAlgorithm encryption; + private IpSecAlgorithm mEncryption; // Authentication Algorithm - IpSecAlgorithm authentication; + private IpSecAlgorithm mAuthentication; @Override public String toString() { return new StringBuilder() - .append("{spiResourceId=") - .append(spiResourceId) - .append(", encryption=") - .append(encryption) - .append(", authentication=") - .append(authentication) + .append("{mSpiResourceId=") + .append(mSpiResourceId) + .append(", mEncryption=") + .append(mEncryption) + .append(", mAuthentication=") + .append(mAuthentication) .append("}") .toString(); } + + static boolean equals(IpSecConfig.Flow lhs, IpSecConfig.Flow rhs) { + if (lhs == null || rhs == null) return (lhs == rhs); + return (lhs.mSpiResourceId == rhs.mSpiResourceId + && IpSecAlgorithm.equals(lhs.mEncryption, rhs.mEncryption) + && IpSecAlgorithm.equals(lhs.mAuthentication, rhs.mAuthentication)); + } } - final Flow[] flow = new Flow[] {new Flow(), new Flow()}; + private final Flow[] mFlow = new Flow[] {new Flow(), new Flow()}; // For tunnel mode IPv4 UDP Encapsulation // IpSecTransform#ENCAP_ESP_*, such as ENCAP_ESP_OVER_UDP_IKE - int encapType; - int encapLocalPortResourceId; - int encapRemotePort; + private int mEncapType = IpSecTransform.ENCAP_NONE; + private int mEncapSocketResourceId = IpSecManager.INVALID_RESOURCE_ID; + private int mEncapRemotePort; // An interval, in seconds between the NattKeepalive packets - int nattKeepaliveInterval; + private int mNattKeepaliveInterval; + + /** Set the mode for this IPsec transform */ + public void setMode(int mode) { + mMode = mode; + } + + /** Set the local IP address for Tunnel mode */ + public void setLocalAddress(String localAddress) { + if (localAddress == null) { + throw new IllegalArgumentException("localAddress may not be null!"); + } + mLocalAddress = localAddress; + } + + /** Set the remote IP address for this IPsec transform */ + public void setRemoteAddress(String remoteAddress) { + if (remoteAddress == null) { + throw new IllegalArgumentException("remoteAddress may not be null!"); + } + mRemoteAddress = remoteAddress; + } + + /** Set the SPI for a given direction by resource ID */ + public void setSpiResourceId(int direction, int resourceId) { + mFlow[direction].mSpiResourceId = resourceId; + } + + /** Set the encryption algorithm for a given direction */ + public void setEncryption(int direction, IpSecAlgorithm encryption) { + mFlow[direction].mEncryption = encryption; + } + + /** Set the authentication algorithm for a given direction */ + public void setAuthentication(int direction, IpSecAlgorithm authentication) { + mFlow[direction].mAuthentication = authentication; + } + + public void setNetwork(Network network) { + mNetwork = network; + } + + public void setEncapType(int encapType) { + mEncapType = encapType; + } + + public void setEncapSocketResourceId(int resourceId) { + mEncapSocketResourceId = resourceId; + } + + public void setEncapRemotePort(int port) { + mEncapRemotePort = port; + } + + public void setNattKeepaliveInterval(int interval) { + mNattKeepaliveInterval = interval; + } // Transport or Tunnel public int getMode() { - return mode; + return mMode; } - public InetAddress getLocalAddress() { - return localAddress; + public String getLocalAddress() { + return mLocalAddress; } public int getSpiResourceId(int direction) { - return flow[direction].spiResourceId; + return mFlow[direction].mSpiResourceId; } - public InetAddress getRemoteAddress() { - return remoteAddress; + public String getRemoteAddress() { + return mRemoteAddress; } public IpSecAlgorithm getEncryption(int direction) { - return flow[direction].encryption; + return mFlow[direction].mEncryption; } public IpSecAlgorithm getAuthentication(int direction) { - return flow[direction].authentication; + return mFlow[direction].mAuthentication; } public Network getNetwork() { - return network; + return mNetwork; } public int getEncapType() { - return encapType; + return mEncapType; } - public int getEncapLocalResourceId() { - return encapLocalPortResourceId; + public int getEncapSocketResourceId() { + return mEncapSocketResourceId; } public int getEncapRemotePort() { - return encapRemotePort; + return mEncapRemotePort; } public int getNattKeepaliveInterval() { - return nattKeepaliveInterval; + return mNattKeepaliveInterval; } // Parcelable Methods @@ -127,82 +192,70 @@ public final class IpSecConfig implements Parcelable { @Override public void writeToParcel(Parcel out, int flags) { - // TODO: Use a byte array or other better method for storing IPs that can also include scope - out.writeString((localAddress != null) ? localAddress.getHostAddress() : null); - // TODO: Use a byte array or other better method for storing IPs that can also include scope - out.writeString((remoteAddress != null) ? remoteAddress.getHostAddress() : null); - out.writeParcelable(network, flags); - out.writeInt(flow[IpSecTransform.DIRECTION_IN].spiResourceId); - out.writeParcelable(flow[IpSecTransform.DIRECTION_IN].encryption, flags); - out.writeParcelable(flow[IpSecTransform.DIRECTION_IN].authentication, flags); - out.writeInt(flow[IpSecTransform.DIRECTION_OUT].spiResourceId); - out.writeParcelable(flow[IpSecTransform.DIRECTION_OUT].encryption, flags); - out.writeParcelable(flow[IpSecTransform.DIRECTION_OUT].authentication, flags); - out.writeInt(encapType); - out.writeInt(encapLocalPortResourceId); - out.writeInt(encapRemotePort); - } - - // Package Private: Used by the IpSecTransform.Builder; - // there should be no public constructor for this object - IpSecConfig() {} - - private static InetAddress readInetAddressFromParcel(Parcel in) { - String addrString = in.readString(); - if (addrString == null) { - return null; - } - try { - return InetAddress.getByName(addrString); - } catch (UnknownHostException e) { - Log.wtf(TAG, "Invalid IpAddress " + addrString); - return null; - } + out.writeInt(mMode); + out.writeString(mLocalAddress); + out.writeString(mRemoteAddress); + out.writeParcelable(mNetwork, flags); + out.writeInt(mFlow[IpSecTransform.DIRECTION_IN].mSpiResourceId); + out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mEncryption, flags); + out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mAuthentication, 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.writeInt(mEncapType); + out.writeInt(mEncapSocketResourceId); + out.writeInt(mEncapRemotePort); + out.writeInt(mNattKeepaliveInterval); } + @VisibleForTesting + public IpSecConfig() {} + private IpSecConfig(Parcel in) { - localAddress = readInetAddressFromParcel(in); - remoteAddress = readInetAddressFromParcel(in); - network = (Network) in.readParcelable(Network.class.getClassLoader()); - flow[IpSecTransform.DIRECTION_IN].spiResourceId = in.readInt(); - flow[IpSecTransform.DIRECTION_IN].encryption = + mMode = in.readInt(); + mLocalAddress = in.readString(); + mRemoteAddress = in.readString(); + mNetwork = (Network) in.readParcelable(Network.class.getClassLoader()); + mFlow[IpSecTransform.DIRECTION_IN].mSpiResourceId = in.readInt(); + mFlow[IpSecTransform.DIRECTION_IN].mEncryption = (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); - flow[IpSecTransform.DIRECTION_IN].authentication = + mFlow[IpSecTransform.DIRECTION_IN].mAuthentication = (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); - flow[IpSecTransform.DIRECTION_OUT].spiResourceId = in.readInt(); - flow[IpSecTransform.DIRECTION_OUT].encryption = + mFlow[IpSecTransform.DIRECTION_OUT].mSpiResourceId = in.readInt(); + mFlow[IpSecTransform.DIRECTION_OUT].mEncryption = (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); - flow[IpSecTransform.DIRECTION_OUT].authentication = + mFlow[IpSecTransform.DIRECTION_OUT].mAuthentication = (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); - encapType = in.readInt(); - encapLocalPortResourceId = in.readInt(); - encapRemotePort = in.readInt(); + mEncapType = in.readInt(); + mEncapSocketResourceId = in.readInt(); + mEncapRemotePort = in.readInt(); + mNattKeepaliveInterval = in.readInt(); } @Override public String toString() { StringBuilder strBuilder = new StringBuilder(); strBuilder - .append("{mode=") - .append(mode == IpSecTransform.MODE_TUNNEL ? "TUNNEL" : "TRANSPORT") - .append(", localAddress=") - .append(localAddress) - .append(", remoteAddress=") - .append(remoteAddress) - .append(", network=") - .append(network) - .append(", encapType=") - .append(encapType) - .append(", encapLocalPortResourceId=") - .append(encapLocalPortResourceId) - .append(", encapRemotePort=") - .append(encapRemotePort) - .append(", nattKeepaliveInterval=") - .append(nattKeepaliveInterval) - .append(", flow[OUT]=") - .append(flow[IpSecTransform.DIRECTION_OUT]) - .append(", flow[IN]=") - .append(flow[IpSecTransform.DIRECTION_IN]) + .append("{mMode=") + .append(mMode == IpSecTransform.MODE_TUNNEL ? "TUNNEL" : "TRANSPORT") + .append(", mLocalAddress=") + .append(mLocalAddress) + .append(", mRemoteAddress=") + .append(mRemoteAddress) + .append(", mNetwork=") + .append(mNetwork) + .append(", mEncapType=") + .append(mEncapType) + .append(", mEncapSocketResourceId=") + .append(mEncapSocketResourceId) + .append(", mEncapRemotePort=") + .append(mEncapRemotePort) + .append(", mNattKeepaliveInterval=") + .append(mNattKeepaliveInterval) + .append(", mFlow[OUT]=") + .append(mFlow[IpSecTransform.DIRECTION_OUT]) + .append(", mFlow[IN]=") + .append(mFlow[IpSecTransform.DIRECTION_IN]) .append("}"); return strBuilder.toString(); @@ -218,4 +271,23 @@ public final class IpSecConfig implements Parcelable { return new IpSecConfig[size]; } }; + + @VisibleForTesting + /** Equals method used for testing */ + public static boolean equals(IpSecConfig lhs, IpSecConfig rhs) { + if (lhs == null || rhs == null) return (lhs == rhs); + return (lhs.mMode == rhs.mMode + && lhs.mLocalAddress.equals(rhs.mLocalAddress) + && lhs.mRemoteAddress.equals(rhs.mRemoteAddress) + && ((lhs.mNetwork != null && lhs.mNetwork.equals(rhs.mNetwork)) + || (lhs.mNetwork == rhs.mNetwork)) + && lhs.mEncapType == rhs.mEncapType + && lhs.mEncapSocketResourceId == rhs.mEncapSocketResourceId + && lhs.mEncapRemotePort == rhs.mEncapRemotePort + && lhs.mNattKeepaliveInterval == rhs.mNattKeepaliveInterval + && IpSecConfig.Flow.equals(lhs.mFlow[IpSecTransform.DIRECTION_OUT], + rhs.mFlow[IpSecTransform.DIRECTION_OUT]) + && IpSecConfig.Flow.equals(lhs.mFlow[IpSecTransform.DIRECTION_IN], + rhs.mFlow[IpSecTransform.DIRECTION_IN])); + } } diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java index 2f791e15..d7b32561 100644 --- a/android/net/IpSecManager.java +++ b/android/net/IpSecManager.java @@ -25,7 +25,11 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.AndroidException; import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + import dalvik.system.CloseGuard; + import java.io.FileDescriptor; import java.io.IOException; import java.net.DatagramSocket; @@ -36,7 +40,9 @@ import java.net.Socket; * This class contains methods for managing IPsec sessions, which will perform kernel-space * encryption and decryption of socket or Network traffic. * - * @hide + * <p>An IpSecManager may be obtained by calling {@link + * android.content.Context#getSystemService(String) Context#getSystemService(String)} with {@link + * android.content.Context#IPSEC_SERVICE Context#IPSEC_SERVICE} */ @SystemService(Context.IPSEC_SERVICE) public final class IpSecManager { @@ -184,7 +190,8 @@ public final class IpSecManager { } /** @hide */ - int getResourceId() { + @VisibleForTesting + public int getResourceId() { return mResourceId; } } @@ -485,7 +492,8 @@ public final class IpSecManager { } /** @hide */ - int getResourceId() { + @VisibleForTesting + public int getResourceId() { return mResourceId; } }; diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java index cfbac58b..e15a2c67 100644 --- a/android/net/IpSecTransform.java +++ b/android/net/IpSecTransform.java @@ -26,9 +26,12 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; + import dalvik.system.CloseGuard; + import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -43,8 +46,6 @@ import java.net.InetAddress; * * <p>An IpSecTransform may either represent a tunnel mode transform that operates on a wide array * of traffic or may represent a transport mode transform operating on a Socket or Sockets. - * - * @hide */ public final class IpSecTransform implements AutoCloseable { private static final String TAG = "IpSecTransform"; @@ -67,10 +68,10 @@ public final class IpSecTransform implements AutoCloseable { public @interface TransformDirection {} /** @hide */ - public static final int MODE_TUNNEL = 0; + public static final int MODE_TRANSPORT = 0; /** @hide */ - public static final int MODE_TRANSPORT = 1; + public static final int MODE_TUNNEL = 1; /** @hide */ public static final int ENCAP_NONE = 0; @@ -112,7 +113,11 @@ public final class IpSecTransform implements AutoCloseable { return IIpSecService.Stub.asInterface(b); } - private void checkResultStatusAndThrow(int status) + /** + * Checks the result status and throws an appropriate exception if + * the status is not Status.OK. + */ + private void checkResultStatus(int status) throws IOException, IpSecManager.ResourceUnavailableException, IpSecManager.SpiUnavailableException { switch (status) { @@ -140,7 +145,7 @@ public final class IpSecTransform implements AutoCloseable { IpSecTransformResponse result = svc.createTransportModeTransform(mConfig, new Binder()); int status = result.status; - checkResultStatusAndThrow(status); + checkResultStatus(status); mResourceId = result.resourceId; /* Keepalive will silently fail if not needed by the config; but, if needed and @@ -242,61 +247,20 @@ public final class IpSecTransform implements AutoCloseable { /* Package */ void startKeepalive(Context c) { - // FIXME: NO_KEEPALIVE needs to be a constant - if (mConfig.getNattKeepaliveInterval() == 0) { - return; - } - - ConnectivityManager cm = - (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); - - if (mKeepalive != null) { - Log.wtf(TAG, "Keepalive already started for this IpSecTransform."); - return; - } - - synchronized (mKeepaliveSyncLock) { - mKeepalive = - cm.startNattKeepalive( - mConfig.getNetwork(), - mConfig.getNattKeepaliveInterval(), - mKeepaliveCallback, - mConfig.getLocalAddress(), - 0x1234, /* FIXME: get the real port number again, - which we need to retrieve from the provided - EncapsulationSocket, and which isn't currently - stashed in IpSecConfig */ - mConfig.getRemoteAddress()); - try { - // FIXME: this is still a horrible way to fudge the synchronous callback - mKeepaliveSyncLock.wait(2000); - } catch (InterruptedException e) { - } - } - if (mKeepaliveStatus != ConnectivityManager.PacketKeepalive.SUCCESS) { - throw new UnsupportedOperationException("Packet Keepalive cannot be started"); + if (mConfig.getNattKeepaliveInterval() != 0) { + Log.wtf(TAG, "Keepalive not yet supported."); } } - /* Package */ - int getResourceId() { + /** @hide */ + @VisibleForTesting + public int getResourceId() { return mResourceId; } /* Package */ void stopKeepalive() { - if (mKeepalive == null) { - return; - } - mKeepalive.stop(); - synchronized (mKeepaliveSyncLock) { - if (mKeepaliveStatus == ConnectivityManager.PacketKeepalive.SUCCESS) { - try { - mKeepaliveSyncLock.wait(2000); - } catch (InterruptedException e) { - } - } - } + return; } /** @@ -322,7 +286,7 @@ public final class IpSecTransform implements AutoCloseable { */ public IpSecTransform.Builder setEncryption( @TransformDirection int direction, IpSecAlgorithm algo) { - mConfig.flow[direction].encryption = algo; + mConfig.setEncryption(direction, algo); return this; } @@ -337,7 +301,7 @@ public final class IpSecTransform implements AutoCloseable { */ public IpSecTransform.Builder setAuthentication( @TransformDirection int direction, IpSecAlgorithm algo) { - mConfig.flow[direction].authentication = algo; + mConfig.setAuthentication(direction, algo); return this; } @@ -360,9 +324,7 @@ public final class IpSecTransform implements AutoCloseable { */ public IpSecTransform.Builder setSpi( @TransformDirection int direction, IpSecManager.SecurityParameterIndex spi) { - // TODO: convert to using the resource Id of the SPI. Then build() can validate - // the owner in the IpSecService - mConfig.flow[direction].spiResourceId = spi.getResourceId(); + mConfig.setSpiResourceId(direction, spi.getResourceId()); return this; } @@ -377,7 +339,7 @@ public final class IpSecTransform implements AutoCloseable { */ @SystemApi public IpSecTransform.Builder setUnderlyingNetwork(Network net) { - mConfig.network = net; + mConfig.setNetwork(net); return this; } @@ -394,10 +356,9 @@ public final class IpSecTransform implements AutoCloseable { */ public IpSecTransform.Builder setIpv4Encapsulation( IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) { - // TODO: check encap type is valid. - mConfig.encapType = ENCAP_ESPINUDP; - mConfig.encapLocalPortResourceId = localSocket.getResourceId(); - mConfig.encapRemotePort = remotePort; + mConfig.setEncapType(ENCAP_ESPINUDP); + mConfig.setEncapSocketResourceId(localSocket.getResourceId()); + mConfig.setEncapRemotePort(remotePort); return this; } @@ -415,7 +376,7 @@ public final class IpSecTransform implements AutoCloseable { */ @SystemApi public IpSecTransform.Builder setNattKeepalive(int intervalSeconds) { - mConfig.nattKeepaliveInterval = intervalSeconds; + mConfig.setNattKeepaliveInterval(intervalSeconds); return this; } @@ -448,10 +409,8 @@ public final class IpSecTransform implements AutoCloseable { public IpSecTransform buildTransportModeTransform(InetAddress remoteAddress) throws IpSecManager.ResourceUnavailableException, IpSecManager.SpiUnavailableException, IOException { - //FIXME: argument validation here - //throw new IllegalArgumentException("Natt Keepalive requires UDP Encapsulation"); - mConfig.mode = MODE_TRANSPORT; - mConfig.remoteAddress = remoteAddress; + mConfig.setMode(MODE_TRANSPORT); + mConfig.setRemoteAddress(remoteAddress.getHostAddress()); return new IpSecTransform(mContext, mConfig).activate(); } @@ -472,9 +431,9 @@ public final class IpSecTransform implements AutoCloseable { InetAddress localAddress, InetAddress remoteAddress) { //FIXME: argument validation here //throw new IllegalArgumentException("Natt Keepalive requires UDP Encapsulation"); - mConfig.localAddress = localAddress; - mConfig.remoteAddress = remoteAddress; - mConfig.mode = MODE_TUNNEL; + mConfig.setLocalAddress(localAddress.getHostAddress()); + mConfig.setRemoteAddress(remoteAddress.getHostAddress()); + mConfig.setMode(MODE_TUNNEL); return new IpSecTransform(mContext, mConfig); } @@ -488,14 +447,5 @@ public final class IpSecTransform implements AutoCloseable { mContext = context; mConfig = new IpSecConfig(); } - - /** - * Return an {@link IpSecConfig} object for testing purposes. - * @hide - */ - @VisibleForTesting - public IpSecConfig getIpSecConfig() { - return mConfig; - } } } diff --git a/android/net/LocalServerSocket.java b/android/net/LocalServerSocket.java index 3fcde330..d1f49d20 100644 --- a/android/net/LocalServerSocket.java +++ b/android/net/LocalServerSocket.java @@ -16,14 +16,15 @@ package android.net; -import java.io.IOException; +import java.io.Closeable; import java.io.FileDescriptor; +import java.io.IOException; /** * Non-standard class for creating an inbound UNIX-domain socket * in the Linux abstract namespace. */ -public class LocalServerSocket { +public class LocalServerSocket implements Closeable { private final LocalSocketImpl impl; private final LocalSocketAddress localAddress; @@ -106,7 +107,7 @@ public class LocalServerSocket { * * @throws IOException */ - public void close() throws IOException + @Override public void close() throws IOException { impl.close(); } diff --git a/android/net/ip/ConnectivityPacketTracker.java b/android/net/ip/ConnectivityPacketTracker.java index 884a8a75..0230f36b 100644 --- a/android/net/ip/ConnectivityPacketTracker.java +++ b/android/net/ip/ConnectivityPacketTracker.java @@ -61,11 +61,11 @@ public class ConnectivityPacketTracker { private static final String MARK_STOP = "--- STOP ---"; private final String mTag; - private final Handler mHandler; private final LocalLog mLog; private final BlockingSocketReader mPacketListener; + private boolean mRunning; - public ConnectivityPacketTracker(NetworkInterface netif, LocalLog log) { + public ConnectivityPacketTracker(Handler h, NetworkInterface netif, LocalLog log) { final String ifname; final int ifindex; final byte[] hwaddr; @@ -81,44 +81,40 @@ public class ConnectivityPacketTracker { } mTag = TAG + "." + ifname; - mHandler = new Handler(); mLog = log; - mPacketListener = new PacketListener(ifindex, hwaddr, mtu); + mPacketListener = new PacketListener(h, ifindex, hwaddr, mtu); } public void start() { - mLog.log(MARK_START); + mRunning = true; mPacketListener.start(); } public void stop() { mPacketListener.stop(); - mLog.log(MARK_STOP); + mRunning = false; } private final class PacketListener extends BlockingSocketReader { private final int mIfIndex; private final byte mHwAddr[]; - PacketListener(int ifindex, byte[] hwaddr, int mtu) { - super(mtu); + PacketListener(Handler h, int ifindex, byte[] hwaddr, int mtu) { + super(h, mtu); mIfIndex = ifindex; mHwAddr = hwaddr; } @Override - protected FileDescriptor createSocket() { + protected FileDescriptor createFd() { FileDescriptor s = null; try { - // TODO: Evaluate switching to SOCK_DGRAM and changing the - // BlockingSocketReader's read() to recvfrom(), so that this - // might work on non-ethernet-like links (via SLL). s = Os.socket(AF_PACKET, SOCK_RAW, 0); NetworkUtils.attachControlPacketFilter(s, ARPHRD_ETHER); Os.bind(s, new PacketSocketAddress((short) ETH_P_ALL, mIfIndex)); } catch (ErrnoException | IOException e) { logError("Failed to create packet tracking socket: ", e); - closeSocket(s); + closeFd(s); return null; } return s; @@ -136,13 +132,27 @@ public class ConnectivityPacketTracker { } @Override + protected void onStart() { + mLog.log(MARK_START); + } + + @Override + protected void onStop() { + if (mRunning) { + mLog.log(MARK_STOP); + } else { + mLog.log(MARK_STOP + " (packet listener stopped unexpectedly)"); + } + } + + @Override protected void logError(String msg, Exception e) { Log.e(mTag, msg, e); addLogEntry(msg + e); } private void addLogEntry(String entry) { - mHandler.post(() -> mLog.log(entry)); + mLog.log(entry); } } } diff --git a/android/net/ip/IpManager.java b/android/net/ip/IpManager.java index b1eb0854..bc07b810 100644 --- a/android/net/ip/IpManager.java +++ b/android/net/ip/IpManager.java @@ -1515,7 +1515,8 @@ public class IpManager extends StateMachine { private ConnectivityPacketTracker createPacketTracker() { try { - return new ConnectivityPacketTracker(mNetworkInterface, mConnectivityPacketLog); + return new ConnectivityPacketTracker( + getHandler(), mNetworkInterface, mConnectivityPacketLog); } catch (IllegalArgumentException e) { return null; } diff --git a/android/net/metrics/WakeupStats.java b/android/net/metrics/WakeupStats.java index d520b974..97e83f96 100644 --- a/android/net/metrics/WakeupStats.java +++ b/android/net/metrics/WakeupStats.java @@ -35,7 +35,7 @@ public class WakeupStats { public long systemWakeups = 0; public long nonApplicationWakeups = 0; public long applicationWakeups = 0; - public long unroutedWakeups = 0; + public long noUidWakeups = 0; public long durationSec = 0; public WakeupStats(String iface) { @@ -58,7 +58,7 @@ public class WakeupStats { systemWakeups++; break; case NO_UID: - unroutedWakeups++; + noUidWakeups++; break; default: if (ev.uid >= Process.FIRST_APPLICATION_UID) { @@ -80,7 +80,7 @@ public class WakeupStats { .append(", system: ").append(systemWakeups) .append(", apps: ").append(applicationWakeups) .append(", non-apps: ").append(nonApplicationWakeups) - .append(", unrouted: ").append(unroutedWakeups) + .append(", no uid: ").append(noUidWakeups) .append(", ").append(durationSec).append("s)") .toString(); } diff --git a/android/net/util/BlockingSocketReader.java b/android/net/util/BlockingSocketReader.java index 12fa1e57..99bf4695 100644 --- a/android/net/util/BlockingSocketReader.java +++ b/android/net/util/BlockingSocketReader.java @@ -16,81 +16,106 @@ package android.net.util; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; + import android.annotation.Nullable; +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; +import android.os.MessageQueue.OnFileDescriptorEventListener; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; -import libcore.io.IoBridge; +import libcore.io.IoUtils; import java.io.FileDescriptor; -import java.io.InterruptedIOException; import java.io.IOException; /** - * A thread that reads from a socket and passes the received packets to a - * subclass's handlePacket() method. The packet receive buffer is recycled - * on every read call, so subclasses should make any copies they would like - * inside their handlePacket() implementation. + * This class encapsulates the mechanics of registering a file descriptor + * with a thread's Looper and handling read events (and errors). + * + * Subclasses MUST implement createFd() and SHOULD override handlePacket(). + + * Subclasses can expect a call life-cycle like the following: + * + * [1] start() calls createFd() and (if all goes well) onStart() + * + * [2] yield, waiting for read event or error notification: + * + * [a] readPacket() && handlePacket() * - * All public methods may be called from any thread. + * [b] if (no error): + * goto 2 + * else: + * goto 3 + * + * [3] stop() calls onStop() if not previously stopped + * + * The packet receive buffer is recycled on every read call, so subclasses + * should make any copies they would like inside their handlePacket() + * implementation. + * + * All public methods MUST only be called from the same thread with which + * the Handler constructor argument is associated. + * + * TODO: rename this class to something more correctly descriptive (something + * like [or less horrible than] FdReadEventsHandler?). * * @hide */ public abstract class BlockingSocketReader { + private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; + private static final int UNREGISTER_THIS_FD = 0; + public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024; + private final Handler mHandler; + private final MessageQueue mQueue; private final byte[] mPacket; - private final Thread mThread; - private volatile FileDescriptor mSocket; - private volatile boolean mRunning; - private volatile long mPacketsReceived; - - // Make it slightly easier for subclasses to properly close a socket - // without having to know this incantation. - public static final void closeSocket(@Nullable FileDescriptor fd) { - try { - IoBridge.closeAndSignalBlockedThreads(fd); - } catch (IOException ignored) {} - } + private FileDescriptor mFd; + private long mPacketsReceived; - protected BlockingSocketReader() { - this(DEFAULT_RECV_BUF_SIZE); + protected static void closeFd(FileDescriptor fd) { + IoUtils.closeQuietly(fd); } - protected BlockingSocketReader(int recvbufsize) { - if (recvbufsize < DEFAULT_RECV_BUF_SIZE) { - recvbufsize = DEFAULT_RECV_BUF_SIZE; - } - mPacket = new byte[recvbufsize]; - mThread = new Thread(() -> { mainLoop(); }); + protected BlockingSocketReader(Handler h) { + this(h, DEFAULT_RECV_BUF_SIZE); } - public final boolean start() { - if (mSocket != null) return false; + protected BlockingSocketReader(Handler h, int recvbufsize) { + mHandler = h; + mQueue = mHandler.getLooper().getQueue(); + mPacket = new byte[Math.max(recvbufsize, DEFAULT_RECV_BUF_SIZE)]; + } - try { - mSocket = createSocket(); - } catch (Exception e) { - logError("Failed to create socket: ", e); - return false; + public final void start() { + if (onCorrectThread()) { + createAndRegisterFd(); + } else { + mHandler.post(() -> { + logError("start() called from off-thread", null); + createAndRegisterFd(); + }); } - - if (mSocket == null) return false; - - mRunning = true; - mThread.start(); - return true; } public final void stop() { - mRunning = false; - closeSocket(mSocket); - mSocket = null; + if (onCorrectThread()) { + unregisterAndDestroyFd(); + } else { + mHandler.post(() -> { + logError("stop() called from off-thread", null); + unregisterAndDestroyFd(); + }); + } } - public final boolean isRunning() { return mRunning; } + public final int recvBufSize() { return mPacket.length; } public final long numPacketsReceived() { return mPacketsReceived; } @@ -98,11 +123,21 @@ public abstract class BlockingSocketReader { * Subclasses MUST create the listening socket here, including setting * all desired socket options, interface or address/port binding, etc. */ - protected abstract FileDescriptor createSocket(); + protected abstract FileDescriptor createFd(); + + /** + * Subclasses MAY override this to change the default read() implementation + * in favour of, say, recvfrom(). + * + * Implementations MUST return the bytes read or throw an Exception. + */ + protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception { + return Os.read(fd, packetBuffer, 0, packetBuffer.length); + } /** * Called by the main loop for every packet. Any desired copies of - * |recvbuf| should be made in here, and the underlying byte array is + * |recvbuf| should be made in here, as the underlying byte array is * reused across all reads. */ protected void handlePacket(byte[] recvbuf, int length) {} @@ -113,43 +148,102 @@ public abstract class BlockingSocketReader { protected void logError(String msg, Exception e) {} /** - * Called by the main loop just prior to exiting. + * Called by start(), if successful, just prior to returning. + */ + protected void onStart() {} + + /** + * Called by stop() just prior to returning. */ - protected void onExit() {} + protected void onStop() {} + + private void createAndRegisterFd() { + if (mFd != null) return; + + try { + mFd = createFd(); + if (mFd != null) { + // Force the socket to be non-blocking. + IoUtils.setBlocking(mFd, false); + } + } catch (Exception e) { + logError("Failed to create socket: ", e); + closeFd(mFd); + mFd = null; + return; + } + + if (mFd == null) return; + + mQueue.addOnFileDescriptorEventListener( + mFd, + FD_EVENTS, + new OnFileDescriptorEventListener() { + @Override + public int onFileDescriptorEvents(FileDescriptor fd, int events) { + // Always call handleInput() so read/recvfrom are given + // a proper chance to encounter a meaningful errno and + // perhaps log a useful error message. + if (!isRunning() || !handleInput()) { + unregisterAndDestroyFd(); + return UNREGISTER_THIS_FD; + } + return FD_EVENTS; + } + }); + onStart(); + } - private final void mainLoop() { + private boolean isRunning() { return (mFd != null) && mFd.valid(); } + + // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error. + private boolean handleInput() { while (isRunning()) { final int bytesRead; try { - // Blocking read. - // TODO: See if this can be converted to recvfrom. - bytesRead = Os.read(mSocket, mPacket, 0, mPacket.length); + bytesRead = readPacket(mFd, mPacket); if (bytesRead < 1) { if (isRunning()) logError("Socket closed, exiting", null); break; } mPacketsReceived++; } catch (ErrnoException e) { - if (e.errno != OsConstants.EINTR) { - if (isRunning()) logError("read error: ", e); + if (e.errno == OsConstants.EAGAIN) { + // We've read everything there is to read this time around. + return true; + } else if (e.errno == OsConstants.EINTR) { + continue; + } else { + if (isRunning()) logError("readPacket error: ", e); break; } - continue; - } catch (IOException ioe) { - if (isRunning()) logError("read error: ", ioe); - continue; + } catch (Exception e) { + if (isRunning()) logError("readPacket error: ", e); + break; } try { handlePacket(mPacket, bytesRead); } catch (Exception e) { - logError("Unexpected exception: ", e); + logError("handlePacket error: ", e); break; } } - stop(); - onExit(); + return false; + } + + private void unregisterAndDestroyFd() { + if (mFd == null) return; + + mQueue.removeOnFileDescriptorEventListener(mFd); + closeFd(mFd); + mFd = null; + onStop(); + } + + private boolean onCorrectThread() { + return (mHandler.getLooper() == Looper.myLooper()); } } diff --git a/android/net/util/NetworkConstants.java b/android/net/util/NetworkConstants.java index 60652681..5a3a8be9 100644 --- a/android/net/util/NetworkConstants.java +++ b/android/net/util/NetworkConstants.java @@ -107,6 +107,20 @@ public final class NetworkConstants { public static final int RFC6177_MIN_PREFIX_LENGTH = 48; /** + * ICMP common (v4/v6) constants. + * + * See also: + * - https://tools.ietf.org/html/rfc792 + * - https://tools.ietf.org/html/rfc4443 + */ + public static final int ICMP_HEADER_TYPE_OFFSET = 0; + public static final int ICMP_HEADER_CODE_OFFSET = 1; + public static final int ICMP_HEADER_CHECKSUM_OFFSET = 2; + public static final int ICMP_ECHO_IDENTIFIER_OFFSET = 4; + public static final int ICMP_ECHO_SEQUENCE_NUMBER_OFFSET = 6; + public static final int ICMP_ECHO_DATA_OFFSET = 8; + + /** * ICMPv6 constants. * * See also: diff --git a/android/net/wifi/WifiConfiguration.java b/android/net/wifi/WifiConfiguration.java index a1453278..6438631c 100644 --- a/android/net/wifi/WifiConfiguration.java +++ b/android/net/wifi/WifiConfiguration.java @@ -790,6 +790,28 @@ public class WifiConfiguration implements Parcelable { /** * @hide + * Returns true if this WiFi config is for an open network. + */ + public boolean isOpenNetwork() { + final int cardinality = allowedKeyManagement.cardinality(); + final boolean hasNoKeyMgmt = cardinality == 0 + || (cardinality == 1 && allowedKeyManagement.get(KeyMgmt.NONE)); + + boolean hasNoWepKeys = true; + if (wepKeys != null) { + for (int i = 0; i < wepKeys.length; i++) { + if (wepKeys[i] != null) { + hasNoWepKeys = false; + break; + } + } + } + + return hasNoKeyMgmt && hasNoWepKeys; + } + + /** + * @hide * Setting this value will force scan results associated with this configuration to * be included in the bucket of networks that are externally scored. * If not set, associated scan results will be treated as legacy saved networks and diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java index 9c8ea88c..b08b4b7c 100644 --- a/android/net/wifi/WifiManager.java +++ b/android/net/wifi/WifiManager.java @@ -967,8 +967,7 @@ public class WifiManager { * <li>allowedGroupCiphers</li> * </ul> * @return a list of network configurations in the form of a list - * of {@link WifiConfiguration} objects. Upon failure to fetch or - * when Wi-Fi is turned off, it can be null. + * of {@link WifiConfiguration} objects. */ public List<WifiConfiguration> getConfiguredNetworks() { try { diff --git a/android/net/wifi/WifiScanner.java b/android/net/wifi/WifiScanner.java index f47d5caf..e3752ac7 100644 --- a/android/net/wifi/WifiScanner.java +++ b/android/net/wifi/WifiScanner.java @@ -1105,8 +1105,6 @@ public class WifiScanner { private static final int BASE = Protocol.BASE_WIFI_SCANNER; /** @hide */ - public static final int CMD_SCAN = BASE + 0; - /** @hide */ public static final int CMD_START_BACKGROUND_SCAN = BASE + 2; /** @hide */ public static final int CMD_STOP_BACKGROUND_SCAN = BASE + 3; @@ -1115,20 +1113,10 @@ public class WifiScanner { /** @hide */ public static final int CMD_SCAN_RESULT = BASE + 5; /** @hide */ - public static final int CMD_AP_FOUND = BASE + 9; - /** @hide */ - public static final int CMD_AP_LOST = BASE + 10; - /** @hide */ - public static final int CMD_WIFI_CHANGE_DETECTED = BASE + 15; - /** @hide */ - public static final int CMD_WIFI_CHANGES_STABILIZED = BASE + 16; - /** @hide */ public static final int CMD_OP_SUCCEEDED = BASE + 17; /** @hide */ public static final int CMD_OP_FAILED = BASE + 18; /** @hide */ - public static final int CMD_PERIOD_CHANGED = BASE + 19; - /** @hide */ public static final int CMD_FULL_SCAN_RESULT = BASE + 20; /** @hide */ public static final int CMD_START_SINGLE_SCAN = BASE + 21; @@ -1359,25 +1347,6 @@ public class WifiScanner { ScanResult result = (ScanResult) msg.obj; ((ScanListener) listener).onFullResult(result); return; - case CMD_PERIOD_CHANGED: - ((ScanListener) listener).onPeriodChanged(msg.arg1); - return; - case CMD_AP_FOUND: - ((BssidListener) listener).onFound( - ((ParcelableScanResults) msg.obj).getResults()); - return; - case CMD_AP_LOST: - ((BssidListener) listener).onLost( - ((ParcelableScanResults) msg.obj).getResults()); - return; - case CMD_WIFI_CHANGE_DETECTED: - ((WifiChangeListener) listener).onChanging( - ((ParcelableScanResults) msg.obj).getResults()); - return; - case CMD_WIFI_CHANGES_STABILIZED: - ((WifiChangeListener) listener).onQuiescence( - ((ParcelableScanResults) msg.obj).getResults()); - return; case CMD_SINGLE_SCAN_COMPLETED: if (DBG) Log.d(TAG, "removing listener for single scan"); removeListener(msg.arg2); diff --git a/android/net/wifi/aware/DiscoverySession.java b/android/net/wifi/aware/DiscoverySession.java index 357f76e3..9f736223 100644 --- a/android/net/wifi/aware/DiscoverySession.java +++ b/android/net/wifi/aware/DiscoverySession.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.net.NetworkSpecifier; -import android.net.wifi.RttManager; import android.util.Log; import dalvik.system.CloseGuard; @@ -224,37 +223,6 @@ public class DiscoverySession implements AutoCloseable { } /** - * Start a ranging operation with the specified peers. The peer IDs are obtained from an - * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, - * byte[], java.util.List)} or - * {@link DiscoverySessionCallback#onMessageReceived(PeerHandle, - * byte[])} operation - can - * only range devices which are part of an ongoing discovery session. - * - * @param params RTT parameters - each corresponding to a specific peer ID (the array sizes - * must be identical). The - * {@link android.net.wifi.RttManager.RttParams#bssid} member must be set to - * a peer ID - not to a MAC address. - * @param listener The listener to receive the results of the ranging session. - * @hide - * [TODO: b/28847998 - track RTT API & visilibity] - */ - public void startRanging(RttManager.RttParams[] params, RttManager.RttListener listener) { - if (mTerminated) { - Log.w(TAG, "startRanging: called on terminated session"); - return; - } - - WifiAwareManager mgr = mMgr.get(); - if (mgr == null) { - Log.w(TAG, "startRanging: called post GC on WifiAwareManager"); - return; - } - - mgr.startRanging(mClientId, mSessionId, params, listener); - } - - /** * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} for * an unencrypted WiFi Aware connection (link) to the specified peer. The * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to diff --git a/android/net/wifi/aware/PeerHandle.java b/android/net/wifi/aware/PeerHandle.java index cd45c524..1b0aba15 100644 --- a/android/net/wifi/aware/PeerHandle.java +++ b/android/net/wifi/aware/PeerHandle.java @@ -32,4 +32,24 @@ public class PeerHandle { /** @hide */ public int peerId; + + /** @hide RTT_API */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof PeerHandle)) { + return false; + } + + return peerId == ((PeerHandle) o).peerId; + } + + /** @hide RTT_API */ + @Override + public int hashCode() { + return peerId; + } } diff --git a/android/net/wifi/aware/WifiAwareManager.java b/android/net/wifi/aware/WifiAwareManager.java index df0d9d23..ed6804d5 100644 --- a/android/net/wifi/aware/WifiAwareManager.java +++ b/android/net/wifi/aware/WifiAwareManager.java @@ -26,7 +26,6 @@ import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkRequest; import android.net.NetworkSpecifier; -import android.net.wifi.RttManager; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -35,9 +34,6 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.util.Log; -import android.util.SparseArray; - -import com.android.internal.annotations.GuardedBy; import libcore.util.HexEncoding; @@ -45,7 +41,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.nio.BufferOverflowException; -import java.util.Arrays; import java.util.List; /** @@ -172,9 +167,6 @@ public class WifiAwareManager { private final Object mLock = new Object(); // lock access to the following vars - @GuardedBy("mLock") - private SparseArray<RttManager.RttListener> mRangingListeners = new SparseArray<>(); - /** @hide */ public WifiAwareManager(Context context, IWifiAwareManager service) { mContext = context; @@ -401,27 +393,6 @@ public class WifiAwareManager { } /** @hide */ - public void startRanging(int clientId, int sessionId, RttManager.RttParams[] params, - RttManager.RttListener listener) { - if (VDBG) { - Log.v(TAG, "startRanging: clientId=" + clientId + ", sessionId=" + sessionId + ", " - + "params=" + Arrays.toString(params) + ", listener=" + listener); - } - - int rangingKey = 0; - try { - rangingKey = mService.startRanging(clientId, sessionId, - new RttManager.ParcelableRttParams(params)); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - - synchronized (mLock) { - mRangingListeners.put(rangingKey, listener); - } - } - - /** @hide */ public NetworkSpecifier createNetworkSpecifier(int clientId, int role, int sessionId, PeerHandle peerHandle, @Nullable byte[] pmk, @Nullable String passphrase) { if (VDBG) { @@ -500,29 +471,12 @@ public class WifiAwareManager { private static final int CALLBACK_CONNECT_SUCCESS = 0; private static final int CALLBACK_CONNECT_FAIL = 1; private static final int CALLBACK_IDENTITY_CHANGED = 2; - private static final int CALLBACK_RANGING_SUCCESS = 3; - private static final int CALLBACK_RANGING_FAILURE = 4; - private static final int CALLBACK_RANGING_ABORTED = 5; private final Handler mHandler; private final WeakReference<WifiAwareManager> mAwareManager; private final Binder mBinder; private final Looper mLooper; - RttManager.RttListener getAndRemoveRangingListener(int rangingId) { - WifiAwareManager mgr = mAwareManager.get(); - if (mgr == null) { - Log.w(TAG, "getAndRemoveRangingListener: called post GC"); - return null; - } - - synchronized (mgr.mLock) { - RttManager.RttListener listener = mgr.mRangingListeners.get(rangingId); - mgr.mRangingListeners.delete(rangingId); - return listener; - } - } - /** * Constructs a {@link AttachCallback} using the specified looper. * All callbacks will delivered on the thread of the specified looper. @@ -567,37 +521,6 @@ public class WifiAwareManager { identityChangedListener.onIdentityChanged((byte[]) msg.obj); } break; - case CALLBACK_RANGING_SUCCESS: { - RttManager.RttListener listener = getAndRemoveRangingListener(msg.arg1); - if (listener == null) { - Log.e(TAG, "CALLBACK_RANGING_SUCCESS rangingId=" + msg.arg1 - + ": no listener registered (anymore)"); - } else { - listener.onSuccess( - ((RttManager.ParcelableRttResults) msg.obj).mResults); - } - break; - } - case CALLBACK_RANGING_FAILURE: { - RttManager.RttListener listener = getAndRemoveRangingListener(msg.arg1); - if (listener == null) { - Log.e(TAG, "CALLBACK_RANGING_SUCCESS rangingId=" + msg.arg1 - + ": no listener registered (anymore)"); - } else { - listener.onFailure(msg.arg2, (String) msg.obj); - } - break; - } - case CALLBACK_RANGING_ABORTED: { - RttManager.RttListener listener = getAndRemoveRangingListener(msg.arg1); - if (listener == null) { - Log.e(TAG, "CALLBACK_RANGING_SUCCESS rangingId=" + msg.arg1 - + ": no listener registered (anymore)"); - } else { - listener.onAborted(); - } - break; - } } } }; @@ -629,43 +552,6 @@ public class WifiAwareManager { msg.obj = mac; mHandler.sendMessage(msg); } - - @Override - public void onRangingSuccess(int rangingId, RttManager.ParcelableRttResults results) { - if (VDBG) { - Log.v(TAG, "onRangingSuccess: rangingId=" + rangingId + ", results=" + results); - } - - Message msg = mHandler.obtainMessage(CALLBACK_RANGING_SUCCESS); - msg.arg1 = rangingId; - msg.obj = results; - mHandler.sendMessage(msg); - } - - @Override - public void onRangingFailure(int rangingId, int reason, String description) { - if (VDBG) { - Log.v(TAG, "onRangingSuccess: rangingId=" + rangingId + ", reason=" + reason - + ", description=" + description); - } - - Message msg = mHandler.obtainMessage(CALLBACK_RANGING_FAILURE); - msg.arg1 = rangingId; - msg.arg2 = reason; - msg.obj = description; - mHandler.sendMessage(msg); - - } - - @Override - public void onRangingAborted(int rangingId) { - if (VDBG) Log.v(TAG, "onRangingAborted: rangingId=" + rangingId); - - Message msg = mHandler.obtainMessage(CALLBACK_RANGING_ABORTED); - msg.arg1 = rangingId; - mHandler.sendMessage(msg); - - } } private static class WifiAwareDiscoverySessionCallbackProxy extends diff --git a/android/net/wifi/rtt/RangingRequest.java b/android/net/wifi/rtt/RangingRequest.java new file mode 100644 index 00000000..997b6800 --- /dev/null +++ b/android/net/wifi/rtt/RangingRequest.java @@ -0,0 +1,237 @@ +/* + * 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.wifi.rtt; + +import android.net.wifi.ScanResult; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +/** + * Defines the ranging request to other devices. The ranging request is built using + * {@link RangingRequest.Builder}. + * A ranging request is executed using + * {@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)}). + * + * @hide RTT_API + */ +public final class RangingRequest implements Parcelable { + private static final int MAX_PEERS = 10; + + /** + * 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)}. + * + * @return Maximum number of peers. + */ + public static int getMaxPeers() { + return MAX_PEERS; + } + + /** @hide */ + public final List<RttPeer> mRttPeers; + + /** @hide */ + private RangingRequest(List<RttPeer> rttPeers) { + mRttPeers = rttPeers; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeList(mRttPeers); + } + + public static final Creator<RangingRequest> CREATOR = new Creator<RangingRequest>() { + @Override + public RangingRequest[] newArray(int size) { + return new RangingRequest[size]; + } + + @Override + public RangingRequest createFromParcel(Parcel in) { + return new RangingRequest(in.readArrayList(null)); + } + }; + + /** @hide */ + @Override + public String toString() { + StringJoiner sj = new StringJoiner(", ", "RangingRequest: mRttPeers=[", ","); + for (RttPeer rp : mRttPeers) { + sj.add(rp.toString()); + } + return sj.toString(); + } + + /** @hide */ + public void enforceValidity() { + if (mRttPeers.size() > MAX_PEERS) { + throw new IllegalArgumentException( + "Ranging to too many peers requested. Use getMaxPeers() API to get limit."); + } + } + + /** + * Builder class used to construct {@link RangingRequest} objects. + */ + public static final class Builder { + private List<RttPeer> mRttPeers = new ArrayList<>(); + + /** + * Add the device specified by the {@link ScanResult} to the list of devices with + * which to measure range. The total number of results added to a request cannot exceed the + * limit specified by {@link #getMaxPeers()}. + * + * @param apInfo Information of an Access Point (AP) obtained in a Scan Result. + * @return The builder to facilitate chaining + * {@code builder.setXXX(..).setXXX(..)}. + */ + public Builder addAp(ScanResult apInfo) { + if (apInfo == null) { + throw new IllegalArgumentException("Null ScanResult!"); + } + mRttPeers.add(new RttPeerAp(apInfo)); + return this; + } + + /** + * Add the devices specified by the {@link ScanResult}s to the list of devices with + * which to measure range. The total number of results added to a request cannot exceed the + * limit specified by {@link #getMaxPeers()}. + * + * @param apInfos Information of an Access Points (APs) obtained in a Scan Result. + * @return The builder to facilitate chaining + * {@code builder.setXXX(..).setXXX(..)}. + */ + public Builder addAps(List<ScanResult> apInfos) { + if (apInfos == null) { + throw new IllegalArgumentException("Null list of ScanResults!"); + } + for (ScanResult scanResult : apInfos) { + addAp(scanResult); + } + return this; + } + + /** + * Build {@link RangingRequest} given the current configurations made on the + * builder. + */ + public RangingRequest build() { + return new RangingRequest(mRttPeers); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof RangingRequest)) { + return false; + } + + RangingRequest lhs = (RangingRequest) o; + + return mRttPeers.size() == lhs.mRttPeers.size() && mRttPeers.containsAll(lhs.mRttPeers); + } + + @Override + public int hashCode() { + return mRttPeers.hashCode(); + } + + /** @hide */ + public interface RttPeer { + // empty (marker interface) + } + + /** @hide */ + public static class RttPeerAp implements RttPeer, Parcelable { + public final ScanResult scanResult; + + public RttPeerAp(ScanResult scanResult) { + this.scanResult = scanResult; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + scanResult.writeToParcel(dest, flags); + } + + public static final Creator<RttPeerAp> CREATOR = new Creator<RttPeerAp>() { + @Override + public RttPeerAp[] newArray(int size) { + return new RttPeerAp[size]; + } + + @Override + public RttPeerAp createFromParcel(Parcel in) { + return new RttPeerAp(ScanResult.CREATOR.createFromParcel(in)); + } + }; + + @Override + public String toString() { + return new StringBuilder("RttPeerAp: scanResult=").append( + scanResult.toString()).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof RttPeerAp)) { + return false; + } + + RttPeerAp lhs = (RttPeerAp) o; + + // Note: the only thing which matters for the request identity is the BSSID of the AP + return TextUtils.equals(scanResult.BSSID, lhs.scanResult.BSSID); + } + + @Override + public int hashCode() { + return scanResult.hashCode(); + } + } +}
\ No newline at end of file diff --git a/android/net/wifi/rtt/RangingResult.java b/android/net/wifi/rtt/RangingResult.java new file mode 100644 index 00000000..918803ef --- /dev/null +++ b/android/net/wifi/rtt/RangingResult.java @@ -0,0 +1,194 @@ +/* + * 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.wifi.rtt; + +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import libcore.util.HexEncoding; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Ranging result for a request started by + * {@link WifiRttManager#startRanging(RangingRequest, RangingResultCallback, Handler)}. Results are + * returned in {@link RangingResultCallback#onRangingResults(List)}. + * <p> + * A ranging result is the distance measurement result for a single device specified in the + * {@link RangingRequest}. + * + * @hide RTT_API + */ +public final class RangingResult implements Parcelable { + private static final String TAG = "RangingResult"; + + private final int mStatus; + private final byte[] mMac; + private final int mDistanceCm; + private final int mDistanceStdDevCm; + private final int mRssi; + private final long mTimestamp; + + /** @hide */ + public RangingResult(int status, byte[] mac, int distanceCm, int distanceStdDevCm, int rssi, + long timestamp) { + mStatus = status; + mMac = mac; + mDistanceCm = distanceCm; + mDistanceStdDevCm = distanceStdDevCm; + 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. + */ + public int getStatus() { + return mStatus; + } + + /** + * @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. + */ + public byte[] getMacAddress() { + return mMac; + } + + /** + * @return The distance (in cm) to the device specified by {@link #getMacAddress()}. + * <p> + * Only valid if {@link #getStatus()} returns {@link RangingResultCallback#STATUS_SUCCESS}. + */ + public int getDistanceCm() { + if (mStatus != RangingResultCallback.STATUS_SUCCESS) { + Log.e(TAG, "getDistanceCm(): invalid value retrieved"); + } + return mDistanceCm; + } + + /** + * @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. + * <p> + * Only valid if {@link #getStatus()} returns {@link RangingResultCallback#STATUS_SUCCESS}. + */ + public int getDistanceStdDevCm() { + if (mStatus != RangingResultCallback.STATUS_SUCCESS) { + Log.e(TAG, "getDistanceStdDevCm(): invalid value retrieved"); + } + return mDistanceStdDevCm; + } + + /** + * @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}. + */ + public int getRssi() { + if (mStatus != RangingResultCallback.STATUS_SUCCESS) { + // TODO: should this be an exception? + Log.e(TAG, "getRssi(): invalid value retrieved"); + } + return mRssi; + } + + /** + * @return The timestamp (in us) at which the ranging operation was performed + * <p> + * Only valid if {@link #getStatus()} returns {@link RangingResultCallback#STATUS_SUCCESS}. + */ + public long getRangingTimestamp() { + return mTimestamp; + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mStatus); + dest.writeByteArray(mMac); + dest.writeInt(mDistanceCm); + dest.writeInt(mDistanceStdDevCm); + dest.writeInt(mRssi); + dest.writeLong(mTimestamp); + } + + /** @hide */ + public static final Creator<RangingResult> CREATOR = new Creator<RangingResult>() { + @Override + public RangingResult[] newArray(int size) { + return new RangingResult[size]; + } + + @Override + public RangingResult createFromParcel(Parcel in) { + int status = in.readInt(); + byte[] mac = in.createByteArray(); + int distanceCm = in.readInt(); + int distanceStdDevCm = in.readInt(); + int rssi = in.readInt(); + long timestamp = in.readLong(); + return new RangingResult(status, mac, distanceCm, distanceStdDevCm, rssi, timestamp); + } + }; + + /** @hide */ + @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( + mTimestamp).append("]").toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof RangingResult)) { + return false; + } + + 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; + } + + @Override + public int hashCode() { + return Objects.hash(mStatus, mMac, mDistanceCm, mDistanceStdDevCm, mRssi, mTimestamp); + } +}
\ No newline at end of file diff --git a/android/net/wifi/rtt/RangingResultCallback.java b/android/net/wifi/rtt/RangingResultCallback.java new file mode 100644 index 00000000..d7270ad2 --- /dev/null +++ b/android/net/wifi/rtt/RangingResultCallback.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.net.wifi.rtt; + +import android.os.Handler; + +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. + * + * @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; + + /** + * Individual range request status, {@link RangingResult#getStatus()}. Indicates ranging + * operation failed and the distance value is invalid. + */ + public static final int STATUS_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. + */ + public abstract void onRangingFailure(); + + /** + * Called when a ranging operation was executed. The list of results corresponds to devices + * specified in the ranging request. + * + * @param results List of range measurements, one per requested device. + */ + public abstract void onRangingResults(List<RangingResult> results); +} diff --git a/android/net/wifi/rtt/WifiRttManager.java b/android/net/wifi/rtt/WifiRttManager.java new file mode 100644 index 00000000..a085de17 --- /dev/null +++ b/android/net/wifi/rtt/WifiRttManager.java @@ -0,0 +1,100 @@ +package android.net.wifi.rtt; + +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_WIFI_STATE; +import static android.Manifest.permission.CHANGE_WIFI_STATE; + +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemService; +import android.content.Context; +import android.os.Binder; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; + +import java.util.List; + +/** + * This class provides the primary API for measuring distance (range) to other devices using the + * IEEE 802.11mc Wi-Fi Round Trip Time (RTT) technology. + * <p> + * The devices which can be ranged include: + * <li>Access Points (APs) + * <p> + * Ranging requests are triggered using + * {@link #startRanging(RangingRequest, RangingResultCallback, Handler)}. Results (in case of + * successful operation) are returned in the {@link RangingResultCallback#onRangingResults(List)} + * callback. + * + * @hide RTT_API + */ +@SystemService(Context.WIFI_RTT2_SERVICE) +public class WifiRttManager { + private static final String TAG = "WifiRttManager"; + private static final boolean VDBG = true; + + private final Context mContext; + private final IWifiRttManager mService; + + /** @hide */ + public WifiRttManager(Context context, IWifiRttManager service) { + mContext = context; + mService = service; + } + + /** + * Initiate a request to range to a set of devices specified in the {@link RangingRequest}. + * Results will be returned in the {@link RangingResultCallback} set of callbacks. + * + * @param request A request specifying a set of devices whose distance measurements are + * requested. + * @param callback A callback for the result of the ranging request. + * @param handler The Handler on whose thread to execute the callbacks of the {@code + * callback} object. If a null is provided then the application's main thread + * will be used. + */ + @RequiresPermission(allOf = {ACCESS_COARSE_LOCATION, CHANGE_WIFI_STATE, ACCESS_WIFI_STATE}) + public void startRanging(RangingRequest request, RangingResultCallback callback, + @Nullable Handler handler) { + if (VDBG) { + Log.v(TAG, "startRanging: request=" + request + ", callback=" + callback + ", handler=" + + handler); + } + + Looper looper = (handler == null) ? Looper.getMainLooper() : handler.getLooper(); + Binder binder = new Binder(); + try { + mService.startRanging(binder, mContext.getOpPackageName(), request, + new RttCallbackProxy(looper, callback)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private static class RttCallbackProxy extends IRttCallback.Stub { + private final Handler mHandler; + private final RangingResultCallback mCallback; + + RttCallbackProxy(Looper looper, RangingResultCallback callback) { + mHandler = new Handler(looper); + mCallback = callback; + } + + @Override + public void onRangingResults(int status, List<RangingResult> results) throws RemoteException { + if (VDBG) { + Log.v(TAG, "RttCallbackProxy: onRanginResults: status=" + status + ", results=" + + results); + } + mHandler.post(() -> { + if (status == RangingResultCallback.STATUS_SUCCESS) { + mCallback.onRangingResults(results); + } else { + mCallback.onRangingFailure(); + } + }); + } + } +} diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java index 66b6b478..98819279 100644 --- a/android/os/BatteryStats.java +++ b/android/os/BatteryStats.java @@ -19,6 +19,7 @@ package android.os; import android.app.job.JobParameters; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.service.batterystats.BatteryStatsServiceDumpProto; import android.telephony.SignalStrength; import android.text.format.DateFormat; import android.util.ArrayMap; @@ -29,11 +30,13 @@ import android.util.Printer; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TimeUtils; +import android.util.proto.ProtoOutputStream; import android.view.Display; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; +import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -78,17 +81,17 @@ public abstract class BatteryStats implements Parcelable { * A constant indicating a sensor timer. */ public static final int SENSOR = 3; - + /** * A constant indicating a a wifi running timer */ public static final int WIFI_RUNNING = 4; - + /** * A constant indicating a full wifi lock timer */ public static final int FULL_WIFI_LOCK = 5; - + /** * A constant indicating a wifi scan */ @@ -190,7 +193,7 @@ public abstract class BatteryStats implements Parcelable { public static final int STATS_SINCE_UNPLUGGED = 2; // NOTE: Update this list if you add/change any stats above. - // These characters are supposed to represent "total", "last", "current", + // These characters are supposed to represent "total", "last", "current", // and "unplugged". They were shortened for efficiency sake. private static final String[] STAT_NAMES = { "l", "c", "u" }; @@ -217,8 +220,10 @@ public abstract class BatteryStats implements Parcelable { * - Package wakeup alarms are now on screen-off timebase * New in version 26: * - Resource power manager (rpm) states [but screenOffRpm is disabled from working properly] + * New in version 27: + * - Always On Display (screen doze mode) time and power */ - static final String CHECKIN_VERSION = "26"; + static final int CHECKIN_VERSION = 27; /** * Old version, we hit 9 and ran out of room, need to remove. @@ -1381,12 +1386,12 @@ public abstract class BatteryStats implements Parcelable { public static final int STATE_PHONE_SCANNING_FLAG = 1<<21; public static final int STATE_SCREEN_ON_FLAG = 1<<20; // consider moving to states2 public static final int STATE_BATTERY_PLUGGED_FLAG = 1<<19; // consider moving to states2 - // empty slot + public static final int STATE_SCREEN_DOZE_FLAG = 1 << 18; // empty slot public static final int STATE_WIFI_MULTICAST_ON_FLAG = 1<<16; public static final int MOST_INTERESTING_STATES = - STATE_BATTERY_PLUGGED_FLAG | STATE_SCREEN_ON_FLAG; + STATE_BATTERY_PLUGGED_FLAG | STATE_SCREEN_ON_FLAG | STATE_SCREEN_DOZE_FLAG; public static final int SETTLE_TO_ZERO_STATES = 0xffff0000 & ~MOST_INTERESTING_STATES; @@ -1414,8 +1419,8 @@ public abstract class BatteryStats implements Parcelable { public static final int STATE2_BLUETOOTH_SCAN_FLAG = 1 << 20; public static final int MOST_INTERESTING_STATES2 = - STATE2_POWER_SAVE_FLAG | STATE2_WIFI_ON_FLAG | STATE2_DEVICE_IDLE_MASK - | STATE2_CHARGING_FLAG | STATE2_PHONE_IN_CALL_FLAG | STATE2_BLUETOOTH_ON_FLAG; + STATE2_POWER_SAVE_FLAG | STATE2_WIFI_ON_FLAG | STATE2_DEVICE_IDLE_MASK + | STATE2_CHARGING_FLAG | STATE2_PHONE_IN_CALL_FLAG | STATE2_BLUETOOTH_ON_FLAG; public static final int SETTLE_TO_ZERO_STATES2 = 0xffff0000 & ~MOST_INTERESTING_STATES2; @@ -1863,6 +1868,21 @@ public abstract class BatteryStats implements Parcelable { */ public abstract int getScreenOnCount(int which); + /** + * Returns the time in microseconds that the screen has been dozing while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getScreenDozeTime(long elapsedRealtimeUs, int which); + + /** + * Returns the number of times the screen was turned dozing. + * + * {@hide} + */ + public abstract int getScreenDozeCount(int which); + public abstract long getInteractiveTime(long elapsedRealtimeUs, int which); public static final int SCREEN_BRIGHTNESS_DARK = 0; @@ -2116,8 +2136,7 @@ public abstract class BatteryStats implements Parcelable { "group", "compl", "dorm", "uninit" }; - public static final BitDescription[] HISTORY_STATE_DESCRIPTIONS - = new BitDescription[] { + public static final BitDescription[] HISTORY_STATE_DESCRIPTIONS = new BitDescription[] { new BitDescription(HistoryItem.STATE_CPU_RUNNING_FLAG, "running", "r"), new BitDescription(HistoryItem.STATE_WAKE_LOCK_FLAG, "wake_lock", "w"), new BitDescription(HistoryItem.STATE_SENSOR_ON_FLAG, "sensor", "s"), @@ -2131,6 +2150,7 @@ public abstract class BatteryStats implements Parcelable { new BitDescription(HistoryItem.STATE_AUDIO_ON_FLAG, "audio", "a"), new BitDescription(HistoryItem.STATE_SCREEN_ON_FLAG, "screen", "S"), new BitDescription(HistoryItem.STATE_BATTERY_PLUGGED_FLAG, "plugged", "BP"), + new BitDescription(HistoryItem.STATE_SCREEN_DOZE_FLAG, "screen_doze", "Sd"), new BitDescription(HistoryItem.STATE_DATA_CONNECTION_MASK, HistoryItem.STATE_DATA_CONNECTION_SHIFT, "data_conn", "Pcn", DATA_CONNECTION_NAMES, DATA_CONNECTION_NAMES), @@ -2467,6 +2487,18 @@ public abstract class BatteryStats implements Parcelable { public abstract int getDischargeAmountScreenOffSinceCharge(); /** + * Get the amount the battery has discharged while the screen was doze, + * since the last time power was unplugged. + */ + public abstract int getDischargeAmountScreenDoze(); + + /** + * Get the amount the battery has discharged while the screen was doze, + * since the last time the device was charged. + */ + public abstract int getDischargeAmountScreenDozeSinceCharge(); + + /** * Returns the total, last, or current battery uptime in microseconds. * * @param curTime the elapsed realtime in microseconds. @@ -2483,7 +2515,7 @@ public abstract class BatteryStats implements Parcelable { public abstract long computeBatteryRealtime(long curTime, int which); /** - * Returns the total, last, or current battery screen off uptime in microseconds. + * Returns the total, last, or current battery screen off/doze uptime in microseconds. * * @param curTime the elapsed realtime in microseconds. * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. @@ -2491,7 +2523,7 @@ public abstract class BatteryStats implements Parcelable { public abstract long computeBatteryScreenOffUptime(long curTime, int which); /** - * Returns the total, last, or current battery screen off realtime in microseconds. + * Returns the total, last, or current battery screen off/doze realtime in microseconds. * * @param curTime the current elapsed realtime in microseconds. * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. @@ -2590,18 +2622,24 @@ public abstract class BatteryStats implements Parcelable { }; /** - * Return the counter keeping track of the amount of battery discharge while the screen was off, - * measured in micro-Ampere-hours. This will be non-zero only if the device's battery has + * Return the amount of battery discharge while the screen was off, measured in + * micro-Ampere-hours. This will be non-zero only if the device's battery has * a coulomb counter. */ - public abstract LongCounter getDischargeScreenOffCoulombCounter(); + public abstract long getMahDischargeScreenOff(int which); /** - * Return the counter keeping track of the amount of battery discharge measured in + * 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 LongCounter getDischargeCoulombCounter(); + public abstract long getMahDischargeScreenDoze(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); /** * Returns the estimated real battery capacity, which may be less than the capacity @@ -2953,6 +2991,31 @@ public abstract class BatteryStats implements Parcelable { } /** + * Dump a given timer stat to the proto stream. + * + * @param proto the ProtoOutputStream to log to + * @param fieldId type of data, the field to save to (e.g. AggregatedBatteryStats.WAKELOCK) + * @param timer a {@link Timer} to dump stats for + * @param rawRealtimeUs the current elapsed realtime of the system in microseconds + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT + */ + private static void dumpTimer(ProtoOutputStream proto, long fieldId, + Timer timer, long rawRealtime, int which) { + if (timer == null) { + return; + } + // Convert from microseconds to milliseconds with rounding + final long totalTimeMs = (timer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; + final int count = timer.getCountLocked(which); + if (totalTimeMs != 0 || count != 0) { + final long token = proto.start(fieldId); + proto.write(TimerProto.DURATION_MS, totalTimeMs); + proto.write(TimerProto.COUNT, count); + proto.end(token); + } + } + + /** * Checks if the ControllerActivityCounter has any data worth dumping. */ private static boolean controllerActivityHasData(ControllerActivityCounter counter, int which) { @@ -3004,6 +3067,38 @@ public abstract class BatteryStats implements Parcelable { pw.println(); } + /** + * Dumps the ControllerActivityCounter if it has any data worth dumping. + */ + private static void dumpControllerActivityProto(ProtoOutputStream proto, long fieldId, + ControllerActivityCounter counter, + int which) { + if (!controllerActivityHasData(counter, which)) { + return; + } + + final long cToken = proto.start(fieldId); + + proto.write(ControllerActivityProto.IDLE_DURATION_MS, + counter.getIdleTimeCounter().getCountLocked(which)); + proto.write(ControllerActivityProto.RX_DURATION_MS, + counter.getRxTimeCounter().getCountLocked(which)); + proto.write(ControllerActivityProto.POWER_MAH, + counter.getPowerCounter().getCountLocked(which) / (1000 * 60 * 60)); + + long tToken; + LongCounter[] txCounters = counter.getTxTimeCounters(); + for (int i = 0; i < txCounters.length; ++i) { + LongCounter c = txCounters[i]; + tToken = proto.start(ControllerActivityProto.TX); + proto.write(ControllerActivityProto.TxLevel.LEVEL, i); + proto.write(ControllerActivityProto.TxLevel.DURATION_MS, c.getCountLocked(which)); + proto.end(tToken); + } + + proto.end(cToken); + } + private final void printControllerActivityIfInteresting(PrintWriter pw, StringBuilder sb, String prefix, String controllerName, ControllerActivityCounter counter, @@ -3112,6 +3207,7 @@ public abstract class BatteryStats implements Parcelable { final long totalRealtime = computeRealtime(rawRealtime, which); final long totalUptime = computeUptime(rawUptime, which); final long screenOnTime = getScreenOnTime(rawRealtime, which); + final long screenDozeTime = getScreenDozeTime(rawRealtime, which); final long interactiveTime = getInteractiveTime(rawRealtime, which); final long powerSaveModeEnabledTime = getPowerSaveModeEnabledTime(rawRealtime, which); final long deviceIdleModeLightTime = getDeviceIdleModeTime(DEVICE_IDLE_MODE_LIGHT, @@ -3124,9 +3220,9 @@ public abstract class BatteryStats implements Parcelable { rawRealtime, which); final int connChanges = getNumConnectivityChange(which); final long phoneOnTime = getPhoneOnTime(rawRealtime, which); - final long dischargeCount = getDischargeCoulombCounter().getCountLocked(which); - final long dischargeScreenOffCount = getDischargeScreenOffCoulombCounter() - .getCountLocked(which); + final long dischargeCount = getMahDischarge(which); + final long dischargeScreenOffCount = getMahDischargeScreenOff(which); + final long dischargeScreenDozeCount = getMahDischargeScreenDoze(which); final StringBuilder sb = new StringBuilder(128); @@ -3143,7 +3239,8 @@ public abstract class BatteryStats implements Parcelable { getStartClockTime(), whichBatteryScreenOffRealtime / 1000, whichBatteryScreenOffUptime / 1000, getEstimatedBatteryCapacity(), - getMinLearnedBatteryCapacity(), getMaxLearnedBatteryCapacity()); + getMinLearnedBatteryCapacity(), getMaxLearnedBatteryCapacity(), + screenDozeTime / 1000); // Calculate wakelock times across all uids. @@ -3295,13 +3392,15 @@ public abstract class BatteryStats implements Parcelable { getDischargeStartLevel()-getDischargeCurrentLevel(), getDischargeStartLevel()-getDischargeCurrentLevel(), getDischargeAmountScreenOn(), getDischargeAmountScreenOff(), - dischargeCount / 1000, dischargeScreenOffCount / 1000); + dischargeCount / 1000, dischargeScreenOffCount / 1000, + getDischargeAmountScreenDoze(), dischargeScreenDozeCount / 1000); } else { dumpLine(pw, 0 /* uid */, category, BATTERY_DISCHARGE_DATA, getLowDischargeAmountSinceCharge(), getHighDischargeAmountSinceCharge(), getDischargeAmountScreenOnSinceCharge(), getDischargeAmountScreenOffSinceCharge(), - dischargeCount / 1000, dischargeScreenOffCount / 1000); + dischargeCount / 1000, dischargeScreenOffCount / 1000, + getDischargeAmountScreenDozeSinceCharge(), dischargeScreenDozeCount / 1000); } if (reqUid < 0) { @@ -3831,6 +3930,7 @@ public abstract class BatteryStats implements Parcelable { which); final long batteryTimeRemaining = computeBatteryTimeRemaining(rawRealtime); final long chargeTimeRemaining = computeChargeTimeRemaining(rawRealtime); + final long screenDozeTime = getScreenDozeTime(rawRealtime, which); final StringBuilder sb = new StringBuilder(128); @@ -3868,25 +3968,35 @@ public abstract class BatteryStats implements Parcelable { sb.setLength(0); sb.append(prefix); - sb.append(" Time on battery: "); - formatTimeMs(sb, whichBatteryRealtime / 1000); sb.append("("); - sb.append(formatRatioLocked(whichBatteryRealtime, totalRealtime)); - sb.append(") realtime, "); - formatTimeMs(sb, whichBatteryUptime / 1000); - sb.append("("); sb.append(formatRatioLocked(whichBatteryUptime, totalRealtime)); - sb.append(") uptime"); + sb.append(" Time on battery: "); + formatTimeMs(sb, whichBatteryRealtime / 1000); sb.append("("); + sb.append(formatRatioLocked(whichBatteryRealtime, totalRealtime)); + sb.append(") realtime, "); + formatTimeMs(sb, whichBatteryUptime / 1000); + sb.append("("); sb.append(formatRatioLocked(whichBatteryUptime, whichBatteryRealtime)); + sb.append(") uptime"); pw.println(sb.toString()); + sb.setLength(0); sb.append(prefix); - sb.append(" Time on battery screen off: "); - formatTimeMs(sb, whichBatteryScreenOffRealtime / 1000); sb.append("("); - sb.append(formatRatioLocked(whichBatteryScreenOffRealtime, totalRealtime)); - sb.append(") realtime, "); - formatTimeMs(sb, whichBatteryScreenOffUptime / 1000); - sb.append("("); - sb.append(formatRatioLocked(whichBatteryScreenOffUptime, totalRealtime)); - sb.append(") uptime"); + sb.append(" Time on battery screen off: "); + formatTimeMs(sb, whichBatteryScreenOffRealtime / 1000); sb.append("("); + sb.append(formatRatioLocked(whichBatteryScreenOffRealtime, whichBatteryRealtime)); + sb.append(") realtime, "); + formatTimeMs(sb, whichBatteryScreenOffUptime / 1000); + sb.append("("); + sb.append(formatRatioLocked(whichBatteryScreenOffUptime, whichBatteryRealtime)); + sb.append(") uptime"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Time on battery screen doze: "); + formatTimeMs(sb, screenDozeTime / 1000); sb.append("("); + sb.append(formatRatioLocked(screenDozeTime, whichBatteryRealtime)); + sb.append(")"); pw.println(sb.toString()); + sb.setLength(0); sb.append(prefix); sb.append(" Total run time: "); @@ -3910,8 +4020,7 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } - final LongCounter dischargeCounter = getDischargeCoulombCounter(); - final long dischargeCount = dischargeCounter.getCountLocked(which); + final long dischargeCount = getMahDischarge(which); if (dischargeCount >= 0) { sb.setLength(0); sb.append(prefix); @@ -3921,8 +4030,7 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } - final LongCounter dischargeScreenOffCounter = getDischargeScreenOffCoulombCounter(); - final long dischargeScreenOffCount = dischargeScreenOffCounter.getCountLocked(which); + final long dischargeScreenOffCount = getMahDischargeScreenOff(which); if (dischargeScreenOffCount >= 0) { sb.setLength(0); sb.append(prefix); @@ -3932,7 +4040,18 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } - final long dischargeScreenOnCount = dischargeCount - dischargeScreenOffCount; + final long dischargeScreenDozeCount = getMahDischargeScreenDoze(which); + if (dischargeScreenDozeCount >= 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Screen doze discharge: "); + sb.append(BatteryStatsHelper.makemAh(dischargeScreenDozeCount / 1000.0)); + sb.append(" mAh"); + pw.println(sb.toString()); + } + + final long dischargeScreenOnCount = + dischargeCount - dischargeScreenOffCount - dischargeScreenDozeCount; if (dischargeScreenOnCount >= 0) { sb.setLength(0); sb.append(prefix); @@ -4340,20 +4459,24 @@ public abstract class BatteryStats implements Parcelable { pw.println(getDischargeCurrentLevel()); } pw.print(prefix); pw.print(" Amount discharged while screen on: "); - pw.println(getDischargeAmountScreenOn()); + pw.println(getDischargeAmountScreenOn()); pw.print(prefix); pw.print(" Amount discharged while screen off: "); - pw.println(getDischargeAmountScreenOff()); + pw.println(getDischargeAmountScreenOff()); + pw.print(prefix); pw.print(" Amount discharged while screen doze: "); + pw.println(getDischargeAmountScreenDoze()); pw.println(" "); } else { pw.print(prefix); pw.println(" Device battery use since last full charge"); pw.print(prefix); pw.print(" Amount discharged (lower bound): "); - pw.println(getLowDischargeAmountSinceCharge()); + pw.println(getLowDischargeAmountSinceCharge()); pw.print(prefix); pw.print(" Amount discharged (upper bound): "); - pw.println(getHighDischargeAmountSinceCharge()); + pw.println(getHighDischargeAmountSinceCharge()); pw.print(prefix); pw.print(" Amount discharged while screen on: "); - pw.println(getDischargeAmountScreenOnSinceCharge()); + pw.println(getDischargeAmountScreenOnSinceCharge()); pw.print(prefix); pw.print(" Amount discharged while screen off: "); - pw.println(getDischargeAmountScreenOffSinceCharge()); + pw.println(getDischargeAmountScreenOffSinceCharge()); + pw.print(prefix); pw.print(" Amount discharged while screen doze: "); + pw.println(getDischargeAmountScreenDozeSinceCharge()); pw.println(); } @@ -5426,8 +5549,9 @@ public abstract class BatteryStats implements Parcelable { } } } - + public void prepareForDumpLocked() { + // We don't need to require subclasses implement this. } public static class HistoryPrinter { @@ -6248,7 +6372,8 @@ public abstract class BatteryStats implements Parcelable { pw.println(); } } - + + // This is called from BatteryStatsService. @SuppressWarnings("unused") public void dumpCheckinLocked(Context context, PrintWriter pw, List<ApplicationInfo> apps, int flags, long histStart) { @@ -6260,10 +6385,7 @@ public abstract class BatteryStats implements Parcelable { long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); - final boolean filtering = (flags & - (DUMP_HISTORY_ONLY|DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) != 0; - - if ((flags&DUMP_INCLUDE_HISTORY) != 0 || (flags&DUMP_HISTORY_ONLY) != 0) { + if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) { if (startIteratingHistoryLocked()) { try { for (int i=0; i<getHistoryStringPoolSize(); i++) { @@ -6287,7 +6409,7 @@ public abstract class BatteryStats implements Parcelable { } } - if (filtering && (flags&(DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) == 0) { + if ((flags & DUMP_HISTORY_ONLY) != 0) { return; } @@ -6320,7 +6442,7 @@ public abstract class BatteryStats implements Parcelable { } } } - if (!filtering || (flags&DUMP_CHARGED_ONLY) != 0) { + if ((flags & DUMP_DAILY_ONLY) == 0) { dumpDurationSteps(pw, "", DISCHARGE_STEP_DATA, getDischargeLevelStepTracker(), true); String[] lineArgs = new String[1]; long timeRemaining = computeBatteryTimeRemaining(SystemClock.elapsedRealtime() * 1000); @@ -6340,4 +6462,33 @@ public abstract class BatteryStats implements Parcelable { (flags&DUMP_DEVICE_WIFI_ONLY) != 0); } } + + /** Dump 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); + final long bToken = proto.start(BatteryStatsServiceDumpProto.BATTERYSTATS); + prepareForDumpLocked(); + + proto.write(BatteryStatsProto.REPORT_VERSION, CHECKIN_VERSION); + proto.write(BatteryStatsProto.PARCEL_VERSION, getParcelVersion()); + proto.write(BatteryStatsProto.START_PLATFORM_VERSION, getStartPlatformVersion()); + proto.write(BatteryStatsProto.END_PLATFORM_VERSION, getEndPlatformVersion()); + + long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); + + if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) { + if (startIteratingHistoryLocked()) { + // TODO: implement dumpProtoHistoryLocked(proto); + } + } + + if ((flags & (DUMP_HISTORY_ONLY | DUMP_DAILY_ONLY)) == 0) { + // TODO: implement dumpProtoAppsLocked(proto, apps); + // TODO: implement dumpProtoSystemLocked(proto); + } + + proto.end(bToken); + proto.flush(); + } } diff --git a/android/os/Binder.java b/android/os/Binder.java index 0df6361d..e9e695bb 100644 --- a/android/os/Binder.java +++ b/android/os/Binder.java @@ -23,7 +23,6 @@ import android.util.Log; import android.util.Slog; import com.android.internal.util.FastPrintWriter; -import com.android.internal.util.FunctionalUtils; import com.android.internal.util.FunctionalUtils.ThrowingRunnable; import com.android.internal.util.FunctionalUtils.ThrowingSupplier; @@ -202,7 +201,7 @@ public class Binder implements IBinder { * then its own pid is returned. */ public static final native int getCallingPid(); - + /** * Return the Linux uid assigned to the process that sent you the * current transaction that is being processed. This uid can be used with @@ -335,7 +334,7 @@ public class Binder implements IBinder { * it needs to. */ public static final native void flushPendingCommands(); - + /** * Add the calling thread to the IPC thread pool. This function does * not return until the current process is exiting. @@ -372,7 +371,7 @@ public class Binder implements IBinder { } } } - + /** * Convenience method for associating a specific interface with the Binder. * After calling, queryLocalInterface() will be implemented for you @@ -383,7 +382,7 @@ public class Binder implements IBinder { mOwner = owner; mDescriptor = descriptor; } - + /** * Default implementation returns an empty interface name. */ @@ -408,7 +407,7 @@ public class Binder implements IBinder { public boolean isBinderAlive() { return true; } - + /** * Use information supplied to attachInterface() to return the * associated IInterface if it matches the requested @@ -630,7 +629,7 @@ public class Binder implements IBinder { } return r; } - + /** * Local implementation is a no-op. */ @@ -643,7 +642,7 @@ public class Binder implements IBinder { public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { return true; } - + protected void finalize() throws Throwable { try { destroyBinder(); @@ -730,7 +729,15 @@ public class Binder implements IBinder { } } +/** + * Java proxy for a native IBinder object. + * Allocated and constructed by the native javaObjectforIBinder function. Never allocated + * directly from Java code. + */ final class BinderProxy implements IBinder { + // See android_util_Binder.cpp for the native half of this. + // TODO: Consider using NativeAllocationRegistry instead of finalization. + // Assume the process-wide default value when created volatile boolean mWarnOnBlocking = Binder.sWarnOnBlocking; @@ -789,7 +796,7 @@ final class BinderProxy implements IBinder { reply.recycle(); } } - + public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -826,7 +833,7 @@ final class BinderProxy implements IBinder { BinderProxy() { mSelf = new WeakReference(this); } - + @Override protected void finalize() throws Throwable { try { @@ -835,9 +842,9 @@ final class BinderProxy implements IBinder { super.finalize(); } } - + private native final void destroy(); - + private static final void sendDeathNotice(DeathRecipient recipient) { if (false) Log.v("JavaBinder", "sendDeathNotice to " + recipient); try { @@ -848,8 +855,20 @@ final class BinderProxy implements IBinder { exc); } } - + + // This WeakReference to "this" is used only by native code to "attach" to the + // native IBinder object. + // Using WeakGlobalRefs instead currently appears unsafe, in that they can yield a + // non-null value after the BinderProxy is enqueued for finalization. + // Used only once immediately after construction. + // TODO: Consider making the extra native-to-java call to compute this on the fly. final private WeakReference mSelf; + + // Native pointer to the wrapped native IBinder object. Counted as strong reference. private long mObject; + + // Native pointer to native DeathRecipientList. Counted as strong reference. + // Basically owned by the JavaProxy object. Reference counted only because DeathRecipients + // hold a weak reference that can be temporarily promoted. private long mOrgue; } diff --git a/android/os/IServiceManager.java b/android/os/IServiceManager.java index 7b11c283..87c65ecc 100644 --- a/android/os/IServiceManager.java +++ b/android/os/IServiceManager.java @@ -18,12 +18,12 @@ package android.os; /** * Basic interface for finding and publishing system services. - * + * * An implementation of this interface is usually published as the * global context object, which can be retrieved via * BinderNative.getContextObject(). An easy way to retrieve this * is with the static method BnServiceManager.getDefault(). - * + * * @hide */ public interface IServiceManager extends IInterface @@ -33,33 +33,33 @@ public interface IServiceManager extends IInterface * service manager. Blocks for a few seconds waiting for it to be * published if it does not already exist. */ - public IBinder getService(String name) throws RemoteException; - + IBinder getService(String name) throws RemoteException; + /** * Retrieve an existing service called @a name from the * service manager. Non-blocking. */ - public IBinder checkService(String name) throws RemoteException; + IBinder checkService(String name) throws RemoteException; /** * Place a new @a service called @a name into the service * manager. */ - public void addService(String name, IBinder service, boolean allowIsolated) + void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority) throws RemoteException; /** * Return a list of all currently running services. */ - public String[] listServices() throws RemoteException; + String[] listServices(int dumpPriority) throws RemoteException; /** * Assign a permission controller to the service manager. After set, this * interface is checked before any services are added. */ - public void setPermissionController(IPermissionController controller) + void setPermissionController(IPermissionController controller) throws RemoteException; - + static final String descriptor = "android.os.IServiceManager"; int GET_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; @@ -68,4 +68,13 @@ public interface IServiceManager extends IInterface int LIST_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3; int CHECK_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4; int SET_PERMISSION_CONTROLLER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+5; + + /* + * Must update values in IServiceManager.h + */ + int DUMP_PRIORITY_CRITICAL = 1 << 0; + int DUMP_PRIORITY_HIGH = 1 << 1; + int DUMP_PRIORITY_NORMAL = 1 << 2; + int DUMP_PRIORITY_ALL = DUMP_PRIORITY_CRITICAL | DUMP_PRIORITY_HIGH + | DUMP_PRIORITY_NORMAL; } diff --git a/android/os/Parcel.java b/android/os/Parcel.java index 031ca91c..857e8a60 100644 --- a/android/os/Parcel.java +++ b/android/os/Parcel.java @@ -1340,6 +1340,13 @@ public final class Parcel { * @see Parcelable */ public final <T extends Parcelable> void writeTypedList(List<T> val) { + writeTypedList(val, 0); + } + + /** + * @hide + */ + public <T extends Parcelable> void writeTypedList(List<T> val, int parcelableFlags) { if (val == null) { writeInt(-1); return; @@ -1348,13 +1355,7 @@ public final class Parcel { int i=0; writeInt(N); while (i < N) { - T item = val.get(i); - if (item != null) { - writeInt(1); - item.writeToParcel(this, 0); - } else { - writeInt(0); - } + writeTypedObject(val.get(i), parcelableFlags); i++; } } @@ -1456,116 +1457,7 @@ public final class Parcel { int N = val.length; writeInt(N); for (int i = 0; i < N; i++) { - T item = val[i]; - if (item != null) { - writeInt(1); - item.writeToParcel(this, parcelableFlags); - } else { - writeInt(0); - } - } - } else { - writeInt(-1); - } - } - - /** - * Write a uniform (all items are null or the same class) array list of - * parcelables. - * - * @param list The list to write. - * - * @hide - */ - public final <T extends Parcelable> void writeTypedArrayList(@Nullable ArrayList<T> list, - int parcelableFlags) { - if (list != null) { - int N = list.size(); - writeInt(N); - boolean wroteCreator = false; - for (int i = 0; i < N; i++) { - T item = list.get(i); - if (item != null) { - writeInt(1); - if (!wroteCreator) { - writeParcelableCreator(item); - wroteCreator = true; - } - item.writeToParcel(this, parcelableFlags); - } else { - writeInt(0); - } - } - } else { - writeInt(-1); - } - } - - /** - * Reads a uniform (all items are null or the same class) array list of - * parcelables. - * - * @return The list or null. - * - * @hide - */ - public final @Nullable <T> ArrayList<T> readTypedArrayList(@Nullable ClassLoader loader) { - int N = readInt(); - if (N <= 0) { - return null; - } - Parcelable.Creator<?> creator = null; - ArrayList<T> result = new ArrayList<T>(N); - for (int i = 0; i < N; i++) { - if (readInt() != 0) { - if (creator == null) { - creator = readParcelableCreator(loader); - if (creator == null) { - return null; - } - } - final T parcelable; - if (creator instanceof Parcelable.ClassLoaderCreator<?>) { - Parcelable.ClassLoaderCreator<?> classLoaderCreator = - (Parcelable.ClassLoaderCreator<?>) creator; - parcelable = (T) classLoaderCreator.createFromParcel(this, loader); - } else { - parcelable = (T) creator.createFromParcel(this); - } - result.add(parcelable); - } else { - result.add(null); - } - } - return result; - } - - /** - * Write a uniform (all items are null or the same class) array set of - * parcelables. - * - * @param set The set to write. - * - * @hide - */ - public final <T extends Parcelable> void writeTypedArraySet(@Nullable ArraySet<T> set, - int parcelableFlags) { - if (set != null) { - int N = set.size(); - writeInt(N); - boolean wroteCreator = false; - for (int i = 0; i < N; i++) { - T item = set.valueAt(i); - if (item != null) { - writeInt(1); - if (!wroteCreator) { - writeParcelableCreator(item); - wroteCreator = true; - } - item.writeToParcel(this, parcelableFlags); - } else { - writeInt(0); - } + writeTypedObject(val[i], parcelableFlags); } } else { writeInt(-1); @@ -1573,43 +1465,6 @@ public final class Parcel { } /** - * Reads a uniform (all items are null or the same class) array set of - * parcelables. - * - * @return The set or null. - * - * @hide - */ - public final @Nullable <T> ArraySet<T> readTypedArraySet(@Nullable ClassLoader loader) { - int N = readInt(); - if (N <= 0) { - return null; - } - Parcelable.Creator<?> creator = null; - ArraySet<T> result = new ArraySet<T>(N); - for (int i = 0; i < N; i++) { - T parcelable = null; - if (readInt() != 0) { - if (creator == null) { - creator = readParcelableCreator(loader); - if (creator == null) { - return null; - } - } - if (creator instanceof Parcelable.ClassLoaderCreator<?>) { - Parcelable.ClassLoaderCreator<?> classLoaderCreator = - (Parcelable.ClassLoaderCreator<?>) creator; - parcelable = (T) classLoaderCreator.createFromParcel(this, loader); - } else { - parcelable = (T) creator.createFromParcel(this); - } - } - result.append(parcelable); - } - return result; - } - - /** * Flatten the Parcelable object into the parcel. * * @param val The Parcelable object to be written. @@ -2458,11 +2313,7 @@ public final class Parcel { } ArrayList<T> l = new ArrayList<T>(N); while (N > 0) { - if (readInt() != 0) { - l.add(c.createFromParcel(this)); - } else { - l.add(null); - } + l.add(readTypedObject(c)); N--; } return l; @@ -2485,18 +2336,10 @@ public final class Parcel { int N = readInt(); int i = 0; for (; i < M && i < N; i++) { - if (readInt() != 0) { - list.set(i, c.createFromParcel(this)); - } else { - list.set(i, null); - } + list.set(i, readTypedObject(c)); } for (; i<N; i++) { - if (readInt() != 0) { - list.add(c.createFromParcel(this)); - } else { - list.add(null); - } + list.add(readTypedObject(c)); } for (; i<M; i++) { list.remove(N); @@ -2641,9 +2484,7 @@ public final class Parcel { } T[] l = c.newArray(N); for (int i=0; i<N; i++) { - if (readInt() != 0) { - l[i] = c.createFromParcel(this); - } + l[i] = readTypedObject(c); } return l; } @@ -2652,11 +2493,7 @@ public final class Parcel { int N = readInt(); if (N == val.length) { for (int i=0; i<N; i++) { - if (readInt() != 0) { - val[i] = c.createFromParcel(this); - } else { - val[i] = null; - } + val[i] = readTypedObject(c); } } else { throw new RuntimeException("bad array lengths"); diff --git a/android/os/ParcelableException.java b/android/os/ParcelableException.java index d84d6299..7f71905d 100644 --- a/android/os/ParcelableException.java +++ b/android/os/ParcelableException.java @@ -52,10 +52,12 @@ public final class ParcelableException extends RuntimeException implements Parce final String msg = in.readString(); try { final Class<?> clazz = Class.forName(name, true, Parcelable.class.getClassLoader()); - return (Throwable) clazz.getConstructor(String.class).newInstance(msg); + if (Throwable.class.isAssignableFrom(clazz)) { + return (Throwable) clazz.getConstructor(String.class).newInstance(msg); + } } catch (ReflectiveOperationException e) { - throw new RuntimeException(name + ": " + msg); } + return new RuntimeException(name + ": " + msg); } /** {@hide} */ diff --git a/android/os/PowerManager.java b/android/os/PowerManager.java index 960c9f5c..7f4dee6e 100644 --- a/android/os/PowerManager.java +++ b/android/os/PowerManager.java @@ -443,6 +443,20 @@ public final class PowerManager { public static final String SHUTDOWN_USER_REQUESTED = "userrequested"; /** + * The value to pass as the 'reason' argument to android_reboot() when battery temperature + * is too high. + * @hide + */ + public static final String SHUTDOWN_BATTERY_THERMAL_STATE = "thermal,battery"; + + /** + * The value to pass as the 'reason' argument to android_reboot() when device is running + * critically low on battery. + * @hide + */ + public static final String SHUTDOWN_LOW_BATTERY = "battery"; + + /** * @hide */ @Retention(RetentionPolicy.SOURCE) @@ -451,7 +465,9 @@ public final class PowerManager { SHUTDOWN_REASON_SHUTDOWN, SHUTDOWN_REASON_REBOOT, SHUTDOWN_REASON_USER_REQUESTED, - SHUTDOWN_REASON_THERMAL_SHUTDOWN + SHUTDOWN_REASON_THERMAL_SHUTDOWN, + SHUTDOWN_REASON_LOW_BATTERY, + SHUTDOWN_REASON_BATTERY_THERMAL }) public @interface ShutdownReason {} @@ -485,6 +501,18 @@ public final class PowerManager { */ public static final int SHUTDOWN_REASON_THERMAL_SHUTDOWN = 4; + /** + * constant for shutdown reason being low battery. + * @hide + */ + public static final int SHUTDOWN_REASON_LOW_BATTERY = 5; + + /** + * constant for shutdown reason being critical battery thermal state. + * @hide + */ + public static final int SHUTDOWN_REASON_BATTERY_THERMAL = 6; + final Context mContext; final IPowerManager mService; final Handler mHandler; @@ -1384,7 +1412,11 @@ public final class PowerManager { */ public void release(int flags) { synchronized (mToken) { - mInternalCount--; + if (mInternalCount > 0) { + // internal count must only be decreased if it is > 0 or state of + // the WakeLock object is broken. + mInternalCount--; + } if ((flags & RELEASE_FLAG_TIMEOUT) == 0) { mExternalCount--; } diff --git a/android/os/ServiceManager.java b/android/os/ServiceManager.java index 34c78455..f41848fa 100644 --- a/android/os/ServiceManager.java +++ b/android/os/ServiceManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * 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. @@ -16,9 +16,29 @@ 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. @@ -27,14 +47,32 @@ 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; } /** - * Is not supposed to return null, but that is fine for layoutlib. + * Returns a reference to a service with the given name, or throws + * {@link NullPointerException} if none is found. + * + * @hide */ public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException { - throw new ServiceNotFoundException(name); + final IBinder binder = getService(name); + if (binder != null) { + return binder; + } else { + throw new ServiceNotFoundException(name); + } } /** @@ -45,7 +83,39 @@ public final class ServiceManager { * @param service the service object */ public static void addService(String name, IBinder service) { - // pass + 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); + } } /** @@ -53,7 +123,17 @@ public final class ServiceManager { * service manager. Non-blocking. */ public static IBinder checkService(String name) { - return null; + 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; + } } /** @@ -62,9 +142,12 @@ public final class ServiceManager { * case of an exception */ public static String[] listServices() { - // actual implementation returns null sometimes, so it's ok - // to return null instead of an empty list. - return null; + try { + return getIServiceManager().listServices(IServiceManager.DUMP_PRIORITY_ALL); + } catch (RemoteException e) { + Log.e(TAG, "error in listServices", e); + return null; + } } /** @@ -76,7 +159,10 @@ public final class ServiceManager { * @hide */ public static void initServiceCache(Map<String, IBinder> cache) { - // pass + if (sCache.size() != 0) { + throw new IllegalStateException("setServiceCache may only be called once"); + } + sCache.putAll(cache); } /** @@ -87,7 +173,6 @@ 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/ServiceManagerNative.java b/android/os/ServiceManagerNative.java index be244264..589b8c49 100644 --- a/android/os/ServiceManagerNative.java +++ b/android/os/ServiceManagerNative.java @@ -40,63 +40,65 @@ public abstract class ServiceManagerNative extends Binder implements IServiceMan if (in != null) { return in; } - + return new ServiceManagerProxy(obj); } - + public ServiceManagerNative() { attachInterface(this, descriptor); } - + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) { try { switch (code) { - case IServiceManager.GET_SERVICE_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - String name = data.readString(); - IBinder service = getService(name); - reply.writeStrongBinder(service); - return true; - } - - case IServiceManager.CHECK_SERVICE_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - String name = data.readString(); - IBinder service = checkService(name); - reply.writeStrongBinder(service); - return true; - } - - case IServiceManager.ADD_SERVICE_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - String name = data.readString(); - IBinder service = data.readStrongBinder(); - boolean allowIsolated = data.readInt() != 0; - addService(name, service, allowIsolated); - return true; - } - - case IServiceManager.LIST_SERVICES_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - String[] list = listServices(); - reply.writeStringArray(list); - return true; - } - - case IServiceManager.SET_PERMISSION_CONTROLLER_TRANSACTION: { - data.enforceInterface(IServiceManager.descriptor); - IPermissionController controller - = IPermissionController.Stub.asInterface( - data.readStrongBinder()); - setPermissionController(controller); - return true; - } + case IServiceManager.GET_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = getService(name); + reply.writeStrongBinder(service); + return true; + } + + case IServiceManager.CHECK_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = checkService(name); + reply.writeStrongBinder(service); + return true; + } + + case IServiceManager.ADD_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = data.readStrongBinder(); + boolean allowIsolated = data.readInt() != 0; + int dumpPriority = data.readInt(); + addService(name, service, allowIsolated, dumpPriority); + return true; + } + + case IServiceManager.LIST_SERVICES_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + int dumpPriority = data.readInt(); + String[] list = listServices(dumpPriority); + reply.writeStringArray(list); + return true; + } + + case IServiceManager.SET_PERMISSION_CONTROLLER_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + IPermissionController controller = + IPermissionController.Stub.asInterface( + data.readStrongBinder()); + setPermissionController(controller); + return true; + } } } catch (RemoteException e) { } - + return false; } @@ -110,11 +112,11 @@ class ServiceManagerProxy implements IServiceManager { public ServiceManagerProxy(IBinder remote) { mRemote = remote; } - + public IBinder asBinder() { return mRemote; } - + public IBinder getService(String name) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -139,7 +141,7 @@ class ServiceManagerProxy implements IServiceManager { return binder; } - public void addService(String name, IBinder service, boolean allowIsolated) + public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -147,12 +149,13 @@ class ServiceManagerProxy implements IServiceManager { data.writeString(name); data.writeStrongBinder(service); data.writeInt(allowIsolated ? 1 : 0); + data.writeInt(dumpPriority); mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0); reply.recycle(); data.recycle(); } - - public String[] listServices() throws RemoteException { + + public String[] listServices(int dumpPriority) throws RemoteException { ArrayList<String> services = new ArrayList<String>(); int n = 0; while (true) { @@ -160,6 +163,7 @@ class ServiceManagerProxy implements IServiceManager { Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IServiceManager.descriptor); data.writeInt(n); + data.writeInt(dumpPriority); n++; try { boolean res = mRemote.transact(LIST_SERVICES_TRANSACTION, data, reply, 0); diff --git a/android/os/StrictMode.java b/android/os/StrictMode.java index f02631c7..826ec1eb 100644 --- a/android/os/StrictMode.java +++ b/android/os/StrictMode.java @@ -16,6 +16,7 @@ package android.os; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.annotation.TestApi; import android.app.ActivityManager; import android.app.ActivityThread; @@ -153,12 +154,15 @@ public final class StrictMode { // Byte 1: Thread-policy /** @hide */ + @TestApi public static final int DETECT_DISK_WRITE = 0x01; // for ThreadPolicy /** @hide */ + @TestApi public static final int DETECT_DISK_READ = 0x02; // for ThreadPolicy /** @hide */ + @TestApi public static final int DETECT_NETWORK = 0x04; // for ThreadPolicy /** @@ -166,6 +170,7 @@ public final class StrictMode { * * @hide */ + @TestApi public static final int DETECT_CUSTOM = 0x08; // for ThreadPolicy /** @@ -173,9 +178,11 @@ public final class StrictMode { * * @hide */ + @TestApi public static final int DETECT_RESOURCE_MISMATCH = 0x10; // for ThreadPolicy /** @hide */ + @TestApi public static final int DETECT_UNBUFFERED_IO = 0x20; // for ThreadPolicy private static final int ALL_THREAD_DETECT_BITS = @@ -193,6 +200,7 @@ public final class StrictMode { * * @hide */ + @TestApi public static final int DETECT_VM_CURSOR_LEAKS = 0x01 << 8; // for VmPolicy /** @@ -200,6 +208,7 @@ public final class StrictMode { * * @hide */ + @TestApi public static final int DETECT_VM_CLOSABLE_LEAKS = 0x02 << 8; // for VmPolicy /** @@ -207,25 +216,32 @@ public final class StrictMode { * * @hide */ + @TestApi public static final int DETECT_VM_ACTIVITY_LEAKS = 0x04 << 8; // for VmPolicy /** @hide */ - private static final int DETECT_VM_INSTANCE_LEAKS = 0x08 << 8; // for VmPolicy + @TestApi + public static final int DETECT_VM_INSTANCE_LEAKS = 0x08 << 8; // for VmPolicy /** @hide */ + @TestApi public static final int DETECT_VM_REGISTRATION_LEAKS = 0x10 << 8; // for VmPolicy /** @hide */ - private static final int DETECT_VM_FILE_URI_EXPOSURE = 0x20 << 8; // for VmPolicy + @TestApi + public static final int DETECT_VM_FILE_URI_EXPOSURE = 0x20 << 8; // for VmPolicy /** @hide */ - private static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8; // for VmPolicy + @TestApi + public static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8; // for VmPolicy /** @hide */ - private static final int DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION = 0x80 << 8; // for VmPolicy + @TestApi + public static final int DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION = 0x80 << 8; // for VmPolicy /** @hide */ - private static final int DETECT_VM_UNTAGGED_SOCKET = 0x80 << 24; // for VmPolicy + @TestApi + public static final int DETECT_VM_UNTAGGED_SOCKET = 0x80 << 24; // for VmPolicy private static final int ALL_VM_DETECT_BITS = DETECT_VM_CURSOR_LEAKS @@ -322,16 +338,36 @@ public final class StrictMode { /** {@hide} */ @TestApi - public interface ViolationListener { - public void onViolation(String message); + public interface ViolationLogger { + + /** Called when penaltyLog is enabled and a violation needs logging. */ + void log(ViolationInfo info); } - private static volatile ViolationListener sListener; + private static final ViolationLogger LOGCAT_LOGGER = + info -> { + String msg; + if (info.durationMillis != -1) { + msg = "StrictMode policy violation; ~duration=" + info.durationMillis + " ms:"; + } else { + msg = "StrictMode policy violation:"; + } + if (info.crashInfo != null) { + Log.d(TAG, msg + " " + info.crashInfo.stackTrace); + } else { + Log.d(TAG, msg + " missing stack trace!"); + } + }; + + private static volatile ViolationLogger sLogger = LOGCAT_LOGGER; /** {@hide} */ @TestApi - public static void setViolationListener(ViolationListener listener) { - sListener = listener; + public static void setViolationLogger(ViolationLogger listener) { + if (listener == null) { + listener = LOGCAT_LOGGER; + } + sLogger = listener; } /** @@ -1512,28 +1548,16 @@ public final class StrictMode { lastViolationTime = vtime; } } else { - mLastViolationTime = new ArrayMap<Integer, Long>(1); + mLastViolationTime = new ArrayMap<>(1); } long now = SystemClock.uptimeMillis(); mLastViolationTime.put(crashFingerprint, now); long timeSinceLastViolationMillis = lastViolationTime == 0 ? Long.MAX_VALUE : (now - lastViolationTime); - if ((info.policy & PENALTY_LOG) != 0 && sListener != null) { - sListener.onViolation(info.crashInfo.stackTrace); - } if ((info.policy & PENALTY_LOG) != 0 && timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { - if (info.durationMillis != -1) { - Log.d( - TAG, - "StrictMode policy violation; ~duration=" - + info.durationMillis - + " ms: " - + info.crashInfo.stackTrace); - } else { - Log.d(TAG, "StrictMode policy violation: " + info.crashInfo.stackTrace); - } + sLogger.log(info); } // The violationMaskSubset, passed to ActivityManager, is a @@ -1849,6 +1873,10 @@ public final class StrictMode { } /** @hide */ + public static final String CLEARTEXT_DETECTED_MSG = + "Detected cleartext network traffic from UID "; + + /** @hide */ public static void onCleartextNetworkDetected(byte[] firstPacket) { byte[] rawAddr = null; if (firstPacket != null) { @@ -1864,14 +1892,10 @@ public final class StrictMode { } final int uid = android.os.Process.myUid(); - String msg = "Detected cleartext network traffic from UID " + uid; + String msg = CLEARTEXT_DETECTED_MSG + uid; if (rawAddr != null) { try { - msg = - "Detected cleartext network traffic from UID " - + uid - + " to " - + InetAddress.getByAddress(rawAddr); + msg += " to " + InetAddress.getByAddress(rawAddr); } catch (UnknownHostException ignored) { } } @@ -1882,12 +1906,13 @@ public final class StrictMode { } /** @hide */ + public static final String UNTAGGED_SOCKET_VIOLATION_MESSAGE = + "Untagged socket detected; use" + + " TrafficStats.setThreadSocketTag() to track all network usage"; + + /** @hide */ public static void onUntaggedSocket() { - onVmPolicyViolation( - null, - new Throwable( - "Untagged socket detected; use" - + " TrafficStats.setThreadSocketTag() to track all network usage")); + onVmPolicyViolation(null, new Throwable(UNTAGGED_SOCKET_VIOLATION_MESSAGE)); } // Map from VM violation fingerprint to uptime millis. @@ -1925,11 +1950,11 @@ public final class StrictMode { } } - if (penaltyLog && sListener != null) { - sListener.onViolation(originStack.toString()); + if (penaltyLog && sLogger != null) { + sLogger.log(info); } if (penaltyLog && timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { - Log.e(TAG, message, originStack); + sLogger.log(info); } int violationMaskSubset = PENALTY_DROPBOX | (ALL_VM_DETECT_BITS & sVmPolicy.mask); @@ -2339,11 +2364,12 @@ public final class StrictMode { * * @hide */ - public static class ViolationInfo implements Parcelable { + @TestApi + public static final class ViolationInfo implements Parcelable { public final String message; /** Stack and other stuff info. */ - public final ApplicationErrorReport.CrashInfo crashInfo; + @Nullable public final ApplicationErrorReport.CrashInfo crashInfo; /** The strict mode policy mask at the time of violation. */ public final int policy; diff --git a/android/os/UserManagerInternal.java b/android/os/UserManagerInternal.java index 17f00c24..9369eebf 100644 --- a/android/os/UserManagerInternal.java +++ b/android/os/UserManagerInternal.java @@ -154,11 +154,21 @@ public abstract class UserManagerInternal { public abstract boolean isUserUnlocked(int userId); /** - * Return whether the given user is running + * Returns whether the given user is running */ public abstract boolean isUserRunning(int userId); /** + * Returns whether the given user is initialized + */ + public abstract boolean isUserInitialized(int userId); + + /** + * Returns whether the given user exists + */ + public abstract boolean exists(int userId); + + /** * Set user's running state */ public abstract void setUserState(int userId, int userState); diff --git a/android/provider/Settings.java b/android/provider/Settings.java index 40ced6ce..a062db43 100644 --- a/android/provider/Settings.java +++ b/android/provider/Settings.java @@ -6966,8 +6966,9 @@ public final class Settings { public static final String NIGHT_DISPLAY_CUSTOM_END_TIME = "night_display_custom_end_time"; /** - * Time in milliseconds (since epoch) when Night display was last activated. Use to decide - * whether to apply the current activated state after a reboot or user change. + * A String representing the LocalDateTime when Night display was last activated. Use to + * decide whether to apply the current activated state after a reboot or user change. In + * legacy cases, this is represented by the time in milliseconds (since epoch). * @hide */ public static final String NIGHT_DISPLAY_LAST_ACTIVATED_TIME = @@ -9368,16 +9369,6 @@ public final class Settings { public static final String DEVICE_IDLE_CONSTANTS = "device_idle_constants"; /** - * Device Idle (Doze) specific settings for watches. See {@code #DEVICE_IDLE_CONSTANTS} - * - * <p> - * Type: string - * @hide - * @see com.android.server.DeviceIdleController.Constants - */ - public static final String DEVICE_IDLE_CONSTANTS_WATCH = "device_idle_constants_watch"; - - /** * Battery Saver specific settings * This is encoded as a key=value list, separated by commas. Ex: * @@ -9403,9 +9394,12 @@ public final class Settings { /** * Battery anomaly detection specific settings - * This is encoded as a key=value list, separated by commas. Ex: + * This is encoded as a key=value list, separated by commas. + * wakeup_blacklisted_tags is a string, encoded as a set of tags, encoded via + * {@link Uri#encode(String)}, separated by colons. Ex: * - * "anomaly_detection_enabled=true,wakelock_threshold=2000" + * "anomaly_detection_enabled=true,wakelock_threshold=2000,wakeup_alarm_enabled=true," + * "wakeup_alarm_threshold=10,wakeup_blacklisted_tags=tag1:tag2:with%2Ccomma:with%3Acolon" * * The following keys are supported: * @@ -9413,6 +9407,11 @@ public final class Settings { * anomaly_detection_enabled (boolean) * wakelock_enabled (boolean) * wakelock_threshold (long) + * wakeup_alarm_enabled (boolean) + * wakeup_alarm_threshold (long) + * wakeup_blacklisted_tags (string) + * bluetooth_scan_enabled (boolean) + * bluetooth_scan_threshold (long) * </pre> * @hide */ @@ -10199,7 +10198,7 @@ public final class Settings { "allow_user_switching_when_system_user_locked"; /** - * Boot count since the device starts running APK level 24. + * Boot count since the device starts running API level 24. * <p> * Type: int */ @@ -10893,6 +10892,26 @@ public final class Settings { */ public static final String ENABLE_DELETION_HELPER_NO_THRESHOLD_TOGGLE = "enable_deletion_helper_no_threshold_toggle"; + + /** + * The list of snooze options for notifications + * This is encoded as a key=value list, separated by commas. Ex: + * + * "default=60,options_array=15:30:60:120" + * + * The following keys are supported: + * + * <pre> + * default (int) + * options_array (string) + * </pre> + * + * All delays in integer minutes. Array order is respected. + * Options will be used in order up to the maximum allowed by the UI. + * @hide + */ + public static final String NOTIFICATION_SNOOZE_OPTIONS = + "notification_snooze_options"; } /** diff --git a/android/service/autofill/AutofillService.java b/android/service/autofill/AutofillService.java index 3e08dcf2..2e59f6c5 100644 --- a/android/service/autofill/AutofillService.java +++ b/android/service/autofill/AutofillService.java @@ -187,7 +187,7 @@ import com.android.internal.os.SomeArgs; * protect a dataset that contains sensitive information by requiring dataset authentication * (see {@link Dataset.Builder#setAuthentication(android.content.IntentSender)}), and to include * info about the "primary" field of the partition in the custom presentation for "secondary" - * fields — that would prevent a malicious app from getting the "primary" fields without the + * fields—that would prevent a malicious app from getting the "primary" fields without the * user realizing they're being released (for example, a malicious app could have fields for a * credit card number, verification code, and expiration date crafted in a way that just the latter * is visible; by explicitly indicating the expiration date is related to a given credit card @@ -305,7 +305,7 @@ import com.android.internal.os.SomeArgs; * <li>Use the {@link android.app.assist.AssistStructure.ViewNode#getWebDomain()} to get the * source of the document. * <li>Get the canonical domain using the - * <a href="https://publicsuffix.org/>Public Suffix List</a> (see example below). + * <a href="https://publicsuffix.org/">Public Suffix List</a> (see example below). * <li>Use <a href="https://developers.google.com/digital-asset-links/">Digital Asset Links</a> * to obtain the package name and certificate fingerprint of the package corresponding to * the canonical domain. @@ -503,13 +503,19 @@ public abstract class AutofillService extends Service { @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback); /** - * Called when user requests service to save the fields of a screen. + * Called when the user requests the service to save the contents of a screen. * * <p>Service must call one of the {@link SaveCallback} methods (like * {@link SaveCallback#onSuccess()} or {@link SaveCallback#onFailure(CharSequence)}) - * to notify the result of the request. + * to notify the Android System of the result of the request. + * + * <p>If the service could not handle the request right away—for example, because it must + * launch an activity asking the user to authenticate first or because the network is + * down—the service could keep the {@link SaveRequest request} and reuse it later, + * but the service must call {@link SaveCallback#onSuccess()} right away. * - * <p><b>Note:</b> To retrieve the actual value of the field, the service should call + * <p><b>Note:</b> To retrieve the actual value of fields input by the user, the service + * should call * {@link android.app.assist.AssistStructure.ViewNode#getAutofillValue()}; if it calls * {@link android.app.assist.AssistStructure.ViewNode#getText()} or other methods, there is no * guarantee such method will return the most recent value of the field. diff --git a/android/service/autofill/AutofillServiceInfo.java b/android/service/autofill/AutofillServiceInfo.java index f1474006..5c7388f7 100644 --- a/android/service/autofill/AutofillServiceInfo.java +++ b/android/service/autofill/AutofillServiceInfo.java @@ -146,4 +146,9 @@ public final class AutofillServiceInfo { public String getSettingsActivity() { return mSettingsActivity; } + + @Override + public String toString() { + return mServiceInfo == null ? "null" : mServiceInfo.toString(); + } } diff --git a/android/service/autofill/Dataset.java b/android/service/autofill/Dataset.java index cb341b1d..ef9598aa 100644 --- a/android/service/autofill/Dataset.java +++ b/android/service/autofill/Dataset.java @@ -29,32 +29,77 @@ import android.widget.RemoteViews; import com.android.internal.util.Preconditions; +import java.io.Serializable; import java.util.ArrayList; +import java.util.regex.Pattern; /** - * A dataset object represents a group of key/value pairs used to autofill parts of a screen. + * A dataset object represents a group of fields (key / value pairs) used to autofill parts of a + * screen. * - * <p>In its simplest form, a dataset contains one or more key / value pairs (comprised of - * {@link AutofillId} and {@link AutofillValue} respectively); and one or more - * {@link RemoteViews presentation} for these pairs (a pair could have its own - * {@link RemoteViews presentation}, or use the default {@link RemoteViews presentation} associated - * with the whole dataset). When an autofill service returns datasets in a {@link FillResponse} + * <a name="BasicUsage"></a> + * <h3>Basic usage</h3> + * + * <p>In its simplest form, a dataset contains one or more fields (comprised of + * an {@link AutofillId id}, a {@link AutofillValue value}, and an optional filter + * {@link Pattern regex}); and one or more {@link RemoteViews presentations} for these fields + * (each field could have its own {@link RemoteViews presentation}, or use the default + * {@link RemoteViews presentation} associated with the whole dataset). + * + * <p>When an autofill service returns datasets in a {@link FillResponse} * and the screen input is focused in a view that is present in at least one of these datasets, - * the Android System displays a UI affordance containing the {@link RemoteViews presentation} of + * the Android System displays a UI containing the {@link RemoteViews presentation} of * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a - * dataset from the affordance, all views in that dataset are autofilled. + * dataset from the UI, all views in that dataset are autofilled. + * + * <a name="Authentication"></a> + * <h3>Dataset authentication</h3> + * + * <p>In a more sophisticated form, the dataset values can be protected until the user authenticates + * the dataset—in that case, when a dataset is selected by the user, the Android System + * launches an intent set by the service to "unlock" the dataset. + * + * <p>For example, when a data set contains credit card information (such as number, + * expiration date, and verification code), you could provide a dataset presentation saying + * "Tap to authenticate". Then when the user taps that option, you would launch an activity asking + * the user to enter the credit card code, and if the user enters a valid code, you could then + * "unlock" the dataset. + * + * <p>You can also use authenticated datasets to offer an interactive UI for the user. For example, + * if the activity being autofilled is an account creation screen, you could use an authenticated + * dataset to automatically generate a random password for the user. * - * <p>In a more sophisticated form, the dataset value can be protected until the user authenticates - * the dataset - see {@link Dataset.Builder#setAuthentication(IntentSender)}. + * <p>See {@link Dataset.Builder#setAuthentication(IntentSender)} for more details about the dataset + * authentication mechanism. * - * @see android.service.autofill.AutofillService for more information and examples about the - * role of datasets in the autofill workflow. + * <a name="Filtering"></a> + * <h3>Filtering</h3> + * <p>The autofill UI automatically changes which values are shown based on value of the view + * anchoring it, following the rules below: + * <ol> + * <li>If the view's {@link android.view.View#getAutofillValue() autofill value} is not + * {@link AutofillValue#isText() text} or is empty, all datasets are shown. + * <li>Datasets that have a filter regex (set through + * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern)} or + * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}) and whose + * regex matches the view's text value converted to lower case are shown. + * <li>Datasets that do not require authentication, have a field value that is + * {@link AutofillValue#isText() text} and whose {@link AutofillValue#getTextValue() value} starts + * with the lower case value of the view's text are shown. + * <li>All other datasets are hidden. + * </ol> + * + * <a name="MoreInfo"></a> + * <h3>More information</h3> + * <p>See {@link android.service.autofill.AutofillService} for more information and examples about + * the role of datasets in the autofill workflow. */ public final class Dataset implements Parcelable { private final ArrayList<AutofillId> mFieldIds; private final ArrayList<AutofillValue> mFieldValues; private final ArrayList<RemoteViews> mFieldPresentations; + private final ArrayList<Pattern> mFieldFilters; private final RemoteViews mPresentation; private final IntentSender mAuthentication; @Nullable String mId; @@ -63,6 +108,7 @@ public final class Dataset implements Parcelable { mFieldIds = builder.mFieldIds; mFieldValues = builder.mFieldValues; mFieldPresentations = builder.mFieldPresentations; + mFieldFilters = builder.mFieldFilters; mPresentation = builder.mPresentation; mAuthentication = builder.mAuthentication; mId = builder.mId; @@ -85,6 +131,12 @@ public final class Dataset implements Parcelable { } /** @hide */ + @Nullable + public Pattern getFilter(int index) { + return mFieldFilters.get(index); + } + + /** @hide */ public @Nullable IntentSender getAuthentication() { return mAuthentication; } @@ -103,6 +155,8 @@ public final class Dataset implements Parcelable { .append(", fieldValues=").append(mFieldValues) .append(", fieldPresentations=") .append(mFieldPresentations == null ? 0 : mFieldPresentations.size()) + .append(", fieldFilters=") + .append(mFieldFilters == null ? 0 : mFieldFilters.size()) .append(", hasPresentation=").append(mPresentation != null) .append(", hasAuthentication=").append(mAuthentication != null) .append(']').toString(); @@ -127,6 +181,7 @@ public final class Dataset implements Parcelable { private ArrayList<AutofillId> mFieldIds; private ArrayList<AutofillValue> mFieldValues; private ArrayList<RemoteViews> mFieldPresentations; + private ArrayList<Pattern> mFieldFilters; private RemoteViews mPresentation; private IntentSender mAuthentication; private boolean mDestroyed; @@ -182,12 +237,12 @@ public final class Dataset implements Parcelable { * credit card information without the CVV for the data set in the {@link FillResponse * response} then the returned data set should contain the CVV entry. * - * <p><b>NOTE:</b> Do not make the provided pending intent + * <p><b>Note:</b> Do not make the provided pending intent * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the * platform needs to fill in the authentication arguments. * * @param authentication Intent to an activity with your authentication flow. - * @return This builder. + * @return this builder. * * @see android.app.PendingIntent */ @@ -214,11 +269,10 @@ public final class Dataset implements Parcelable { * * @param id id for this dataset or {@code null} to unset. * - * @return This builder. + * @return this builder. */ public @NonNull Builder setId(@Nullable String id) { throwIfDestroyed(); - mId = id; return this; } @@ -230,17 +284,16 @@ public final class Dataset implements Parcelable { * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. * @param value value to be autofilled. Pass {@code null} if you do not have the value * but the target view is a logical part of the dataset. For example, if - * the dataset needs an authentication and you have no access to the value. - * @return This builder. + * 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(); - if (mPresentation == null) { - throw new IllegalStateException("Dataset presentation not set on constructor"); - } - setValueAndPresentation(id, value, null); + Preconditions.checkState(mPresentation != null, + "Dataset presentation not set on constructor"); + setLifeTheUniverseAndEverything(id, value, null, null); return this; } @@ -250,23 +303,81 @@ public final class Dataset implements Parcelable { * * @param id id returned by {@link * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. - * @param value value to be auto filled. Pass {@code null} if you do not have the value + * @param value the value to be autofilled. Pass {@code null} if you do not have the value * but the target view is a logical part of the dataset. For example, if - * the dataset needs an authentication and you have no access to the value. - * Filtering matches any user typed string to {@code null} values. - * @param presentation The presentation used to visualize this field. - * @return This builder. + * the dataset needs authentication and you have no access to the value. + * @param presentation the presentation used to visualize this field. + * @return this builder. + * */ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation) { throwIfDestroyed(); Preconditions.checkNotNull(presentation, "presentation cannot be null"); - setValueAndPresentation(id, value, presentation); + setLifeTheUniverseAndEverything(id, value, presentation, null); + return this; + } + + /** + * Sets the value of a field using an <a href="#Filtering">explicit filter</a>. + * + * <p>This method is typically used when the dataset is not authenticated and the field + * value is not {@link AutofillValue#isText() text} but the service still wants to allow + * the user to filter it out. + * + * @param id id returned by {@link + * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. + * @param value the value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if + * the dataset needs authentication and you have no access to the value. + * @param filter regex used to determine if the dataset should be shown in the autofill UI. + * + * @return this builder. + * @throws IllegalStateException if the builder was constructed without a + * {@link RemoteViews presentation}. + */ + public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, + @NonNull Pattern filter) { + throwIfDestroyed(); + Preconditions.checkNotNull(filter, "filter cannot be null"); + Preconditions.checkState(mPresentation != null, + "Dataset presentation not set on constructor"); + setLifeTheUniverseAndEverything(id, value, null, filter); + return this; + } + + /** + * Sets the value of a field, using a custom {@link RemoteViews presentation} to + * visualize it and a <a href="#Filtering">explicit filter</a>. + * + * <p>Typically used to allow filtering on + * {@link Dataset.Builder#setAuthentication(IntentSender) authenticated datasets}. For + * example, if the dataset represents a credit card number and the service does not want to + * show the "Tap to authenticate" message until the user tapped 4 digits, in which case + * the filter would be {@code Pattern.compile("\\d.{4,}")}. + * + * @param id id returned by {@link + * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. + * @param value the value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if + * the dataset needs authentication and you have no access to the value. + * @param presentation the presentation used to visualize this field. + * @param filter regex used to determine if the dataset should be shown in the autofill UI. + * + * @return this builder. + */ + public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, + @NonNull Pattern filter, @NonNull RemoteViews presentation) { + throwIfDestroyed(); + Preconditions.checkNotNull(filter, "filter cannot be null"); + Preconditions.checkNotNull(presentation, "presentation cannot be null"); + setLifeTheUniverseAndEverything(id, value, presentation, filter); return this; } - private void setValueAndPresentation(AutofillId id, AutofillValue value, - RemoteViews presentation) { + private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, + @Nullable AutofillValue value, @Nullable RemoteViews presentation, + @Nullable Pattern filter) { Preconditions.checkNotNull(id, "id cannot be null"); if (mFieldIds != null) { final int existingIdx = mFieldIds.indexOf(id); @@ -279,10 +390,12 @@ public final class Dataset implements Parcelable { mFieldIds = new ArrayList<>(); mFieldValues = new ArrayList<>(); mFieldPresentations = new ArrayList<>(); + mFieldFilters = new ArrayList<>(); } mFieldIds.add(id); mFieldValues.add(value); mFieldPresentations.add(presentation); + mFieldFilters.add(filter); } /** @@ -290,8 +403,9 @@ public final class Dataset implements Parcelable { * * <p>You should not interact with this builder once this method is called. * - * <p>It is required that you specify at least one field before calling this method. It's - * also mandatory to provide a presentation view to visualize the data set in the UI. + * @throws IllegalStateException if no field was set (through + * {@link #setValue(AutofillId, AutofillValue)} or + * {@link #setValue(AutofillId, AutofillValue, RemoteViews)}). * * @return The built dataset. */ @@ -299,7 +413,7 @@ public final class Dataset implements Parcelable { throwIfDestroyed(); mDestroyed = true; if (mFieldIds == null) { - throw new IllegalArgumentException("at least one value must be set"); + throw new IllegalStateException("at least one value must be set"); } return new Dataset(this); } @@ -323,9 +437,10 @@ public final class Dataset implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeParcelable(mPresentation, flags); - parcel.writeTypedArrayList(mFieldIds, flags); - parcel.writeTypedArrayList(mFieldValues, flags); + parcel.writeTypedList(mFieldIds, flags); + parcel.writeTypedList(mFieldValues, flags); parcel.writeParcelableList(mFieldPresentations, flags); + parcel.writeSerializable(mFieldFilters); parcel.writeParcelable(mAuthentication, flags); parcel.writeString(mId); } @@ -340,10 +455,14 @@ public final class Dataset implements Parcelable { final Builder builder = (presentation == null) ? new Builder() : new Builder(presentation); - final ArrayList<AutofillId> ids = parcel.readTypedArrayList(null); - final ArrayList<AutofillValue> values = parcel.readTypedArrayList(null); + final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR); + final ArrayList<AutofillValue> values = + parcel.createTypedArrayList(AutofillValue.CREATOR); final ArrayList<RemoteViews> presentations = new ArrayList<>(); parcel.readParcelableList(presentations, null); + @SuppressWarnings("unchecked") + final ArrayList<Serializable> filters = + (ArrayList<Serializable>) parcel.readSerializable(); final int idCount = (ids != null) ? ids.size() : 0; final int valueCount = (values != null) ? values.size() : 0; for (int i = 0; i < idCount; i++) { @@ -351,7 +470,8 @@ public final class Dataset implements Parcelable { final AutofillValue value = (valueCount > i) ? values.get(i) : null; final RemoteViews fieldPresentation = presentations.isEmpty() ? null : presentations.get(i); - builder.setValueAndPresentation(id, value, fieldPresentation); + final Pattern filter = (Pattern) filters.get(i); + builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, filter); } builder.setAuthentication(parcel.readParcelable(null)); builder.setId(parcel.readString()); diff --git a/android/service/autofill/ImageTransformation.java b/android/service/autofill/ImageTransformation.java index 2151f74f..4afda249 100644 --- a/android/service/autofill/ImageTransformation.java +++ b/android/service/autofill/ImageTransformation.java @@ -20,11 +20,12 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.DrawableRes; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import android.view.autofill.AutofillId; import android.widget.ImageView; import android.widget.RemoteViews; @@ -43,9 +44,9 @@ import java.util.regex.Pattern; * * <pre class="prettyprint"> * new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"), - * R.drawable.ic_credit_card_logo1) - * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2) - * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3) + * R.drawable.ic_credit_card_logo1, "Brand 1") + * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2, "Brand 2") + * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3, "Brand 3") * .build(); * </pre> * @@ -59,7 +60,7 @@ public final class ImageTransformation extends InternalTransformation implements private static final String TAG = "ImageTransformation"; private final AutofillId mId; - private final ArrayList<Pair<Pattern, Integer>> mOptions; + private final ArrayList<Option> mOptions; private ImageTransformation(Builder builder) { mId = builder.mId; @@ -82,17 +83,21 @@ public final class ImageTransformation extends InternalTransformation implements } for (int i = 0; i < size; i++) { - final Pair<Pattern, Integer> option = mOptions.get(i); + final Option option = mOptions.get(i); try { - if (option.first.matcher(value).matches()) { + if (option.pattern.matcher(value).matches()) { Log.d(TAG, "Found match at " + i + ": " + option); - parentTemplate.setImageViewResource(childViewId, option.second); + parentTemplate.setImageViewResource(childViewId, option.resId); + if (option.contentDescription != null) { + parentTemplate.setContentDescription(childViewId, + option.contentDescription); + } return; } } catch (Exception e) { // Do not log full exception to avoid PII leaking - Log.w(TAG, "Error matching regex #" + i + "(" + option.first.pattern() + ") on id " - + option.second + ": " + e.getClass()); + Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id " + + option.resId + ": " + e.getClass()); throw e; } @@ -105,25 +110,44 @@ public final class ImageTransformation extends InternalTransformation implements */ public static class Builder { private final AutofillId mId; - private final ArrayList<Pair<Pattern, Integer>> mOptions = new ArrayList<>(); + private final ArrayList<Option> mOptions = new ArrayList<>(); private boolean mDestroyed; /** - * Create a new builder for a autofill id and add a first option. + * Creates a new builder for a autofill id and add a first option. * * @param id id of the screen field that will be used to evaluate whether the image should * be used. * @param regex regular expression defining what should be matched to use this image. * @param resId resource id of the image (in the autofill service's package). The * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * + * @deprecated use + * {@link #ImageTransformation.Builder(AutofillId, Pattern, int, CharSequence)} instead. */ + @Deprecated public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) { mId = Preconditions.checkNotNull(id); - addOption(regex, resId); } /** + * Creates a new builder for a autofill id and add a first option. + * + * @param id id of the screen field that will be used to evaluate whether the image should + * be used. + * @param regex regular expression defining what should be matched to use this image. + * @param resId resource id of the image (in the autofill service's package). The + * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * @param contentDescription content description to be applied in the child view. + */ + public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId, + @NonNull CharSequence contentDescription) { + mId = Preconditions.checkNotNull(id); + addOption(regex, resId, contentDescription); + } + + /** * Adds an option to replace the child view with a different image when the regex matches. * * @param regex regular expression defining what should be matched to use this image. @@ -131,17 +155,43 @@ public final class ImageTransformation extends InternalTransformation implements * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. * * @return this build + * + * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead. */ + @Deprecated public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) { + addOptionInternal(regex, resId, null); + return this; + } + + /** + * Adds an option to replace the child view with a different image and content description + * when the regex matches. + * + * @param regex regular expression defining what should be matched to use this image. + * @param resId resource id of the image (in the autofill service's package). The + * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * @param contentDescription content description to be applied in the child view. + * + * @return this build + */ + public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId, + @NonNull CharSequence contentDescription) { + addOptionInternal(regex, resId, Preconditions.checkNotNull(contentDescription)); + return this; + } + + private void addOptionInternal(@NonNull Pattern regex, @DrawableRes int resId, + @Nullable CharSequence contentDescription) { throwIfDestroyed(); Preconditions.checkNotNull(regex); Preconditions.checkArgument(resId != 0); - mOptions.add(new Pair<>(regex, resId)); - return this; + mOptions.add(new Option(regex, resId, contentDescription)); } + /** * Creates a new {@link ImageTransformation} instance. */ @@ -178,15 +228,18 @@ public final class ImageTransformation extends InternalTransformation implements parcel.writeParcelable(mId, flags); final int size = mOptions.size(); - final Pattern[] regexs = new Pattern[size]; + final Pattern[] patterns = new Pattern[size]; final int[] resIds = new int[size]; + final CharSequence[] contentDescriptions = new String[size]; for (int i = 0; i < size; i++) { - Pair<Pattern, Integer> regex = mOptions.get(i); - regexs[i] = regex.first; - resIds[i] = regex.second; + final Option option = mOptions.get(i); + patterns[i] = option.pattern; + resIds[i] = option.resId; + contentDescriptions[i] = option.contentDescription; } - parcel.writeSerializable(regexs); + parcel.writeSerializable(patterns); parcel.writeIntArray(resIds); + parcel.writeCharSequenceArray(contentDescriptions); } public static final Parcelable.Creator<ImageTransformation> CREATOR = @@ -197,15 +250,22 @@ public final class ImageTransformation extends InternalTransformation implements final Pattern[] regexs = (Pattern[]) parcel.readSerializable(); final int[] resIds = parcel.createIntArray(); + final CharSequence[] contentDescriptions = parcel.readCharSequenceArray(); // Always go through the builder to ensure the data ingested by the system obeys the // contract of the builder to avoid attacks using specially crafted parcels. - final ImageTransformation.Builder builder = new ImageTransformation.Builder(id, - regexs[0], resIds[0]); + final CharSequence contentDescription = contentDescriptions[0]; + final ImageTransformation.Builder builder = (contentDescription != null) + ? new ImageTransformation.Builder(id, regexs[0], resIds[0], contentDescription) + : new ImageTransformation.Builder(id, regexs[0], resIds[0]); final int size = regexs.length; for (int i = 1; i < size; i++) { - builder.addOption(regexs[i], resIds[i]); + if (contentDescriptions[i] != null) { + builder.addOption(regexs[i], resIds[i], contentDescriptions[i]); + } else { + builder.addOption(regexs[i], resIds[i]); + } } return builder.build(); @@ -216,4 +276,16 @@ public final class ImageTransformation extends InternalTransformation implements return new ImageTransformation[size]; } }; + + private static final class Option { + public final Pattern pattern; + public final int resId; + public final CharSequence contentDescription; + + Option(Pattern pattern, int resId, CharSequence contentDescription) { + this.pattern = pattern; + this.resId = resId; + this.contentDescription = TextUtils.trimNoCopySpans(contentDescription); + } + } } diff --git a/android/service/autofill/InternalSanitizer.java b/android/service/autofill/InternalSanitizer.java new file mode 100644 index 00000000..95d2f660 --- /dev/null +++ b/android/service/autofill/InternalSanitizer.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcelable; +import android.view.autofill.AutofillValue; + +/** + * Superclass of all sanitizers the system understands. As this is not public all public subclasses + * have to implement {@link Sanitizer} again. + * + * @hide + */ +@TestApi +public abstract class InternalSanitizer implements Sanitizer, Parcelable { + + /** + * Sanitizes an {@link AutofillValue}. + * + * @hide + */ + public abstract AutofillValue sanitize(@NonNull AutofillValue value); +} diff --git a/android/service/autofill/Sanitizer.java b/android/service/autofill/Sanitizer.java new file mode 100644 index 00000000..38757ac7 --- /dev/null +++ b/android/service/autofill/Sanitizer.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +/** + * Helper class used to sanitize user input before using it in a save request. + * + * <p>Typically used to avoid displaying the save UI for values that are autofilled but reformatted + * by the app—for example, if the autofill service sends a credit card number + * value as "004815162342108" and the app automatically changes it to "0048 1516 2342 108". + */ +public interface Sanitizer { +} diff --git a/android/service/autofill/SaveCallback.java b/android/service/autofill/SaveCallback.java index 3a701384..7207f1df 100644 --- a/android/service/autofill/SaveCallback.java +++ b/android/service/autofill/SaveCallback.java @@ -34,9 +34,13 @@ public final class SaveCallback { /** * Notifies the Android System that an - * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully fulfilled + * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully handled * by the service. * + * <p>If the service could not handle the request right away—for example, because it must + * launch an activity asking the user to authenticate first or because the network is + * down—it should still call {@link #onSuccess()}. + * * @throws RuntimeException if an error occurred while calling the Android System. */ public void onSuccess() { @@ -51,9 +55,16 @@ public final class SaveCallback { /** * Notifies the Android System that an - * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} could not be fulfilled + * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} could not be handled * by the service. * + * <p>This method should only be called when the service could not handle the request right away + * and could not recover or retry it. If the service could retry or recover, it could keep + * the {@link SaveRequest} and call {@link #onSuccess()} instead. + * + * <p><b>Note:</b> The Android System displays an UI with the supplied error message; if + * you prefer to show your own message, call {@link #onSuccess()} instead. + * * @param message error message to be displayed to the user. * * @throws RuntimeException if an error occurred while calling the Android System. diff --git a/android/service/autofill/SaveInfo.java b/android/service/autofill/SaveInfo.java index e0a07305..1b9240cc 100644 --- a/android/service/autofill/SaveInfo.java +++ b/android/service/autofill/SaveInfo.java @@ -25,6 +25,8 @@ import android.app.Activity; import android.content.IntentSender; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.DebugUtils; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; @@ -232,6 +234,8 @@ public final class SaveInfo implements Parcelable { private final int mFlags; private final CustomDescription mCustomDescription; private final InternalValidator mValidator; + private final InternalSanitizer[] mSanitizerKeys; + private final AutofillId[][] mSanitizerValues; private SaveInfo(Builder builder) { mType = builder.mType; @@ -243,6 +247,18 @@ public final class SaveInfo implements Parcelable { mFlags = builder.mFlags; mCustomDescription = builder.mCustomDescription; mValidator = builder.mValidator; + if (builder.mSanitizers == null) { + mSanitizerKeys = null; + mSanitizerValues = null; + } else { + final int size = builder.mSanitizers.size(); + mSanitizerKeys = new InternalSanitizer[size]; + mSanitizerValues = new AutofillId[size][]; + for (int i = 0; i < size; i++) { + mSanitizerKeys[i] = builder.mSanitizers.keyAt(i); + mSanitizerValues[i] = builder.mSanitizers.valueAt(i); + } + } } /** @hide */ @@ -292,6 +308,18 @@ public final class SaveInfo implements Parcelable { return mValidator; } + /** @hide */ + @Nullable + public InternalSanitizer[] getSanitizerKeys() { + return mSanitizerKeys; + } + + /** @hide */ + @Nullable + public AutofillId[][] getSanitizerValues() { + return mSanitizerValues; + } + /** * A builder for {@link SaveInfo} objects. */ @@ -307,6 +335,9 @@ public final class SaveInfo implements Parcelable { private int mFlags; private CustomDescription mCustomDescription; private InternalValidator mValidator; + private ArrayMap<InternalSanitizer, AutofillId[]> mSanitizers; + // Set used to validate against duplicate ids. + private ArraySet<AutofillId> mSanitizerIds; /** * Creates a new builder. @@ -530,6 +561,61 @@ public final class SaveInfo implements Parcelable { } /** + * Adds a sanitizer for one or more field. + * + * <p>When a sanitizer is set for a field, the {@link AutofillValue} is sent to the + * sanitizer before a save request is <a href="#TriggeringSaveRequest">triggered</a>. + * + * <p>Typically used to avoid displaying the save UI for values that are autofilled but + * reformattedby the app. For example, to remove spaces between every 4 digits of a + * credit card number: + * + * <pre class="prettyprint"> + * builder.addSanitizer( + * new TextValueSanitizer(Pattern.compile("^(\\d{4}\s?\\d{4}\s?\\d{4}\s?\\d{4})$"), + * "$1$2$3$4"), ccNumberId); + * </pre> + * + * <p>The same sanitizer can be reused to sanitize multiple fields. For example, to trim + * both the username and password fields: + * + * <pre class="prettyprint"> + * builder.addSanitizer( + * new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"), + * usernameId, passwordId); + * </pre> + * + * @param sanitizer an implementation provided by the Android System. + * @param ids id of fields whose value will be sanitized. + * @return this builder. + * + * @throws IllegalArgumentException if a sanitizer for any of the {@code ids} has already + * been added or if {@code ids} is empty. + */ + public @NonNull Builder addSanitizer(@NonNull Sanitizer sanitizer, + @NonNull AutofillId... ids) { + throwIfDestroyed(); + Preconditions.checkArgument(!ArrayUtils.isEmpty(ids), "ids cannot be empty or null"); + Preconditions.checkArgument((sanitizer instanceof InternalSanitizer), + "not provided by Android System: " + sanitizer); + + if (mSanitizers == null) { + mSanitizers = new ArrayMap<>(); + mSanitizerIds = new ArraySet<>(ids.length); + } + + // Check for duplicates first. + for (AutofillId id : ids) { + Preconditions.checkArgument(!mSanitizerIds.contains(id), "already added %s", id); + mSanitizerIds.add(id); + } + + mSanitizers.put((InternalSanitizer) sanitizer, ids); + + return this; + } + + /** * Builds a new {@link SaveInfo} instance. * * @throws IllegalStateException if no @@ -569,6 +655,10 @@ public final class SaveInfo implements Parcelable { .append(", mFlags=").append(mFlags) .append(", mCustomDescription=").append(mCustomDescription) .append(", validation=").append(mValidator) + .append(", sanitizerKeys=") + .append(mSanitizerKeys == null ? "N/A:" : mSanitizerKeys.length) + .append(", sanitizerValues=") + .append(mSanitizerValues == null ? "N/A:" : mSanitizerValues.length) .append("]").toString(); } @@ -591,6 +681,12 @@ public final class SaveInfo implements Parcelable { parcel.writeCharSequence(mDescription); parcel.writeParcelable(mCustomDescription, flags); parcel.writeParcelable(mValidator, flags); + parcel.writeParcelableArray(mSanitizerKeys, flags); + if (mSanitizerKeys != null) { + for (int i = 0; i < mSanitizerValues.length; i++) { + parcel.writeParcelableArray(mSanitizerValues[i], flags); + } + } parcel.writeInt(mFlags); } @@ -621,6 +717,16 @@ public final class SaveInfo implements Parcelable { if (validator != null) { builder.setValidator(validator); } + final InternalSanitizer[] sanitizers = + parcel.readParcelableArray(null, InternalSanitizer.class); + if (sanitizers != null) { + final int size = sanitizers.length; + for (int i = 0; i < size; i++) { + final AutofillId[] autofillIds = + parcel.readParcelableArray(null, AutofillId.class); + builder.addSanitizer(sanitizers[i], autofillIds); + } + } builder.setFlags(parcel.readInt()); return builder.build(); } diff --git a/android/service/autofill/SaveRequest.java b/android/service/autofill/SaveRequest.java index 1a6c5b0b..65fdb5c4 100644 --- a/android/service/autofill/SaveRequest.java +++ b/android/service/autofill/SaveRequest.java @@ -48,7 +48,8 @@ public final class SaveRequest implements Parcelable { } private SaveRequest(@NonNull Parcel parcel) { - this(parcel.readTypedArrayList(null), parcel.readBundle(), parcel.createStringArrayList()); + this(parcel.createTypedArrayList(FillContext.CREATOR), + parcel.readBundle(), parcel.createStringArrayList()); } /** @@ -84,7 +85,7 @@ public final class SaveRequest implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { - parcel.writeTypedArrayList(mFillContexts, flags); + parcel.writeTypedList(mFillContexts, flags); parcel.writeBundle(mClientState); parcel.writeStringList(mDatasetIds); } diff --git a/android/service/autofill/TextValueSanitizer.java b/android/service/autofill/TextValueSanitizer.java new file mode 100644 index 00000000..12e85b1d --- /dev/null +++ b/android/service/autofill/TextValueSanitizer.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Slog; +import android.view.autofill.AutofillValue; + +import com.android.internal.util.Preconditions; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Sanitizes a text {@link AutofillValue} using a regular expression (regex) substitution. + * + * <p>For example, to remove spaces from groups of 4-digits in a credit card: + * + * <pre class="prettyprint"> + * new TextValueSanitizer(Pattern.compile("^(\\d{4}\s?\\d{4}\s?\\d{4}\s?\\d{4})$"), "$1$2$3$4") + * </pre> + */ +public final class TextValueSanitizer extends InternalSanitizer implements + Sanitizer, Parcelable { + private static final String TAG = "TextValueSanitizer"; + + private final Pattern mRegex; + private final String mSubst; + + /** + * Default constructor. + * + * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that + * are used to substitute parts of the {@link AutofillValue#getTextValue() text value}. + * @param subst the string that substitutes the matched regex, using {@code $} for + * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc). + */ + public TextValueSanitizer(@NonNull Pattern regex, @NonNull String subst) { + mRegex = Preconditions.checkNotNull(regex); + mSubst = Preconditions.checkNotNull(subst); + } + + /** @hide */ + @Override + @TestApi + public AutofillValue sanitize(@NonNull AutofillValue value) { + if (value == null) { + Slog.w(TAG, "sanitize() called with null value"); + return null; + } + if (!value.isText()) return value; + + final CharSequence text = value.getTextValue(); + + try { + final Matcher matcher = mRegex.matcher(text); + if (!matcher.matches()) return value; + + final CharSequence sanitized = matcher.replaceAll(mSubst); + return AutofillValue.forText(sanitized); + } catch (Exception e) { + Slog.w(TAG, "Exception evaluating " + mRegex + "/" + mSubst + ": " + e); + return value; + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "TextValueSanitizer: [regex=" + mRegex + ", subst=" + mSubst + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeSerializable(mRegex); + parcel.writeString(mSubst); + } + + public static final Parcelable.Creator<TextValueSanitizer> CREATOR = + new Parcelable.Creator<TextValueSanitizer>() { + @Override + public TextValueSanitizer createFromParcel(Parcel parcel) { + return new TextValueSanitizer((Pattern) parcel.readSerializable(), parcel.readString()); + } + + @Override + public TextValueSanitizer[] newArray(int size) { + return new TextValueSanitizer[size]; + } + }; +} diff --git a/android/service/carrier/CarrierService.java b/android/service/carrier/CarrierService.java index 813acc23..2707f146 100644 --- a/android/service/carrier/CarrierService.java +++ b/android/service/carrier/CarrierService.java @@ -17,10 +17,13 @@ package android.service.carrier; import android.annotation.CallSuper; import android.app.Service; import android.content.Intent; +import android.os.Bundle; import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.ServiceManager; +import android.util.Log; import com.android.internal.telephony.ITelephonyRegistry; @@ -48,6 +51,8 @@ import com.android.internal.telephony.ITelephonyRegistry; */ public abstract class CarrierService extends Service { + private static final String LOG_TAG = "CarrierService"; + public static final String CARRIER_SERVICE_INTERFACE = "android.service.carrier.CarrierService"; private static ITelephonyRegistry sRegistry; @@ -133,11 +138,26 @@ public abstract class CarrierService extends Service { /** * A wrapper around ICarrierService that forwards calls to implementations of * {@link CarrierService}. + * @hide */ - private class ICarrierServiceWrapper extends ICarrierService.Stub { + public class ICarrierServiceWrapper extends ICarrierService.Stub { + /** @hide */ + public static final int RESULT_OK = 0; + /** @hide */ + public static final int RESULT_ERROR = 1; + /** @hide */ + public static final String KEY_CONFIG_BUNDLE = "config_bundle"; + @Override - public PersistableBundle getCarrierConfig(CarrierIdentifier id) { - return CarrierService.this.onLoadConfig(id); + public void getCarrierConfig(CarrierIdentifier id, ResultReceiver result) { + try { + Bundle data = new Bundle(); + data.putParcelable(KEY_CONFIG_BUNDLE, CarrierService.this.onLoadConfig(id)); + result.send(RESULT_OK, data); + } catch (Exception e) { + Log.e(LOG_TAG, "Error in onLoadConfig: " + e.getMessage(), e); + result.send(RESULT_ERROR, null); + } } } } diff --git a/android/service/notification/Adjustment.java b/android/service/notification/Adjustment.java index ce678fc8..7348cf68 100644 --- a/android/service/notification/Adjustment.java +++ b/android/service/notification/Adjustment.java @@ -56,6 +56,15 @@ public final class Adjustment implements Parcelable { public static final String KEY_GROUP_KEY = "key_group_key"; /** + * Data type: int, one of {@link NotificationListenerService.Ranking#USER_SENTIMENT_POSITIVE}, + * {@link NotificationListenerService.Ranking#USER_SENTIMENT_NEUTRAL}, + * {@link NotificationListenerService.Ranking#USER_SENTIMENT_NEGATIVE}. Used to express how + * a user feels about notifications in the same {@link android.app.NotificationChannel} as + * the notification represented by {@link #getKey()}. + */ + public static final String KEY_USER_SENTIMENT = "key_user_sentiment"; + + /** * Create a notification adjustment. * * @param pkg The package of the notification. diff --git a/android/service/notification/NotificationAssistantService.java b/android/service/notification/NotificationAssistantService.java index d94017cd..8e52bfa8 100644 --- a/android/service/notification/NotificationAssistantService.java +++ b/android/service/notification/NotificationAssistantService.java @@ -16,12 +16,9 @@ package android.service.notification; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.NotificationChannel; import android.content.Context; import android.content.Intent; import android.os.Handler; @@ -30,9 +27,9 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; + import com.android.internal.os.SomeArgs; -import java.util.ArrayList; import java.util.List; /** @@ -79,7 +76,7 @@ public abstract class NotificationAssistantService extends NotificationListenerS String snoozeCriterionId); /** - * A notification was posted by an app. Called before alert. + * A notification was posted by an app. Called before post. * * @param sbn the new notification * @return an adjustment or null to take no action, within 100ms. @@ -87,6 +84,34 @@ public abstract class NotificationAssistantService extends NotificationListenerS abstract public Adjustment onNotificationEnqueued(StatusBarNotification sbn); /** + * Implement this method to learn when notifications are removed, how they were interacted with + * before removal, and why they were removed. + * <p> + * This might occur because the user has dismissed the notification using system UI (or another + * notification listener) or because the app has withdrawn the notification. + * <p> + * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the + * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight + * fields such as {@link android.app.Notification#contentView} and + * {@link android.app.Notification#largeIcon}. However, all other fields on + * {@link StatusBarNotification}, sufficient to match this call with a prior call to + * {@link #onNotificationPosted(StatusBarNotification)}, will be intact. + * + ** @param sbn A data structure encapsulating at least the original information (tag and id) + * and source (package name) used to post the {@link android.app.Notification} that + * was just removed. + * @param rankingMap The current ranking map that can be used to retrieve ranking information + * for active notifications. + * @param stats Stats about how the user interacted with the notification before it was removed. + * @param reason see {@link #REASON_LISTENER_CANCEL}, etc. + */ + @Override + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, + NotificationStats stats, int reason) { + onNotificationRemoved(sbn, rankingMap, reason); + } + + /** * Updates a notification. N.B. this won’t cause * an existing notification to alert, but might allow a future update to * this notification to alert. diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java index a5223fd8..08d3118b 100644 --- a/android/service/notification/NotificationListenerService.java +++ b/android/service/notification/NotificationListenerService.java @@ -20,6 +20,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SdkConstant; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.app.INotificationManager; import android.app.Notification; import android.app.Notification.Builder; @@ -265,7 +266,10 @@ public abstract class NotificationListenerService extends Service { @GuardedBy("mLock") private RankingMap mRankingMap; - private INotificationManager mNoMan; + /** + * @hide + */ + protected INotificationManager mNoMan; /** * Only valid after a successful call to (@link registerAsService}. @@ -389,6 +393,18 @@ public abstract class NotificationListenerService extends Service { } /** + * NotificationStats are not populated for notification listeners, so fall back to + * {@link #onNotificationRemoved(StatusBarNotification, RankingMap, int)}. + * + * @hide + */ + @TestApi + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, + NotificationStats stats, int reason) { + onNotificationRemoved(sbn, rankingMap, reason); + } + + /** * Implement this method to learn about when the listener is enabled and connected to * the notification manager. You are safe to call {@link #getActiveNotifications()} * at this time. @@ -1200,7 +1216,7 @@ public abstract class NotificationListenerService extends Service { @Override public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder, - NotificationRankingUpdate update, int reason) { + NotificationRankingUpdate update, NotificationStats stats, int reason) { StatusBarNotification sbn; try { sbn = sbnHolder.get(); @@ -1215,6 +1231,7 @@ public abstract class NotificationListenerService extends Service { args.arg1 = sbn; args.arg2 = mRankingMap; args.arg3 = reason; + args.arg4 = stats; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_REMOVED, args).sendToTarget(); } @@ -1324,6 +1341,26 @@ public abstract class NotificationListenerService extends Service { * @hide */ public static final int VISIBILITY_NO_OVERRIDE = NotificationManager.VISIBILITY_NO_OVERRIDE; + /** + * The user is likely to have a negative reaction to this notification. + */ + public static final int USER_SENTIMENT_NEGATIVE = -1; + /** + * It is not known how the user will react to this notification. + */ + public static final int USER_SENTIMENT_NEUTRAL = 0; + /** + * The user is likely to have a positive reaction to this notification. + */ + public static final int USER_SENTIMENT_POSITIVE = 1; + + /** @hide */ + @IntDef(prefix = { "USER_SENTIMENT_" }, value = { + USER_SENTIMENT_NEGATIVE, USER_SENTIMENT_NEUTRAL, USER_SENTIMENT_POSITIVE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface UserSentiment {} + private String mKey; private int mRank = -1; private boolean mIsAmbient; @@ -1341,6 +1378,7 @@ public abstract class NotificationListenerService extends Service { // Notification assistant snooze criteria. private ArrayList<SnoozeCriterion> mSnoozeCriteria; private boolean mShowBadge; + private @UserSentiment int mUserSentiment = USER_SENTIMENT_NEUTRAL; public Ranking() {} @@ -1436,6 +1474,17 @@ public abstract class NotificationListenerService extends Service { } /** + * Returns how the system thinks the user feels about notifications from the + * channel provided by {@link #getChannel()}. You can use this information to expose + * controls to help the user block this channel's notifications, if the sentiment is + * {@link #USER_SENTIMENT_NEGATIVE}, or emphasize this notification if the sentiment is + * {@link #USER_SENTIMENT_POSITIVE}. + */ + public int getUserSentiment() { + return mUserSentiment; + } + + /** * If the {@link NotificationAssistantService} has added people to this notification, then * this will be non-null. * @hide @@ -1471,7 +1520,8 @@ public abstract class NotificationListenerService extends Service { int visibilityOverride, int suppressedVisualEffects, int importance, CharSequence explanation, String overrideGroupKey, NotificationChannel channel, ArrayList<String> overridePeople, - ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge) { + ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge, + int userSentiment) { mKey = key; mRank = rank; mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; @@ -1485,6 +1535,7 @@ public abstract class NotificationListenerService extends Service { mOverridePeople = overridePeople; mSnoozeCriteria = snoozeCriteria; mShowBadge = showBadge; + mUserSentiment = userSentiment; } /** @@ -1532,6 +1583,7 @@ public abstract class NotificationListenerService extends Service { private ArrayMap<String, ArrayList<String>> mOverridePeople; private ArrayMap<String, ArrayList<SnoozeCriterion>> mSnoozeCriteria; private ArrayMap<String, Boolean> mShowBadge; + private ArrayMap<String, Integer> mUserSentiment; private RankingMap(NotificationRankingUpdate rankingUpdate) { mRankingUpdate = rankingUpdate; @@ -1560,7 +1612,7 @@ public abstract class NotificationListenerService extends Service { getVisibilityOverride(key), getSuppressedVisualEffects(key), getImportance(key), getImportanceExplanation(key), getOverrideGroupKey(key), getChannel(key), getOverridePeople(key), getSnoozeCriteria(key), - getShowBadge(key)); + getShowBadge(key), getUserSentiment(key)); return rank >= 0; } @@ -1677,6 +1729,17 @@ public abstract class NotificationListenerService extends Service { return showBadge == null ? false : showBadge.booleanValue(); } + private int getUserSentiment(String key) { + synchronized (this) { + if (mUserSentiment == null) { + buildUserSentimentLocked(); + } + } + Integer userSentiment = mUserSentiment.get(key); + return userSentiment == null + ? Ranking.USER_SENTIMENT_NEUTRAL : userSentiment.intValue(); + } + // Locked by 'this' private void buildRanksLocked() { String[] orderedKeys = mRankingUpdate.getOrderedKeys(); @@ -1776,6 +1839,15 @@ public abstract class NotificationListenerService extends Service { } } + // Locked by 'this' + private void buildUserSentimentLocked() { + Bundle userSentiment = mRankingUpdate.getUserSentiment(); + mUserSentiment = new ArrayMap<>(userSentiment.size()); + for (String key : userSentiment.keySet()) { + mUserSentiment.put(key, userSentiment.getInt(key)); + } + } + // ----------- Parcelable @Override @@ -1835,8 +1907,9 @@ public abstract class NotificationListenerService extends Service { StatusBarNotification sbn = (StatusBarNotification) args.arg1; RankingMap rankingMap = (RankingMap) args.arg2; int reason = (int) args.arg3; + NotificationStats stats = (NotificationStats) args.arg4; args.recycle(); - onNotificationRemoved(sbn, rankingMap, reason); + onNotificationRemoved(sbn, rankingMap, stats, reason); } break; case MSG_ON_LISTENER_CONNECTED: { diff --git a/android/service/notification/NotificationRankingUpdate.java b/android/service/notification/NotificationRankingUpdate.java index 326b212a..6d51db09 100644 --- a/android/service/notification/NotificationRankingUpdate.java +++ b/android/service/notification/NotificationRankingUpdate.java @@ -35,12 +35,13 @@ public class NotificationRankingUpdate implements Parcelable { private final Bundle mOverridePeople; private final Bundle mSnoozeCriteria; private final Bundle mShowBadge; + private final Bundle mUserSentiment; public NotificationRankingUpdate(String[] keys, String[] interceptedKeys, Bundle visibilityOverrides, Bundle suppressedVisualEffects, int[] importance, Bundle explanation, Bundle overrideGroupKeys, Bundle channels, Bundle overridePeople, Bundle snoozeCriteria, - Bundle showBadge) { + Bundle showBadge, Bundle userSentiment) { mKeys = keys; mInterceptedKeys = interceptedKeys; mVisibilityOverrides = visibilityOverrides; @@ -52,6 +53,7 @@ public class NotificationRankingUpdate implements Parcelable { mOverridePeople = overridePeople; mSnoozeCriteria = snoozeCriteria; mShowBadge = showBadge; + mUserSentiment = userSentiment; } public NotificationRankingUpdate(Parcel in) { @@ -67,6 +69,7 @@ public class NotificationRankingUpdate implements Parcelable { mOverridePeople = in.readBundle(); mSnoozeCriteria = in.readBundle(); mShowBadge = in.readBundle(); + mUserSentiment = in.readBundle(); } @Override @@ -87,6 +90,7 @@ public class NotificationRankingUpdate implements Parcelable { out.writeBundle(mOverridePeople); out.writeBundle(mSnoozeCriteria); out.writeBundle(mShowBadge); + out.writeBundle(mUserSentiment); } public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR @@ -143,4 +147,8 @@ public class NotificationRankingUpdate implements Parcelable { public Bundle getShowBadge() { return mShowBadge; } + + public Bundle getUserSentiment() { + return mUserSentiment; + } } diff --git a/android/service/notification/NotificationStats.java b/android/service/notification/NotificationStats.java new file mode 100644 index 00000000..76d5328d --- /dev/null +++ b/android/service/notification/NotificationStats.java @@ -0,0 +1,256 @@ +/** + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.notification; + +import android.annotation.IntDef; +import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.app.RemoteInput; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @hide + */ +@TestApi +@SystemApi +public final class NotificationStats implements Parcelable { + + private boolean mSeen; + private boolean mExpanded; + private boolean mDirectReplied; + private boolean mSnoozed; + private boolean mViewedSettings; + private boolean mInteracted; + + /** @hide */ + @IntDef(prefix = { "DISMISSAL_SURFACE_" }, value = { + DISMISSAL_NOT_DISMISSED, DISMISSAL_OTHER, DISMISSAL_PEEK, DISMISSAL_AOD, DISMISSAL_SHADE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DismissalSurface {} + + + private @DismissalSurface int mDismissalSurface = DISMISSAL_NOT_DISMISSED; + + /** + * Notification has not been dismissed yet. + */ + public static final int DISMISSAL_NOT_DISMISSED = -1; + /** + * Notification has been dismissed from a {@link NotificationListenerService} or the app + * itself. + */ + public static final int DISMISSAL_OTHER = 0; + /** + * Notification has been dismissed while peeking. + */ + public static final int DISMISSAL_PEEK = 1; + /** + * Notification has been dismissed from always on display. + */ + public static final int DISMISSAL_AOD = 2; + /** + * Notification has been dismissed from the notification shade. + */ + public static final int DISMISSAL_SHADE = 3; + + public NotificationStats() { + } + + protected NotificationStats(Parcel in) { + mSeen = in.readByte() != 0; + mExpanded = in.readByte() != 0; + mDirectReplied = in.readByte() != 0; + mSnoozed = in.readByte() != 0; + mViewedSettings = in.readByte() != 0; + mInteracted = in.readByte() != 0; + mDismissalSurface = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte((byte) (mSeen ? 1 : 0)); + dest.writeByte((byte) (mExpanded ? 1 : 0)); + dest.writeByte((byte) (mDirectReplied ? 1 : 0)); + dest.writeByte((byte) (mSnoozed ? 1 : 0)); + dest.writeByte((byte) (mViewedSettings ? 1 : 0)); + dest.writeByte((byte) (mInteracted ? 1 : 0)); + dest.writeInt(mDismissalSurface); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<NotificationStats> CREATOR = new Creator<NotificationStats>() { + @Override + public NotificationStats createFromParcel(Parcel in) { + return new NotificationStats(in); + } + + @Override + public NotificationStats[] newArray(int size) { + return new NotificationStats[size]; + } + }; + + /** + * Returns whether the user has seen this notification at least once. + */ + public boolean hasSeen() { + return mSeen; + } + + /** + * Records that the user as seen this notification at least once. + */ + public void setSeen() { + mSeen = true; + } + + /** + * Returns whether the user has expanded this notification at least once. + */ + public boolean hasExpanded() { + return mExpanded; + } + + /** + * Records that the user has expanded this notification at least once. + */ + public void setExpanded() { + mExpanded = true; + mInteracted = true; + } + + /** + * Returns whether the user has replied to a notification that has a + * {@link android.app.Notification.Action.Builder#addRemoteInput(RemoteInput) direct reply} at + * least once. + */ + public boolean hasDirectReplied() { + return mDirectReplied; + } + + /** + * Records that the user has replied to a notification that has a + * {@link android.app.Notification.Action.Builder#addRemoteInput(RemoteInput) direct reply} + * at least once. + */ + public void setDirectReplied() { + mDirectReplied = true; + mInteracted = true; + } + + /** + * Returns whether the user has snoozed this notification at least once. + */ + public boolean hasSnoozed() { + return mSnoozed; + } + + /** + * Records that the user has snoozed this notification at least once. + */ + public void setSnoozed() { + mSnoozed = true; + mInteracted = true; + } + + /** + * Returns whether the user has viewed the in-shade settings for this notification at least + * once. + */ + public boolean hasViewedSettings() { + return mViewedSettings; + } + + /** + * Records that the user has viewed the in-shade settings for this notification at least once. + */ + public void setViewedSettings() { + mViewedSettings = true; + mInteracted = true; + } + + /** + * Returns whether the user has interacted with this notification beyond having viewed it. + */ + public boolean hasInteracted() { + return mInteracted; + } + + /** + * Returns from which surface the notification was dismissed. + */ + public @DismissalSurface int getDismissalSurface() { + return mDismissalSurface; + } + + /** + * Returns from which surface the notification was dismissed. + */ + public void setDismissalSurface(@DismissalSurface int dismissalSurface) { + mDismissalSurface = dismissalSurface; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NotificationStats that = (NotificationStats) o; + + if (mSeen != that.mSeen) return false; + if (mExpanded != that.mExpanded) return false; + if (mDirectReplied != that.mDirectReplied) return false; + if (mSnoozed != that.mSnoozed) return false; + if (mViewedSettings != that.mViewedSettings) return false; + if (mInteracted != that.mInteracted) return false; + return mDismissalSurface == that.mDismissalSurface; + } + + @Override + public int hashCode() { + int result = (mSeen ? 1 : 0); + result = 31 * result + (mExpanded ? 1 : 0); + result = 31 * result + (mDirectReplied ? 1 : 0); + result = 31 * result + (mSnoozed ? 1 : 0); + result = 31 * result + (mViewedSettings ? 1 : 0); + result = 31 * result + (mInteracted ? 1 : 0); + result = 31 * result + mDismissalSurface; + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("NotificationStats{"); + sb.append("mSeen=").append(mSeen); + sb.append(", mExpanded=").append(mExpanded); + sb.append(", mDirectReplied=").append(mDirectReplied); + sb.append(", mSnoozed=").append(mSnoozed); + sb.append(", mViewedSettings=").append(mViewedSettings); + sb.append(", mInteracted=").append(mInteracted); + sb.append(", mDismissalSurface=").append(mDismissalSurface); + sb.append('}'); + return sb.toString(); + } +} diff --git a/android/service/settings/suggestions/Suggestion.java b/android/service/settings/suggestions/Suggestion.java index f27cc2eb..cfeb7fce 100644 --- a/android/service/settings/suggestions/Suggestion.java +++ b/android/service/settings/suggestions/Suggestion.java @@ -16,12 +16,17 @@ package android.service.settings.suggestions; +import android.annotation.IntDef; import android.annotation.SystemApi; import android.app.PendingIntent; +import android.graphics.drawable.Icon; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Data object that has information about a device suggestion. * @@ -30,9 +35,27 @@ import android.text.TextUtils; @SystemApi public final class Suggestion implements Parcelable { + /** + * @hide + */ + @IntDef(flag = true, value = { + FLAG_HAS_BUTTON, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Flags { + } + + /** + * Flag for suggestion type with a single button + */ + public static final int FLAG_HAS_BUTTON = 1 << 0; + private final String mId; private final CharSequence mTitle; private final CharSequence mSummary; + private final Icon mIcon; + @Flags + private final int mFlags; private final PendingIntent mPendingIntent; /** @@ -57,6 +80,22 @@ public final class Suggestion implements Parcelable { } /** + * Optional icon for this suggestion. + */ + public Icon getIcon() { + return mIcon; + } + + /** + * Optional flags for this suggestion. This will influence UI when rendering suggestion in + * different style. + */ + @Flags + public int getFlags() { + return mFlags; + } + + /** * The Intent to launch when the suggestion is activated. */ public PendingIntent getPendingIntent() { @@ -67,6 +106,8 @@ public final class Suggestion implements Parcelable { mId = builder.mId; mTitle = builder.mTitle; mSummary = builder.mSummary; + mIcon = builder.mIcon; + mFlags = builder.mFlags; mPendingIntent = builder.mPendingIntent; } @@ -74,6 +115,8 @@ public final class Suggestion implements Parcelable { mId = in.readString(); mTitle = in.readCharSequence(); mSummary = in.readCharSequence(); + mIcon = in.readParcelable(Icon.class.getClassLoader()); + mFlags = in.readInt(); mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader()); } @@ -99,6 +142,8 @@ public final class Suggestion implements Parcelable { dest.writeString(mId); dest.writeCharSequence(mTitle); dest.writeCharSequence(mSummary); + dest.writeParcelable(mIcon, flags); + dest.writeInt(mFlags); dest.writeParcelable(mPendingIntent, flags); } @@ -109,6 +154,9 @@ public final class Suggestion implements Parcelable { private final String mId; private CharSequence mTitle; private CharSequence mSummary; + private Icon mIcon; + @Flags + private int mFlags; private PendingIntent mPendingIntent; public Builder(String id) { @@ -135,6 +183,23 @@ public final class Suggestion implements Parcelable { } /** + * Sets icon for the suggestion. + */ + public Builder setIcon(Icon icon) { + mIcon = icon; + return this; + } + + /** + * Sets a UI type for this suggestion. This will influence UI when rendering suggestion in + * different style. + */ + public Builder setFlags(@Flags int flags) { + mFlags = flags; + return this; + } + + /** * Sets suggestion intent */ public Builder setPendingIntent(PendingIntent pendingIntent) { diff --git a/android/slice/Slice.java b/android/slice/Slice.java new file mode 100644 index 00000000..57686548 --- /dev/null +++ b/android/slice/Slice.java @@ -0,0 +1,347 @@ +/* + * 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.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; + +import android.annotation.NonNull; +import android.annotation.StringDef; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.widget.RemoteViews; + +import com.android.internal.util.ArrayUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * 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 { + + /** + * @hide + */ + @StringDef({HINT_TITLE, HINT_LIST, HINT_LIST_ITEM, HINT_LARGE, HINT_ACTIONS, HINT_SELECTED, + HINT_SOURCE, HINT_MESSAGE, HINT_HORIZONTAL, HINT_NO_TINT}) + public @interface SliceHint{ } + + /** + * Hint that this content is a title of other content in the slice. + */ + public static final String HINT_TITLE = "title"; + /** + * Hint that all sub-items/sub-slices within this content should be considered + * to have {@link #HINT_LIST_ITEM}. + */ + public static final String HINT_LIST = "list"; + /** + * Hint that this item is part of a list and should be formatted as if is part + * of a list. + */ + public static final String HINT_LIST_ITEM = "list_item"; + /** + * Hint that this content is important and should be larger when displayed if + * possible. + */ + public static final String HINT_LARGE = "large"; + /** + * Hint that this slice contains a number of actions that can be grouped together + * in a sort of controls area of the UI. + */ + public static final String HINT_ACTIONS = "actions"; + /** + * Hint indicating that this item (and its sub-items) are the current selection. + */ + public static final String HINT_SELECTED = "selected"; + /** + * Hint to indicate that this is a message as part of a communication + * sequence in this slice. + */ + public static final String HINT_MESSAGE = "message"; + /** + * Hint to tag the source (i.e. sender) of a {@link #HINT_MESSAGE}. + */ + public static final String HINT_SOURCE = "source"; + /** + * Hint that list items within this slice or subslice would appear better + * if organized horizontally. + */ + public static final String HINT_HORIZONTAL = "horizontal"; + /** + * Hint to indicate that this content should not be tinted. + */ + public static final String HINT_NO_TINT = "no_tint"; + + // These two are coming over from prototyping, but we probably don't want in + // public API, at least not right now. + /** + * @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) { + mHints = hints; + mItems = items.toArray(new SliceItem[items.size()]); + mUri = uri; + } + + protected Slice(Parcel in) { + mHints = in.readStringArray(); + int n = in.readInt(); + mItems = new SliceItem[n]; + for (int i = 0; i < n; i++) { + mItems[i] = SliceItem.CREATOR.createFromParcel(in); + } + mUri = Uri.CREATOR.createFromParcel(in); + } + + /** + * @return The Uri that this Slice represents. + */ + public Uri getUri() { + return mUri; + } + + /** + * @return All child {@link SliceItem}s that this Slice contains. + */ + public SliceItem[] getItems() { + return mItems; + } + + /** + * @return All hints associated with this Slice. + */ + public @SliceHint String[] getHints() { + return mHints; + } + + /** + * @hide + */ + public SliceItem getPrimaryIcon() { + for (SliceItem item : getItems()) { + if (item.getType() == TYPE_IMAGE) { + return item; + } + if (!(item.getType() == 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); + if (icon != null) return icon; + } + } + return null; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringArray(mHints); + dest.writeInt(mItems.length); + for (int i = 0; i < mItems.length; i++) { + mItems[i].writeToParcel(dest, flags); + } + mUri.writeToParcel(dest, 0); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * @hide + */ + public boolean hasHint(@SliceHint String hint) { + return ArrayUtils.contains(mHints, hint); + } + + /** + * A Builder used to construct {@link Slice}s + */ + public static class Builder { + + private final Uri mUri; + private ArrayList<SliceItem> mItems = new ArrayList<>(); + private @SliceHint ArrayList<String> mHints = new ArrayList<>(); + + /** + * Create a builder which will construct a {@link Slice} for the Given Uri. + * @param uri Uri to tag for this slice. + */ + public Builder(@NonNull Uri uri) { + mUri = uri; + } + + /** + * Create a builder for a {@link Slice} that is a sub-slice of the slice + * being constructed by the provided builder. + * @param parent The builder constructing the parent slice + */ + public Builder(@NonNull Slice.Builder parent) { + mUri = parent.mUri.buildUpon().appendPath("_gen") + .appendPath(String.valueOf(mItems.size())).build(); + } + + /** + * Add hints to the Slice being constructed + */ + public Builder addHints(@SliceHint String... hints) { + mHints.addAll(Arrays.asList(hints)); + return this; + } + + /** + * Add a sub-slice to the slice being constructed + */ + public Builder addSubSlice(@NonNull Slice slice) { + mItems.add(new SliceItem(slice, TYPE_SLICE, slice.getHints())); + return this; + } + + /** + * 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])); + return this; + } + + /** + * Add text to the slice being constructed + */ + public Builder addText(CharSequence text, @SliceHint String... hints) { + mItems.add(new SliceItem(text, TYPE_TEXT, hints)); + return this; + } + + /** + * Add an image to the slice being constructed + */ + public Builder addIcon(Icon icon, @SliceHint String... hints) { + mItems.add(new SliceItem(icon, TYPE_IMAGE, hints)); + return this; + } + + /** + * @hide This isn't final + */ + public Builder addRemoteView(RemoteViews remoteView, @SliceHint String... hints) { + mItems.add(new SliceItem(remoteView, TYPE_REMOTE_VIEW, hints)); + return this; + } + + /** + * 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)); + return this; + } + + /** + * Add a color to the slice being constructed + */ + public Builder addColor(int color, @SliceHint String... hints) { + mItems.add(new SliceItem(color, TYPE_COLOR, hints)); + return this; + } + + /** + * 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)); + return this; + } + + /** + * Construct the slice. + */ + public Slice build() { + return new Slice(mItems, mHints.toArray(new String[mHints.size()]), mUri); + } + } + + public static final Creator<Slice> CREATOR = new Creator<Slice>() { + @Override + public Slice createFromParcel(Parcel in) { + return new Slice(in); + } + + @Override + public Slice[] newArray(int size) { + return new Slice[size]; + } + }; + + /** + * @hide + * @return A string representation of this slice. + */ + public String getString() { + return getString(""); + } + + private String getString(String indent) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mItems.length; i++) { + sb.append(indent); + if (mItems[i].getType() == TYPE_SLICE) { + sb.append("slice:\n"); + sb.append(mItems[i].getSlice().getString(indent + " ")); + } else if (mItems[i].getType() == TYPE_TEXT) { + sb.append("text: "); + sb.append(mItems[i].getText()); + sb.append("\n"); + } else { + sb.append(SliceItem.typeToString(mItems[i].getType())); + sb.append("\n"); + } + } + return sb.toString(); + } +} diff --git a/android/slice/SliceItem.java b/android/slice/SliceItem.java new file mode 100644 index 00000000..2827ab9d --- /dev/null +++ b/android/slice/SliceItem.java @@ -0,0 +1,344 @@ +/* + * 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.slice; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.app.PendingIntent; +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; + + +/** + * A SliceItem is a single unit in the tree structure of a {@link Slice}. + * + * A SliceItem a piece of content and some hints about what that content + * means or how it should be displayed. The types of content can be: + * <li>{@link #TYPE_SLICE}</li> + * <li>{@link #TYPE_TEXT}</li> + * <li>{@link #TYPE_IMAGE}</li> + * <li>{@link #TYPE_ACTION}</li> + * <li>{@link #TYPE_COLOR}</li> + * <li>{@link #TYPE_TIMESTAMP}</li> + * <li>{@link #TYPE_REMOTE_INPUT}</li> + * + * The hints that a {@link SliceItem} are a set of strings which annotate + * the content. The hints that are guaranteed to be understood by the system + * are defined on {@link Slice}. + * @hide + */ +public final class SliceItem implements Parcelable { + + /** + * @hide + */ + @IntDef({TYPE_SLICE, TYPE_TEXT, TYPE_IMAGE, TYPE_ACTION, TYPE_COLOR, + TYPE_TIMESTAMP, TYPE_REMOTE_INPUT}) + public @interface SliceType {} + + /** + * A {@link SliceItem} that contains a {@link Slice} + */ + public static final int TYPE_SLICE = 1; + /** + * A {@link SliceItem} that contains a {@link CharSequence} + */ + public static final int TYPE_TEXT = 2; + /** + * A {@link SliceItem} that contains an {@link Icon} + */ + public static final int TYPE_IMAGE = 3; + /** + * A {@link SliceItem} that contains a {@link PendingIntent} + * + * Note: Actions contain 2 pieces of data, In addition to the pending intent, the + * item contains a {@link Slice} that the action applies to. + */ + public static final int TYPE_ACTION = 4; + /** + * @hide This isn't final + */ + public static final int TYPE_REMOTE_VIEW = 5; + /** + * A {@link SliceItem} that contains a Color int. + */ + public static final int TYPE_COLOR = 6; + /** + * A {@link SliceItem} that contains a timestamp. + */ + public static final int TYPE_TIMESTAMP = 8; + /** + * A {@link SliceItem} that contains a {@link RemoteInput}. + */ + public static final int TYPE_REMOTE_INPUT = 9; + + /** + * @hide + */ + protected @SliceHint String[] mHints; + private final int mType; + private final Object mObj; + + /** + * @hide + */ + public SliceItem(Object obj, @SliceType int type, @SliceHint String[] hints) { + mHints = hints; + mType = type; + mObj = obj; + } + + /** + * @hide + */ + public SliceItem(PendingIntent intent, Slice slice, int type, @SliceHint String[] hints) { + this(new Pair<>(intent, slice), type, hints); + } + + /** + * Gets all hints associated with this SliceItem. + * @return Array of hints. + */ + public @NonNull @SliceHint String[] getHints() { + return mHints; + } + + /** + * @hide + */ + public void addHint(@SliceHint String hint) { + mHints = ArrayUtils.appendElement(String.class, mHints, hint); + } + + /** + * @hide + */ + public void removeHint(String hint) { + ArrayUtils.removeElement(String.class, mHints, hint); + } + + public @SliceType int getType() { + return mType; + } + + /** + * @return The text held by this {@link #TYPE_TEXT} SliceItem + */ + public CharSequence getText() { + return (CharSequence) mObj; + } + + /** + * @return The icon held by this {@link #TYPE_IMAGE} SliceItem + */ + public Icon getIcon() { + return (Icon) mObj; + } + + /** + * @return The pending intent held by this {@link #TYPE_ACTION} SliceItem + */ + public PendingIntent getAction() { + return ((Pair<PendingIntent, Slice>) mObj).first; + } + + /** + * @hide This isn't final + */ + public RemoteViews getRemoteView() { + return (RemoteViews) mObj; + } + + /** + * @return The remote input held by this {@link #TYPE_REMOTE_INPUT} SliceItem + */ + public RemoteInput getRemoteInput() { + return (RemoteInput) mObj; + } + + /** + * @return The color held by this {@link #TYPE_COLOR} SliceItem + */ + public int getColor() { + return (Integer) mObj; + } + + /** + * @return The slice held by this {@link #TYPE_ACTION} or {@link #TYPE_SLICE} SliceItem + */ + public Slice getSlice() { + if (getType() == TYPE_ACTION) { + return ((Pair<PendingIntent, Slice>) mObj).second; + } + return (Slice) mObj; + } + + /** + * @return The timestamp held by this {@link #TYPE_TIMESTAMP} SliceItem + */ + public long getTimestamp() { + return (Long) mObj; + } + + /** + * @param hint The hint to check for + * @return true if this item contains the given hint + */ + public boolean hasHint(@SliceHint String hint) { + return ArrayUtils.contains(mHints, hint); + } + + /** + * @hide + */ + public SliceItem(Parcel in) { + mHints = in.readStringArray(); + mType = in.readInt(); + mObj = readObj(mType, in); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringArray(mHints); + dest.writeInt(mType); + writeObj(dest, flags, mObj, mType); + } + + /** + * @hide + */ + public boolean hasHints(@SliceHint String[] hints) { + if (hints == null) return true; + for (String hint : hints) { + if (!TextUtils.isEmpty(hint) && !ArrayUtils.contains(mHints, hint)) { + return false; + } + } + return true; + } + + /** + * @hide + */ + public boolean hasAnyHints(@SliceHint String[] hints) { + if (hints == null) return false; + for (String hint : hints) { + if (ArrayUtils.contains(mHints, hint)) { + return true; + } + } + return false; + } + + private void writeObj(Parcel dest, int flags, Object obj, int type) { + switch (type) { + case TYPE_SLICE: + case TYPE_REMOTE_VIEW: + case TYPE_IMAGE: + case TYPE_REMOTE_INPUT: + ((Parcelable) obj).writeToParcel(dest, flags); + break; + case TYPE_ACTION: + ((Pair<PendingIntent, Slice>) obj).first.writeToParcel(dest, flags); + ((Pair<PendingIntent, Slice>) obj).second.writeToParcel(dest, flags); + break; + case TYPE_TEXT: + TextUtils.writeToParcel((CharSequence) mObj, dest, flags); + break; + case TYPE_COLOR: + dest.writeInt((Integer) mObj); + break; + case TYPE_TIMESTAMP: + dest.writeLong((Long) mObj); + break; + } + } + + private static Object readObj(int type, Parcel in) { + switch (type) { + case TYPE_SLICE: + return Slice.CREATOR.createFromParcel(in); + case TYPE_TEXT: + return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + case TYPE_IMAGE: + return Icon.CREATOR.createFromParcel(in); + case TYPE_ACTION: + return new Pair<PendingIntent, Slice>( + PendingIntent.CREATOR.createFromParcel(in), + Slice.CREATOR.createFromParcel(in)); + case TYPE_REMOTE_VIEW: + return RemoteViews.CREATOR.createFromParcel(in); + case TYPE_COLOR: + return in.readInt(); + case TYPE_TIMESTAMP: + return in.readLong(); + case TYPE_REMOTE_INPUT: + return RemoteInput.CREATOR.createFromParcel(in); + } + throw new RuntimeException("Unsupported type " + type); + } + + public static final Creator<SliceItem> CREATOR = new Creator<SliceItem>() { + @Override + public SliceItem createFromParcel(Parcel in) { + return new SliceItem(in); + } + + @Override + public SliceItem[] newArray(int size) { + return new SliceItem[size]; + } + }; + + /** + * @hide + */ + public static String typeToString(int type) { + switch (type) { + case TYPE_SLICE: + return "Slice"; + case TYPE_TEXT: + return "Text"; + case TYPE_IMAGE: + return "Image"; + case TYPE_ACTION: + return "Action"; + case TYPE_REMOTE_VIEW: + return "RemoteView"; + case TYPE_COLOR: + return "Color"; + case TYPE_TIMESTAMP: + return "Timestamp"; + case TYPE_REMOTE_INPUT: + return "RemoteInput"; + } + return "Unrecognized type: " + type; + } +} diff --git a/android/slice/SliceProvider.java b/android/slice/SliceProvider.java new file mode 100644 index 00000000..4e21371b --- /dev/null +++ b/android/slice/SliceProvider.java @@ -0,0 +1,156 @@ +/* + * 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.slice; + +import android.Manifest.permission; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.util.concurrent.CountDownLatch; + +/** + * A SliceProvider allows app to provide content to be displayed in system + * spaces. This content is templated and can contain actions, and the behavior + * of how it is surfaced is specific to the system surface. + * + * <p>Slices are not currently live content. They are bound once and shown to the + * user. If the content changes due to a callback from user interaction, then + * {@link ContentResolver#notifyChange(Uri, ContentObserver)} + * should be used to notify the system.</p> + * + * <p>The provider needs to be declared in the manifest to provide the authority + * for the app. The authority for most slices is expected to match the package + * of the application.</p> + * <pre class="prettyprint"> + * {@literal + * <provider + * android:name="com.android.mypkg.MySliceProvider" + * android:authorities="com.android.mypkg" />} + * </pre> + * + * @see Slice + * @hide + */ +public abstract class SliceProvider extends ContentProvider { + + private static final String TAG = "SliceProvider"; + /** + * @hide + */ + public static final String EXTRA_BIND_URI = "slice_uri"; + /** + * @hide + */ + public static final String METHOD_SLICE = "bind_slice"; + /** + * @hide + */ + public static final String EXTRA_SLICE = "slice"; + + private static final boolean DEBUG = false; + + /** + * Implemented to create a slice. Will be called on the main thread. + * @see {@link Slice}. + */ + public abstract Slice onBindSlice(Uri sliceUri); + + @Override + public final int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + if (DEBUG) Log.d(TAG, "update " + uri); + return 0; + } + + @Override + public final int delete(Uri uri, String selection, String[] selectionArgs) { + if (DEBUG) Log.d(TAG, "delete " + uri); + return 0; + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, String[] + selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Cursor query(Uri uri, String[] projection, Bundle queryArgs, + CancellationSignal cancellationSignal) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Uri insert(Uri uri, ContentValues values) { + if (DEBUG) Log.d(TAG, "insert " + uri); + return null; + } + + @Override + public final String getType(Uri uri) { + if (DEBUG) Log.d(TAG, "getType " + uri); + return null; + } + + @Override + public final Bundle call(String method, String arg, Bundle extras) { + if (method.equals(METHOD_SLICE)) { + getContext().enforceCallingPermission(permission.BIND_SLICE, + "Slice binding requires the permission BIND_SLICE"); + Uri uri = extras.getParcelable(EXTRA_BIND_URI); + + Slice s = handleBindSlice(uri); + Bundle b = new Bundle(); + b.putParcelable(EXTRA_SLICE, s); + return b; + } + return super.call(method, arg, extras); + } + + private Slice handleBindSlice(Uri sliceUri) { + Slice[] output = new Slice[1]; + CountDownLatch latch = new CountDownLatch(1); + Handler mainHandler = new Handler(Looper.getMainLooper()); + mainHandler.post(() -> { + output[0] = onBindSlice(sliceUri); + latch.countDown(); + }); + try { + latch.await(); + return output[0]; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/android/slice/SliceQuery.java b/android/slice/SliceQuery.java new file mode 100644 index 00000000..d99b26a5 --- /dev/null +++ b/android/slice/SliceQuery.java @@ -0,0 +1,151 @@ +/* + * 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.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; +import java.util.Queue; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * A bunch of utilities for searching the contents of a slice. + * @hide + */ +public class SliceQuery { + private static final String TAG = "SliceQuery"; + + /** + * @hide + */ + public static SliceItem findNotContaining(SliceItem container, List<SliceItem> list) { + SliceItem ret = null; + while (ret == null && list.size() != 0) { + SliceItem remove = list.remove(0); + if (!contains(container, remove)) { + ret = remove; + } + } + return ret; + } + + /** + * @hide + */ + private static boolean contains(SliceItem container, SliceItem item) { + if (container == null || item == null) return false; + return stream(container).filter(s -> (s == item)).findAny().isPresent(); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, int type) { + return findAll(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, int type, String hints, String nonHints) { + return findAll(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, int type, String[] hints, + String[] nonHints) { + return stream(s).filter(item -> (type == -1 || item.getType() == type) + && (item.hasHints(hints) && !item.hasAnyHints(nonHints))) + .collect(Collectors.toList()); + } + + /** + * @hide + */ + public static SliceItem find(Slice s, int type, String hints, String nonHints) { + return find(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @hide + */ + public static SliceItem find(Slice s, int type) { + return find(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, int type) { + return find(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, int type, String hints, String nonHints) { + return find(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @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); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, int type, String[] hints, String[] nonHints) { + return stream(s).filter(item -> (item.getType() == type || type == -1) + && (item.hasHints(hints) && !item.hasAnyHints(nonHints))).findFirst().orElse(null); + } + + /** + * @hide + */ + public static Stream<SliceItem> stream(SliceItem slice) { + Queue<SliceItem> items = new LinkedList(); + items.add(slice); + Iterator<SliceItem> iterator = new Iterator<SliceItem>() { + @Override + public boolean hasNext() { + return items.size() != 0; + } + + @Override + public SliceItem next() { + SliceItem item = items.poll(); + if (item.getType() == TYPE_SLICE || item.getType() == TYPE_ACTION) { + items.addAll(Arrays.asList(item.getSlice().getItems())); + } + return item; + } + }; + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); + } +} diff --git a/android/slice/views/ActionRow.java b/android/slice/views/ActionRow.java new file mode 100644 index 00000000..93e9c035 --- /dev/null +++ b/android/slice/views/ActionRow.java @@ -0,0 +1,201 @@ +/* + * 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.slice.views; + +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.app.RemoteInput; +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; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * @hide + */ +public class ActionRow extends FrameLayout { + + private static final int MAX_ACTIONS = 5; + private final int mSize; + private final int mIconPadding; + private final LinearLayout mActionsGroup; + private final boolean mFullActions; + private int mColor = Color.BLACK; + + public ActionRow(Context context, boolean fullActions) { + super(context); + mFullActions = fullActions; + mSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, + context.getResources().getDisplayMetrics()); + mIconPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12, + context.getResources().getDisplayMetrics()); + mActionsGroup = new LinearLayout(context); + mActionsGroup.setOrientation(LinearLayout.HORIZONTAL); + mActionsGroup.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + addView(mActionsGroup); + } + + private void setColor(int color) { + mColor = color; + for (int i = 0; i < mActionsGroup.getChildCount(); i++) { + View view = mActionsGroup.getChildAt(i); + SliceItem item = (SliceItem) view.getTag(); + boolean tint = !item.hasHint(Slice.HINT_NO_TINT); + if (tint) { + ((ImageView) view).setImageTintList(ColorStateList.valueOf(mColor)); + } + } + } + + private ImageView addAction(Icon icon, boolean allowTint, SliceItem image) { + ImageView imageView = new ImageView(getContext()); + imageView.setPadding(mIconPadding, mIconPadding, mIconPadding, mIconPadding); + imageView.setScaleType(ScaleType.FIT_CENTER); + imageView.setImageIcon(icon); + if (allowTint) { + imageView.setImageTintList(ColorStateList.valueOf(mColor)); + } + imageView.setBackground(SliceViewUtil.getDrawable(getContext(), + android.R.attr.selectableItemBackground)); + imageView.setTag(image); + addAction(imageView); + return imageView; + } + + /** + * Set the actions and color for this action row. + */ + public void setActions(SliceItem actionRow, SliceItem defColor) { + removeAllViews(); + mActionsGroup.removeAllViews(); + addView(mActionsGroup); + + SliceItem color = SliceQuery.find(actionRow, SliceItem.TYPE_COLOR); + if (color == null) { + color = defColor; + } + if (color != null) { + setColor(color.getColor()); + } + SliceQuery.findAll(actionRow, SliceItem.TYPE_ACTION).forEach(action -> { + if (mActionsGroup.getChildCount() >= MAX_ACTIONS) { + return; + } + SliceItem image = SliceQuery.find(action, SliceItem.TYPE_IMAGE); + if (image == null) { + return; + } + boolean tint = !image.hasHint(Slice.HINT_NO_TINT); + SliceItem input = SliceQuery.find(action, SliceItem.TYPE_REMOTE_INPUT); + if (input != null && input.getRemoteInput().getAllowFreeFormInput()) { + addAction(image.getIcon(), tint, image).setOnClickListener( + v -> handleRemoteInputClick(v, action.getAction(), input.getRemoteInput())); + createRemoteInputView(mColor, getContext()); + } else { + addAction(image.getIcon(), tint, image).setOnClickListener(v -> AsyncTask.execute( + () -> { + try { + action.getAction().send(); + } catch (CanceledException e) { + e.printStackTrace(); + } + })); + } + }); + setVisibility(getChildCount() != 0 ? View.VISIBLE : View.GONE); + } + + private void addAction(View child) { + mActionsGroup.addView(child, new LinearLayout.LayoutParams(mSize, mSize, 1)); + } + + private void createRemoteInputView(int color, Context context) { + View riv = RemoteInputView.inflate(context, this); + riv.setVisibility(View.INVISIBLE); + addView(riv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + riv.setBackgroundColor(color); + } + + private boolean handleRemoteInputClick(View view, PendingIntent pendingIntent, + RemoteInput input) { + if (input == null) { + return false; + } + + ViewParent p = view.getParent().getParent(); + RemoteInputView riv = null; + while (p != null) { + if (p instanceof View) { + View pv = (View) p; + riv = findRemoteInputView(pv); + if (riv != null) { + break; + } + } + p = p.getParent(); + } + if (riv == null) { + return false; + } + + int width = view.getWidth(); + if (view instanceof TextView) { + // Center the reveal on the text which might be off-center from the TextView + TextView tv = (TextView) view; + if (tv.getLayout() != null) { + int innerWidth = (int) tv.getLayout().getLineWidth(0); + innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight(); + width = Math.min(width, innerWidth); + } + } + int cx = view.getLeft() + width / 2; + int cy = view.getTop() + view.getHeight() / 2; + int w = riv.getWidth(); + int h = riv.getHeight(); + int r = Math.max( + Math.max(cx + cy, cx + (h - cy)), + Math.max((w - cx) + cy, (w - cx) + (h - cy))); + + riv.setRevealParameters(cx, cy, r); + riv.setPendingIntent(pendingIntent); + riv.setRemoteInput(new RemoteInput[] { + input + }, input); + riv.focusAnimated(); + return true; + } + + private RemoteInputView findRemoteInputView(View v) { + if (v == null) { + return null; + } + return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG); + } +} diff --git a/android/slice/views/GridView.java b/android/slice/views/GridView.java new file mode 100644 index 00000000..18a90f7d --- /dev/null +++ b/android/slice/views/GridView.java @@ -0,0 +1,186 @@ +/* + * 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.slice.views; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +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; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * @hide + */ +public class GridView extends LinearLayout implements SliceListView { + + private static final String TAG = "GridView"; + + private static final int MAX_IMAGES = 3; + private static final int MAX_ALL = 5; + private boolean mIsAllImages; + + public GridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mIsAllImages) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = width / getChildCount(); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY, + height); + getLayoutParams().height = height; + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).getLayoutParams().height = height; + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public void setSliceItem(SliceItem slice) { + mIsAllImages = true; + removeAllViews(); + int total = 1; + if (slice.getType() == SliceItem.TYPE_SLICE) { + SliceItem[] items = slice.getSlice().getItems(); + total = items.length; + for (int i = 0; i < total; i++) { + SliceItem item = items[i]; + if (isFull()) { + continue; + } + if (!addItem(item)) { + mIsAllImages = false; + } + } + } else { + if (!isFull()) { + if (!addItem(slice)) { + mIsAllImages = false; + } + } + } + if (total > getChildCount() && mIsAllImages) { + addExtraCount(total - getChildCount()); + } + } + + private void addExtraCount(int numExtra) { + View last = getChildAt(getChildCount() - 1); + FrameLayout frame = new FrameLayout(getContext()); + frame.setLayoutParams(last.getLayoutParams()); + + removeView(last); + frame.addView(last, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + TextView v = new TextView(getContext()); + v.setTextColor(Color.WHITE); + v.setBackgroundColor(0x4d000000); + v.setText(getResources().getString(R.string.slice_more_content, numExtra)); + v.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + v.setGravity(Gravity.CENTER); + frame.addView(v, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + addView(frame); + } + + private boolean isFull() { + return getChildCount() >= (mIsAllImages ? MAX_IMAGES : MAX_ALL); + } + + /** + * Returns true if this item is just an image. + */ + private boolean addItem(SliceItem item) { + if (item.getType() == SliceItem.TYPE_IMAGE) { + ImageView v = new ImageView(getContext()); + v.setImageIcon(item.getIcon()); + v.setScaleType(ScaleType.CENTER_CROP); + addView(v, new LayoutParams(0, MATCH_PARENT, 1)); + return true; + } else { + LinearLayout v = new LinearLayout(getContext()); + int s = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 12, getContext().getResources().getDisplayMetrics()); + v.setPadding(0, s, 0, 0); + v.setOrientation(LinearLayout.VERTICAL); + v.setGravity(Gravity.CENTER_HORIZONTAL); + // 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.forEach(i -> { + Context context = getContext(); + switch (i.getType()) { + case SliceItem.TYPE_TEXT: + boolean title = false; + if ((item.hasAnyHints(new String[] { + Slice.HINT_LARGE, Slice.HINT_TITLE + }))) { + title = true; + } + TextView tv = (TextView) LayoutInflater.from(context).inflate( + title ? R.layout.slice_title : R.layout.slice_secondary_text, null); + tv.setText(i.getText()); + v.addView(tv); + break; + case SliceItem.TYPE_IMAGE: + ImageView iv = new ImageView(context); + iv.setImageIcon(i.getIcon()); + if (item.hasHint(Slice.HINT_LARGE)) { + iv.setLayoutParams(new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } else { + int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 48, context.getResources().getDisplayMetrics()); + iv.setLayoutParams(new LayoutParams(size, size)); + } + v.addView(iv); + break; + case SliceItem.TYPE_REMOTE_VIEW: + v.addView(i.getRemoteView().apply(context, v)); + break; + case SliceItem.TYPE_COLOR: + // TODO: Support color to tint stuff here. + break; + } + }); + addView(v, new LayoutParams(0, WRAP_CONTENT, 1)); + return false; + } + } +} diff --git a/android/slice/views/LargeSliceAdapter.java b/android/slice/views/LargeSliceAdapter.java new file mode 100644 index 00000000..e77a1b2a --- /dev/null +++ b/android/slice/views/LargeSliceAdapter.java @@ -0,0 +1,224 @@ +/* + * 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.slice.views; + +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; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; + +import com.android.internal.R; +import com.android.internal.widget.RecyclerView; +import com.android.internal.widget.RecyclerView.ViewHolder; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @hide + */ +public class LargeSliceAdapter extends RecyclerView.Adapter<SliceViewHolder> { + + public static final int TYPE_DEFAULT = 1; + public static final int TYPE_HEADER = 2; + public static final int TYPE_GRID = 3; + public static final int TYPE_MESSAGE = 4; + public static final int TYPE_MESSAGE_LOCAL = 5; + public static final int TYPE_REMOTE_VIEWS = 6; + + private final IdGenerator mIdGen = new IdGenerator(); + private final Context mContext; + private List<SliceWrapper> mSlices = new ArrayList<>(); + private SliceItem mColor; + + public LargeSliceAdapter(Context context) { + mContext = context; + setHasStableIds(true); + } + + /** + * Set the {@link SliceItem}'s to be displayed in the adapter and the accent color. + */ + public void setSliceItems(List<SliceItem> slices, SliceItem color) { + mColor = color; + mIdGen.resetUsage(); + mSlices = slices.stream().map(s -> new SliceWrapper(s, mIdGen)) + .collect(Collectors.toList()); + notifyDataSetChanged(); + } + + @Override + public SliceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = inflateforType(viewType); + v.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + return new SliceViewHolder(v); + } + + @Override + public int getItemViewType(int position) { + return mSlices.get(position).mType; + } + + @Override + public long getItemId(int position) { + return mSlices.get(position).mId; + } + + @Override + public int getItemCount() { + return mSlices.size(); + } + + @Override + public void onBindViewHolder(SliceViewHolder holder, int position) { + SliceWrapper slice = mSlices.get(position); + if (holder.mSliceView != null) { + holder.mSliceView.setColor(mColor); + holder.mSliceView.setSliceItem(slice.mItem); + } else if (slice.mType == TYPE_REMOTE_VIEWS) { + FrameLayout frame = (FrameLayout) holder.itemView; + frame.removeAllViews(); + frame.addView(slice.mItem.getRemoteView().apply(mContext, frame)); + } + } + + private View inflateforType(int viewType) { + switch (viewType) { + case TYPE_REMOTE_VIEWS: + return new FrameLayout(mContext); + case TYPE_GRID: + return LayoutInflater.from(mContext).inflate(R.layout.slice_grid, null); + case TYPE_MESSAGE: + return LayoutInflater.from(mContext).inflate(R.layout.slice_message, null); + case TYPE_MESSAGE_LOCAL: + return LayoutInflater.from(mContext).inflate(R.layout.slice_message_local, null); + } + return new SmallTemplateView(mContext); + } + + protected static class SliceWrapper { + private final SliceItem mItem; + private final int mType; + private final long mId; + + public SliceWrapper(SliceItem item, IdGenerator idGen) { + mItem = item; + mType = getType(item); + mId = idGen.getId(item); + } + + public static int getType(SliceItem item) { + if (item.getType() == SliceItem.TYPE_REMOTE_VIEW) { + return TYPE_REMOTE_VIEWS; + } + if (item.hasHint(Slice.HINT_MESSAGE)) { + // TODO: Better way to determine me or not? Something more like Messaging style. + if (SliceQuery.find(item, -1, Slice.HINT_SOURCE, null) != null) { + return TYPE_MESSAGE; + } else { + return TYPE_MESSAGE_LOCAL; + } + } + if (item.hasHint(Slice.HINT_HORIZONTAL)) { + return TYPE_GRID; + } + return TYPE_DEFAULT; + } + } + + /** + * A {@link ViewHolder} for presenting slices in {@link LargeSliceAdapter}. + */ + public static class SliceViewHolder extends ViewHolder { + public final SliceListView mSliceView; + + public SliceViewHolder(View itemView) { + super(itemView); + mSliceView = itemView instanceof SliceListView ? (SliceListView) itemView : null; + } + } + + /** + * View slices being displayed in {@link LargeSliceAdapter}. + */ + public interface SliceListView { + /** + * Set the slice item for this view. + */ + void setSliceItem(SliceItem slice); + + /** + * Set the color for the items in this view. + */ + default void setColor(SliceItem color) { + + } + } + + private static class IdGenerator { + private long mNextLong = 0; + private final ArrayMap<String, Long> mCurrentIds = new ArrayMap<>(); + private final ArrayMap<String, Integer> mUsedIds = new ArrayMap<>(); + + public long getId(SliceItem item) { + String str = genString(item); + if (!mCurrentIds.containsKey(str)) { + mCurrentIds.put(str, mNextLong++); + } + long id = mCurrentIds.get(str); + int index = mUsedIds.getOrDefault(str, 0); + mUsedIds.put(str, index + 1); + return id + index * 10000; + } + + private String genString(SliceItem item) { + StringBuilder builder = new StringBuilder(); + SliceQuery.stream(item).forEach(i -> { + builder.append(i.getType()); + i.removeHint(Slice.HINT_SELECTED); + builder.append(i.getHints()); + switch (i.getType()) { + case SliceItem.TYPE_REMOTE_VIEW: + builder.append(i.getRemoteView()); + break; + case SliceItem.TYPE_IMAGE: + builder.append(i.getIcon()); + break; + case SliceItem.TYPE_TEXT: + builder.append(i.getText()); + break; + case SliceItem.TYPE_COLOR: + builder.append(i.getColor()); + break; + } + }); + return builder.toString(); + } + + public void resetUsage() { + mUsedIds.clear(); + } + } +} diff --git a/android/slice/views/LargeTemplateView.java b/android/slice/views/LargeTemplateView.java new file mode 100644 index 00000000..d53e8fcb --- /dev/null +++ b/android/slice/views/LargeTemplateView.java @@ -0,0 +1,116 @@ +/* + * 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.slice.views; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +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; + +/** + * @hide + */ +public class LargeTemplateView extends SliceModeView { + private final LargeSliceAdapter mAdapter; + private final RecyclerView mRecyclerView; + private final int mDefaultHeight; + private final int mMaxHeight; + private Slice mSlice; + + public LargeTemplateView(Context context) { + super(context); + + mRecyclerView = new RecyclerView(getContext()); + mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + mAdapter = new LargeSliceAdapter(context); + mRecyclerView.setAdapter(mAdapter); + addView(mRecyclerView); + int width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300, + getResources().getDisplayMetrics()); + setLayoutParams(new LayoutParams(width, WRAP_CONTENT)); + mDefaultHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, + getResources().getDisplayMetrics()); + mMaxHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, + getResources().getDisplayMetrics()); + } + + @Override + public String getMode() { + return SliceView.MODE_LARGE; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mRecyclerView.getLayoutParams().height = WRAP_CONTENT; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mRecyclerView.getMeasuredHeight() > mMaxHeight + || mSlice.hasHint(Slice.HINT_PARTIAL)) { + mRecyclerView.getLayoutParams().height = mDefaultHeight; + } else { + mRecyclerView.getLayoutParams().height = mRecyclerView.getMeasuredHeight(); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public void setSlice(Slice slice) { + SliceItem color = SliceQuery.find(slice, SliceItem.TYPE_COLOR); + mSlice = slice; + List<SliceItem> items = new ArrayList<>(); + boolean[] hasHeader = new boolean[1]; + if (slice.hasHint(Slice.HINT_LIST)) { + addList(slice, items); + } else { + Arrays.asList(slice.getItems()).forEach(item -> { + if (item.hasHint(Slice.HINT_ACTIONS)) { + return; + } else if (item.getType() == SliceItem.TYPE_COLOR) { + return; + } else if (item.getType() == SliceItem.TYPE_SLICE + && item.hasHint(Slice.HINT_LIST)) { + addList(item.getSlice(), items); + } else if (item.hasHint(Slice.HINT_LIST_ITEM)) { + items.add(item); + } else if (!hasHeader[0]) { + hasHeader[0] = true; + items.add(0, item); + } else { + item.addHint(Slice.HINT_LIST_ITEM); + items.add(item); + } + }); + } + mAdapter.setSliceItems(items, color); + } + + private void addList(Slice slice, List<SliceItem> items) { + List<SliceItem> sliceItems = Arrays.asList(slice.getItems()); + sliceItems.forEach(i -> i.addHint(Slice.HINT_LIST_ITEM)); + items.addAll(sliceItems); + } +} diff --git a/android/slice/views/MessageView.java b/android/slice/views/MessageView.java new file mode 100644 index 00000000..7b03e0bd --- /dev/null +++ b/android/slice/views/MessageView.java @@ -0,0 +1,77 @@ +/* + * 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.slice.views; + +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; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * @hide + */ +public class MessageView extends LinearLayout implements SliceListView { + + private TextView mDetails; + private ImageView mIcon; + + public MessageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mDetails = findViewById(android.R.id.summary); + mIcon = findViewById(android.R.id.icon); + } + + @Override + public void setSliceItem(SliceItem slice) { + SliceItem source = SliceQuery.find(slice, SliceItem.TYPE_IMAGE, Slice.HINT_SOURCE, null); + if (source != null) { + final int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 24, getContext().getResources().getDisplayMetrics()); + // TODO try and turn this into a drawable + Bitmap iconBm = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); + Canvas iconCanvas = new Canvas(iconBm); + Drawable d = source.getIcon().loadDrawable(getContext()); + d.setBounds(0, 0, iconSize, iconSize); + d.draw(iconCanvas); + mIcon.setImageBitmap(SliceViewUtil.getCircularBitmap(iconBm)); + } + SpannableStringBuilder builder = new SpannableStringBuilder(); + SliceQuery.findAll(slice, SliceItem.TYPE_TEXT).forEach(text -> { + if (builder.length() != 0) { + builder.append('\n'); + } + builder.append(text.getText()); + }); + mDetails.setText(builder.toString()); + } + +} diff --git a/android/slice/views/RemoteInputView.java b/android/slice/views/RemoteInputView.java new file mode 100644 index 00000000..a29bb5c0 --- /dev/null +++ b/android/slice/views/RemoteInputView.java @@ -0,0 +1,445 @@ +/* + * 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.slice.views; + +import android.animation.Animator; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutManager; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.internal.R; + +/** + * Host for the remote input. + * + * @hide + */ +// TODO this should be unified with SystemUI RemoteInputView (b/67527720) +public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher { + + private static final String TAG = "RemoteInput"; + + /** + * A marker object that let's us easily find views of this class. + */ + public static final Object VIEW_TAG = new Object(); + + private RemoteEditText mEditText; + private ImageButton mSendButton; + private ProgressBar mProgressBar; + private PendingIntent mPendingIntent; + private RemoteInput[] mRemoteInputs; + private RemoteInput mRemoteInput; + + private int mRevealCx; + private int mRevealCy; + private int mRevealR; + private boolean mResetting; + + public RemoteInputView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mProgressBar = findViewById(R.id.remote_input_progress); + mSendButton = findViewById(R.id.remote_input_send); + mSendButton.setOnClickListener(this); + + mEditText = (RemoteEditText) getChildAt(0); + mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + final boolean isSoftImeEvent = event == null + && (actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT + || actionId == EditorInfo.IME_ACTION_SEND); + final boolean isKeyboardEnterKey = event != null + && KeyEvent.isConfirmKey(event.getKeyCode()) + && event.getAction() == KeyEvent.ACTION_DOWN; + + if (isSoftImeEvent || isKeyboardEnterKey) { + if (mEditText.length() > 0) { + sendRemoteInput(); + } + // Consume action to prevent IME from closing. + return true; + } + return false; + } + }); + mEditText.addTextChangedListener(this); + mEditText.setInnerFocusable(false); + mEditText.mRemoteInputView = this; + } + + private void sendRemoteInput() { + Bundle results = new Bundle(); + results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString()); + Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent, + results); + + mEditText.setEnabled(false); + mSendButton.setVisibility(INVISIBLE); + mProgressBar.setVisibility(VISIBLE); + mEditText.mShowImeOnInputConnection = false; + + // Tell ShortcutManager that this package has been "activated". ShortcutManager + // will reset the throttling for this package. + // Strictly speaking, the intent receiver may be different from the intent creator, + // but that's an edge case, and also because we can't always know which package will receive + // an intent, so we just reset for the creator. + getContext().getSystemService(ShortcutManager.class).onApplicationActive( + mPendingIntent.getCreatorPackage(), + getContext().getUserId()); + + try { + mPendingIntent.send(mContext, 0, fillInIntent); + reset(); + } catch (PendingIntent.CanceledException e) { + Log.i(TAG, "Unable to send remote input result", e); + Toast.makeText(mContext, "Failure sending pending intent for inline reply :(", + Toast.LENGTH_SHORT).show(); + reset(); + } + } + + /** + * Creates a remote input view. + */ + public static RemoteInputView inflate(Context context, ViewGroup root) { + RemoteInputView v = (RemoteInputView) LayoutInflater.from(context).inflate( + R.layout.slice_remote_input, root, false); + v.setTag(VIEW_TAG); + return v; + } + + @Override + public void onClick(View v) { + if (v == mSendButton) { + sendRemoteInput(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + // We never want for a touch to escape to an outer view or one we covered. + return true; + } + + private void onDefocus() { + setVisibility(INVISIBLE); + } + + /** + * Set the pending intent for remote input. + */ + public void setPendingIntent(PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + } + + /** + * Set the remote inputs for this view. + */ + public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) { + mRemoteInputs = remoteInputs; + mRemoteInput = remoteInput; + mEditText.setHint(mRemoteInput.getLabel()); + } + + /** + * Focuses the remote input view. + */ + public void focusAnimated() { + if (getVisibility() != VISIBLE) { + Animator animator = ViewAnimationUtils.createCircularReveal( + this, mRevealCx, mRevealCy, 0, mRevealR); + animator.setDuration(200); + animator.start(); + } + focus(); + } + + private void focus() { + setVisibility(VISIBLE); + mEditText.setInnerFocusable(true); + mEditText.mShowImeOnInputConnection = true; + mEditText.setSelection(mEditText.getText().length()); + mEditText.requestFocus(); + updateSendButton(); + } + + private void reset() { + mResetting = true; + + mEditText.getText().clear(); + mEditText.setEnabled(true); + mSendButton.setVisibility(VISIBLE); + mProgressBar.setVisibility(INVISIBLE); + updateSendButton(); + onDefocus(); + + mResetting = false; + } + + @Override + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + if (mResetting && child == mEditText) { + // Suppress text events if it happens during resetting. Ideally this would be + // suppressed by the text view not being shown, but that doesn't work here because it + // needs to stay visible for the animation. + return false; + } + return super.onRequestSendAccessibilityEvent(child, event); + } + + private void updateSendButton() { + mSendButton.setEnabled(mEditText.getText().length() != 0); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + updateSendButton(); + } + + /** + * Tries to find an action that matches the current pending intent of this view and updates its + * state to that of the found action + * + * @return true if a matching action was found, false otherwise + */ + public boolean updatePendingIntentFromActions(Notification.Action[] actions) { + if (mPendingIntent == null || actions == null) { + return false; + } + Intent current = mPendingIntent.getIntent(); + if (current == null) { + return false; + } + + for (Notification.Action a : actions) { + RemoteInput[] inputs = a.getRemoteInputs(); + if (a.actionIntent == null || inputs == null) { + continue; + } + Intent candidate = a.actionIntent.getIntent(); + if (!current.filterEquals(candidate)) { + continue; + } + + RemoteInput input = null; + for (RemoteInput i : inputs) { + if (i.getAllowFreeFormInput()) { + input = i; + } + } + if (input == null) { + continue; + } + setPendingIntent(a.actionIntent); + setRemoteInput(inputs, input); + return true; + } + return false; + } + + /** + * @hide + */ + public void setRevealParameters(int cx, int cy, int r) { + mRevealCx = cx; + mRevealCy = cy; + mRevealR = r; + } + + @Override + public void dispatchStartTemporaryDetach() { + super.dispatchStartTemporaryDetach(); + // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and + // won't lose IME focus. + detachViewFromParent(mEditText); + } + + @Override + public void dispatchFinishTemporaryDetach() { + if (isAttachedToWindow()) { + attachViewToParent(mEditText, 0, mEditText.getLayoutParams()); + } else { + removeDetachedView(mEditText, false /* animate */); + } + super.dispatchFinishTemporaryDetach(); + } + + /** + * An EditText that changes appearance based on whether it's focusable and becomes un-focusable + * whenever the user navigates away from it or it becomes invisible. + */ + public static class RemoteEditText extends EditText { + + private final Drawable mBackground; + private RemoteInputView mRemoteInputView; + boolean mShowImeOnInputConnection; + + public RemoteEditText(Context context, AttributeSet attrs) { + super(context, attrs); + mBackground = getBackground(); + } + + private void defocusIfNeeded(boolean animate) { + if (mRemoteInputView != null || isTemporarilyDetached()) { + if (isTemporarilyDetached()) { + // We might get reattached but then the other one of HUN / expanded might steal + // our focus, so we'll need to save our text here. + } + return; + } + if (isFocusable() && isEnabled()) { + setInnerFocusable(false); + if (mRemoteInputView != null) { + mRemoteInputView.onDefocus(); + } + mShowImeOnInputConnection = false; + } + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if (!isShown()) { + defocusIfNeeded(false /* animate */); + } + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + if (!focused) { + defocusIfNeeded(true /* animate */); + } + } + + @Override + public void getFocusedRect(Rect r) { + super.getFocusedRect(r); + r.top = mScrollY; + r.bottom = mScrollY + (mBottom - mTop); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + // Eat the DOWN event here to prevent any default behavior. + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + defocusIfNeeded(true /* animate */); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + final InputConnection inputConnection = super.onCreateInputConnection(outAttrs); + + if (mShowImeOnInputConnection && inputConnection != null) { + final InputMethodManager imm = InputMethodManager.getInstance(); + if (imm != null) { + // onCreateInputConnection is called by InputMethodManager in the middle of + // setting up the connection to the IME; wait with requesting the IME until that + // work has completed. + post(new Runnable() { + @Override + public void run() { + imm.viewClicked(RemoteEditText.this); + imm.showSoftInput(RemoteEditText.this, 0); + } + }); + } + } + + return inputConnection; + } + + @Override + public void onCommitCompletion(CompletionInfo text) { + clearComposingText(); + setText(text.getText()); + setSelection(getText().length()); + } + + void setInnerFocusable(boolean focusable) { + setFocusableInTouchMode(focusable); + setFocusable(focusable); + setCursorVisible(focusable); + + if (focusable) { + requestFocus(); + setBackground(mBackground); + } else { + setBackground(null); + } + + } + } +} diff --git a/android/slice/views/ShortcutView.java b/android/slice/views/ShortcutView.java new file mode 100644 index 00000000..8fe2f1ac --- /dev/null +++ b/android/slice/views/ShortcutView.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.slice.views; + +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +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; + +/** + * @hide + */ +public class ShortcutView extends SliceModeView { + + private static final String TAG = "ShortcutView"; + + private PendingIntent mAction; + private Uri mUri; + private int mLargeIconSize; + private int mSmallIconSize; + + public ShortcutView(Context context) { + super(context); + mLargeIconSize = getContext().getResources() + .getDimensionPixelSize(R.dimen.slice_shortcut_size); + mSmallIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.slice_icon_size); + setLayoutParams(new ViewGroup.LayoutParams(mLargeIconSize, mLargeIconSize)); + } + + @Override + public void setSlice(Slice slice) { + removeAllViews(); + SliceItem sliceItem = SliceQuery.find(slice, SliceItem.TYPE_ACTION); + SliceItem iconItem = slice.getPrimaryIcon(); + SliceItem textItem = sliceItem != null + ? SliceQuery.find(sliceItem, SliceItem.TYPE_TEXT) + : SliceQuery.find(slice, SliceItem.TYPE_TEXT); + SliceItem colorItem = sliceItem != null + ? SliceQuery.find(sliceItem, SliceItem.TYPE_COLOR) + : SliceQuery.find(slice, SliceItem.TYPE_COLOR); + if (colorItem == null) { + colorItem = SliceQuery.find(slice, SliceItem.TYPE_COLOR); + } + // TODO: pick better default colour + final int color = colorItem != null ? colorItem.getColor() : Color.GRAY; + ShapeDrawable circle = new ShapeDrawable(new OvalShape()); + circle.setTint(color); + setBackground(circle); + if (iconItem != null) { + final boolean isLarge = iconItem.hasHint(Slice.HINT_LARGE); + final int iconSize = isLarge ? mLargeIconSize : mSmallIconSize; + SliceViewUtil.createCircledIcon(getContext(), color, iconSize, iconItem.getIcon(), + isLarge, this /* parent */); + mAction = sliceItem != null ? sliceItem.getAction() + : null; + mUri = slice.getUri(); + setClickable(true); + } else { + setClickable(false); + } + } + + @Override + public String getMode() { + return SliceView.MODE_SHORTCUT; + } + + @Override + public boolean performClick() { + if (!callOnClick()) { + try { + if (mAction != null) { + mAction.send(); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW).setData(mUri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(intent); + } + } catch (CanceledException e) { + e.printStackTrace(); + } + } + return true; + } +} diff --git a/android/slice/views/SliceView.java b/android/slice/views/SliceView.java new file mode 100644 index 00000000..f3792481 --- /dev/null +++ b/android/slice/views/SliceView.java @@ -0,0 +1,249 @@ +/* + * 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.slice.views; + +import android.annotation.StringDef; +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; + +/** + * A view that can display a {@link Slice} in different {@link SliceMode}'s. + * + * @hide + */ +public class SliceView extends LinearLayout { + + private static final String TAG = "SliceView"; + + /** + * @hide + */ + public abstract static class SliceModeView extends FrameLayout { + + public SliceModeView(Context context) { + super(context); + } + + /** + * @return the {@link SliceMode} of the slice being presented. + */ + public abstract String getMode(); + + /** + * @param slice the slice to show in this view. + */ + public abstract void setSlice(Slice slice); + } + + /** + * @hide + */ + @StringDef({ + MODE_SMALL, MODE_LARGE, MODE_SHORTCUT + }) + public @interface SliceMode {} + + /** + * Mode indicating this slice should be presented in small template format. + */ + public static final String MODE_SMALL = "SLICE_SMALL"; + /** + * Mode indicating this slice should be presented in large template format. + */ + public static final String MODE_LARGE = "SLICE_LARGE"; + /** + * Mode indicating this slice should be presented as an icon. + */ + public static final String MODE_SHORTCUT = "SLICE_ICON"; + + /** + * Will select the type of slice binding based on size of the View. TODO: Put in some info about + * that selection. + */ + private static final String MODE_AUTO = "auto"; + + private String mMode = MODE_AUTO; + private SliceModeView mCurrentView; + private final ActionRow mActions; + private Slice mCurrentSlice; + private boolean mShowActions = true; + + /** + * Simple constructor to create a slice view from code. + * + * @param context The context the view is running in. + */ + public SliceView(Context context) { + super(context); + setOrientation(LinearLayout.VERTICAL); + mActions = new ActionRow(mContext, true); + mActions.setBackground(new ColorDrawable(0xffeeeeee)); + mCurrentView = new LargeTemplateView(mContext); + addView(mCurrentView); + addView(mActions); + } + + /** + * @hide + */ + public void bindSlice(Intent intent) { + // TODO + } + + /** + * Binds this view to the {@link Slice} associated with the provided {@link Uri}. + */ + public void bindSlice(Uri sliceUri) { + validate(sliceUri); + Slice s = mContext.getContentResolver().bindSlice(sliceUri); + bindSlice(s); + } + + /** + * Binds this view to the provided {@link Slice}. + */ + public void bindSlice(Slice slice) { + mCurrentSlice = slice; + if (mCurrentSlice != null) { + reinflate(); + } + } + + /** + * Call to clean up the view. + */ + public void unbindSlice() { + mCurrentSlice = null; + } + + /** + * Set the {@link SliceMode} this view should present in. + */ + public void setMode(@SliceMode String mode) { + setMode(mode, false /* animate */); + } + + /** + * @hide + */ + public void setMode(@SliceMode String mode, boolean animate) { + if (animate) { + Log.e(TAG, "Animation not supported yet"); + } + mMode = mode; + reinflate(); + } + + /** + * @return the {@link SliceMode} this view is presenting in. + */ + public @SliceMode String getMode() { + if (mMode.equals(MODE_AUTO)) { + return MODE_LARGE; + } + return mMode; + } + + /** + * @hide + * + * Whether this view should show a row of actions with it. + */ + public void setShowActionRow(boolean show) { + mShowActions = show; + reinflate(); + } + + private SliceModeView createView(String mode) { + switch (mode) { + case MODE_SHORTCUT: + return new ShortcutView(getContext()); + case MODE_SMALL: + return new SmallTemplateView(getContext()); + } + return new LargeTemplateView(getContext()); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + unbindSlice(); + } + + private void reinflate() { + if (mCurrentSlice == null) { + return; + } + // TODO: Smarter mapping here from one state to the next. + SliceItem color = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_COLOR); + SliceItem[] items = mCurrentSlice.getItems(); + SliceItem actionRow = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_SLICE, + Slice.HINT_ACTIONS, + Slice.HINT_ALT); + String mode = getMode(); + if (!mode.equals(mCurrentView.getMode())) { + removeAllViews(); + mCurrentView = createView(mode); + addView(mCurrentView); + addView(mActions); + } + if (items.length > 1 || (items.length != 0 && items[0] != actionRow)) { + mCurrentView.setVisibility(View.VISIBLE); + mCurrentView.setSlice(mCurrentSlice); + } else { + mCurrentView.setVisibility(View.GONE); + } + + boolean showActions = mShowActions && actionRow != null + && !mode.equals(MODE_SHORTCUT); + if (showActions) { + mActions.setActions(actionRow, color); + mActions.setVisibility(View.VISIBLE); + } else { + mActions.setVisibility(View.GONE); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // TODO -- may need to rethink for AGSA + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + requestDisallowInterceptTouchEvent(true); + } + return super.onInterceptTouchEvent(ev); + } + + private static void validate(Uri sliceUri) { + if (!ContentResolver.SCHEME_SLICE.equals(sliceUri.getScheme())) { + throw new RuntimeException("Invalid uri " + sliceUri); + } + if (sliceUri.getPathSegments().size() == 0) { + throw new RuntimeException("Invalid uri " + sliceUri); + } + } +} diff --git a/android/slice/views/SliceViewUtil.java b/android/slice/views/SliceViewUtil.java new file mode 100644 index 00000000..1b5a6d1e --- /dev/null +++ b/android/slice/views/SliceViewUtil.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.slice.views; + +import android.annotation.ColorInt; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +/** + * A bunch of utilities for slice UI. + * + * @hide + */ +public class SliceViewUtil { + + /** + * @hide + */ + @ColorInt + public static int getColorAccent(Context context) { + return getColorAttr(context, android.R.attr.colorAccent); + } + + /** + * @hide + */ + @ColorInt + public static int getColorError(Context context) { + return getColorAttr(context, android.R.attr.colorError); + } + + /** + * @hide + */ + @ColorInt + public static int getDefaultColor(Context context, int resId) { + final ColorStateList list = context.getResources().getColorStateList(resId, + context.getTheme()); + + return list.getDefaultColor(); + } + + /** + * @hide + */ + @ColorInt + public static int getDisabled(Context context, int inputColor) { + return applyAlphaAttr(context, android.R.attr.disabledAlpha, inputColor); + } + + /** + * @hide + */ + @ColorInt + public static int applyAlphaAttr(Context context, int attr, int inputColor) { + TypedArray ta = context.obtainStyledAttributes(new int[] { + attr + }); + float alpha = ta.getFloat(0, 0); + ta.recycle(); + return applyAlpha(alpha, inputColor); + } + + /** + * @hide + */ + @ColorInt + public static int applyAlpha(float alpha, int inputColor) { + alpha *= Color.alpha(inputColor); + return Color.argb((int) (alpha), Color.red(inputColor), Color.green(inputColor), + Color.blue(inputColor)); + } + + /** + * @hide + */ + @ColorInt + public static int getColorAttr(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[] { + attr + }); + @ColorInt + int colorAccent = ta.getColor(0, 0); + ta.recycle(); + return colorAccent; + } + + /** + * @hide + */ + public static int getThemeAttr(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[] { + attr + }); + int theme = ta.getResourceId(0, 0); + ta.recycle(); + return theme; + } + + /** + * @hide + */ + public static Drawable getDrawable(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[] { + attr + }); + Drawable drawable = ta.getDrawable(0); + ta.recycle(); + return drawable; + } + + /** + * @hide + */ + public static void createCircledIcon(Context context, int color, int iconSize, Icon icon, + boolean isLarge, ViewGroup parent) { + ImageView v = new ImageView(context); + v.setImageIcon(icon); + parent.addView(v); + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + if (isLarge) { + // XXX better way to convert from icon -> bitmap or crop an icon (?) + Bitmap iconBm = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); + Canvas iconCanvas = new Canvas(iconBm); + v.layout(0, 0, iconSize, iconSize); + v.draw(iconCanvas); + v.setImageBitmap(getCircularBitmap(iconBm)); + } else { + v.setColorFilter(Color.WHITE); + } + lp.width = iconSize; + lp.height = iconSize; + lp.gravity = Gravity.CENTER; + } + + /** + * @hide + */ + public static Bitmap getCircularBitmap(Bitmap bitmap) { + Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), + bitmap.getHeight(), Config.ARGB_8888); + Canvas canvas = new Canvas(output); + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2, + bitmap.getWidth() / 2, paint); + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + return output; + } +} diff --git a/android/slice/views/SmallTemplateView.java b/android/slice/views/SmallTemplateView.java new file mode 100644 index 00000000..b0b181ed --- /dev/null +++ b/android/slice/views/SmallTemplateView.java @@ -0,0 +1,211 @@ +/* + * 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.slice.views; + +import android.app.PendingIntent.CanceledException; +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; +import android.widget.TextView; + +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; + +/** + * Small template is also used to construct list items for use with {@link LargeTemplateView}. + * + * @hide + */ +public class SmallTemplateView extends SliceModeView implements SliceListView { + + private static final String TAG = "SmallTemplateView"; + + private int mIconSize; + private int mPadding; + + private LinearLayout mStartContainer; + private TextView mTitleText; + private TextView mSecondaryText; + private LinearLayout mEndContainer; + + public SmallTemplateView(Context context) { + super(context); + inflate(context, R.layout.slice_small_template, this); + mIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.slice_icon_size); + mPadding = getContext().getResources().getDimensionPixelSize(R.dimen.slice_padding); + + mStartContainer = (LinearLayout) findViewById(android.R.id.icon_frame); + mTitleText = (TextView) findViewById(android.R.id.title); + mSecondaryText = (TextView) findViewById(android.R.id.summary); + mEndContainer = (LinearLayout) findViewById(android.R.id.widget_frame); + } + + @Override + public String getMode() { + return SliceView.MODE_SMALL; + } + + @Override + public void setSliceItem(SliceItem slice) { + resetViews(); + SliceItem colorItem = SliceQuery.find(slice, SliceItem.TYPE_COLOR); + int color = colorItem != null ? colorItem.getColor() : -1; + + // Look for any title elements + List<SliceItem> titleItems = SliceQuery.findAll(slice, -1, Slice.HINT_TITLE, + null); + boolean hasTitleText = false; + boolean hasTitleItem = false; + for (int i = 0; i < titleItems.size(); i++) { + SliceItem item = titleItems.get(i); + if (!hasTitleItem) { + // icon, action icon, or timestamp + if (item.getType() == SliceItem.TYPE_ACTION) { + hasTitleItem = addIcon(item, color, mStartContainer); + } else if (item.getType() == SliceItem.TYPE_IMAGE) { + addIcon(item, color, mStartContainer); + hasTitleItem = true; + } else if (item.getType() == SliceItem.TYPE_TIMESTAMP) { + TextView tv = new TextView(getContext()); + tv.setText(convertTimeToString(item.getTimestamp())); + hasTitleItem = true; + } + } + if (!hasTitleText && item.getType() == SliceItem.TYPE_TEXT) { + mTitleText.setText(item.getText()); + hasTitleText = true; + } + if (hasTitleText && hasTitleItem) { + break; + } + } + mTitleText.setVisibility(hasTitleText ? View.VISIBLE : View.GONE); + mStartContainer.setVisibility(hasTitleItem ? View.VISIBLE : View.GONE); + + if (slice.getType() != SliceItem.TYPE_SLICE) { + return; + } + + // Deal with remaining items + int itemCount = 0; + boolean hasSummary = false; + ArrayList<SliceItem> sliceItems = new ArrayList<SliceItem>( + Arrays.asList(slice.getSlice().getItems())); + for (int i = 0; i < sliceItems.size(); i++) { + SliceItem item = sliceItems.get(i); + if (!hasSummary && item.getType() == SliceItem.TYPE_TEXT + && !item.hasHint(Slice.HINT_TITLE)) { + // TODO -- Should combine all text items? + mSecondaryText.setText(item.getText()); + hasSummary = true; + } + if (itemCount <= 3) { + if (item.getType() == SliceItem.TYPE_ACTION) { + if (addIcon(item, color, mEndContainer)) { + itemCount++; + } + } else if (item.getType() == SliceItem.TYPE_IMAGE) { + addIcon(item, color, mEndContainer); + itemCount++; + } else if (item.getType() == SliceItem.TYPE_TIMESTAMP) { + TextView tv = new TextView(getContext()); + tv.setText(convertTimeToString(item.getTimestamp())); + 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]); + } + } + } + } + } + + @Override + public void setSlice(Slice slice) { + setSliceItem(new SliceItem(slice, SliceItem.TYPE_SLICE, slice.getHints())); + } + + /** + * @return Whether an icon was added. + */ + private boolean addIcon(SliceItem sliceItem, int color, LinearLayout container) { + SliceItem image = null; + SliceItem action = null; + if (sliceItem.getType() == SliceItem.TYPE_ACTION) { + image = SliceQuery.find(sliceItem.getSlice(), SliceItem.TYPE_IMAGE); + action = sliceItem; + } else if (sliceItem.getType() == SliceItem.TYPE_IMAGE) { + image = sliceItem; + } + if (image != null) { + ImageView iv = new ImageView(getContext()); + iv.setImageIcon(image.getIcon()); + if (action != null) { + final SliceItem sliceAction = action; + iv.setOnClickListener(v -> AsyncTask.execute( + () -> { + try { + sliceAction.getAction().send(); + } catch (CanceledException e) { + e.printStackTrace(); + } + })); + iv.setBackground(SliceViewUtil.getDrawable(getContext(), + android.R.attr.selectableItemBackground)); + } + if (color != -1 && !sliceItem.hasHint(Slice.HINT_NO_TINT)) { + iv.setColorFilter(color); + } + container.addView(iv); + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) iv.getLayoutParams(); + lp.width = mIconSize; + lp.height = mIconSize; + lp.setMarginStart(mPadding); + return true; + } + return false; + } + + private String convertTimeToString(long time) { + // TODO -- figure out what format(s) we support + Date date = new Date(time); + Format format = new SimpleDateFormat("MM dd yyyy HH:mm:ss"); + return format.format(date); + } + + private void resetViews() { + mStartContainer.removeAllViews(); + mEndContainer.removeAllViews(); + mTitleText.setText(null); + mSecondaryText.setText(null); + } +} diff --git a/android/support/LibraryVersions.java b/android/support/LibraryVersions.java index f8e6e81a..a046d95e 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-alpha9-1"); + private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0-beta2"); /** * Version code for Room @@ -43,12 +43,12 @@ public class LibraryVersions { /** * Version code for RecyclerView & Room paging */ - public static final Version PAGING = new Version("1.0.0-alpha1"); + public static final Version PAGING = new Version("1.0.0-alpha3"); /** * Version code for Lifecycle libs that are required by the support library */ - public static final Version LIFECYCLES_CORE = new Version("1.0.0"); + public static final Version LIFECYCLES_CORE = new Version("1.0.2"); /** * Version code for Lifecycle runtime libs that are required by the support library diff --git a/android/support/Version.java b/android/support/Version.java index b88d8cfe..69b7f5e2 100644 --- a/android/support/Version.java +++ b/android/support/Version.java @@ -42,7 +42,29 @@ public class Version implements Comparable<Version> { @Override public int compareTo(Version version) { - return mMajor != version.mMajor ? mMajor - version.mMajor : mMinor - version.mMinor; + if (mMajor != version.mMajor) { + return mMajor - version.mMajor; + } + if (mMinor != version.mMinor) { + return mMinor - version.mMinor; + } + if (mPatch != version.mPatch) { + return mPatch - version.mPatch; + } + if (mExtra == null) { + if (version.mExtra == null) { + return 0; + } + // not having any extra is always a later version + return 1; + } else { + if (version.mExtra == null) { + // not having any extra is always a later version + return -1; + } + // gradle uses lexicographic ordering + return mExtra.compareTo(version.mExtra); + } } public boolean isPatch() { @@ -73,4 +95,26 @@ public class Version implements Comparable<Version> { public String toString() { return mMajor + "." + mMinor + "." + mPatch + (mExtra != null ? mExtra : ""); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Version version = (Version) o; + + if (mMajor != version.mMajor) return false; + if (mMinor != version.mMinor) return false; + if (mPatch != version.mPatch) return false; + return mExtra != null ? mExtra.equals(version.mExtra) : version.mExtra == null; + } + + @Override + public int hashCode() { + int result = mMajor; + result = 31 * result + mMinor; + result = 31 * result + mPatch; + result = 31 * result + (mExtra != null ? mExtra.hashCode() : 0); + return result; + } } diff --git a/android/support/VersionFileWriterTask.java b/android/support/VersionFileWriterTask.java new file mode 100644 index 00000000..aafa0236 --- /dev/null +++ b/android/support/VersionFileWriterTask.java @@ -0,0 +1,109 @@ +/* + * 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; + +import com.android.build.gradle.LibraryExtension; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Task that allows to write a version to a given output file. + */ +public class VersionFileWriterTask extends DefaultTask { + public static final String RESOURCE_DIRECTORY = "generatedResources"; + public static final String VERSION_FILE_PATH = + RESOURCE_DIRECTORY + "/META-INF/%s_%s.version"; + + private String mVersion; + private File mOutputFile; + + /** + * Sets up Android Library project to have a task that generates a version file. + * + * @param project an Android Library project. + */ + public static void setUpAndroidLibrary(Project project) { + project.afterEvaluate(new Action<Project>() { + @Override + public void execute(Project project) { + LibraryExtension library = + project.getExtensions().findByType(LibraryExtension.class); + + String group = (String) project.getProperties().get("group"); + String artifactId = (String) project.getProperties().get("name"); + String version = (String) project.getProperties().get("version"); + + // Add a java resource file to the library jar for version tracking purposes. + File artifactName = new File(project.getBuildDir(), + String.format(VersionFileWriterTask.VERSION_FILE_PATH, + group, artifactId)); + + VersionFileWriterTask writeVersionFile = + project.getTasks().create("writeVersionFile", VersionFileWriterTask.class); + writeVersionFile.setVersion(version); + writeVersionFile.setOutputFile(artifactName); + + library.getLibraryVariants().all( + libraryVariant -> libraryVariant.getProcessJavaResources().dependsOn( + writeVersionFile)); + + library.getSourceSets().getByName("main").getResources().srcDir( + new File(project.getBuildDir(), VersionFileWriterTask.RESOURCE_DIRECTORY) + ); + } + }); + } + + @Input + public String getVersion() { + return mVersion; + } + + public void setVersion(String version) { + mVersion = version; + } + + @OutputFile + public File getOutputFile() { + return mOutputFile; + } + + public void setOutputFile(File outputFile) { + mOutputFile = outputFile; + } + + /** + * The main method for actually writing out the file. + * + * @throws IOException + */ + @TaskAction + public void run() throws IOException { + PrintWriter writer = new PrintWriter(mOutputFile); + writer.println(mVersion); + writer.close(); + } +} diff --git a/android/support/annotation/NavigationRes.java b/android/support/annotation/NavigationRes.java new file mode 100644 index 00000000..a0510268 --- /dev/null +++ b/android/support/annotation/NavigationRes.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.annotation; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a navigation resource reference (e.g. {@code R.navigation.flow}). + */ +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE}) +public @interface NavigationRes { +} diff --git a/android/support/car/utils/ColumnCalculator.java b/android/support/car/utils/ColumnCalculator.java new file mode 100644 index 00000000..96e081b9 --- /dev/null +++ b/android/support/car/utils/ColumnCalculator.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.support.car.utils; + +import android.content.Context; +import android.content.res.Resources; +import android.support.car.R; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; + +/** + * Utility class that calculates the size of the columns that will fit on the screen. A column's + * width is determined by the size of the margins and gutters (space between the columns) that fit + * on-screen. + * + * <p>Refer to the appropriate dimens and integers for the size of the margins and number of + * columns. + */ +public class ColumnCalculator { + private static final String TAG = "ColumnCalculator"; + + private static ColumnCalculator sInstance; + private static int sScreenWidth; + + private int mNumOfColumns; + private int mNumOfGutters; + private int mColumnWidth; + private int mGutterSize; + + /** + * Gets an instance of the {@link ColumnCalculator}. If this is the first time that this + * method has been called, then the given {@link Context} will be used to retrieve resources. + * + * @param context The current calling Context. + * @return An instance of {@link ColumnCalculator}. + */ + public static ColumnCalculator getInstance(Context context) { + if (sInstance == null) { + WindowManager windowManager = (WindowManager) context.getSystemService( + Context.WINDOW_SERVICE); + DisplayMetrics displayMetrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(displayMetrics); + sScreenWidth = displayMetrics.widthPixels; + + sInstance = new ColumnCalculator(context); + } + + return sInstance; + } + + private ColumnCalculator(Context context) { + Resources res = context.getResources(); + int marginSize = res.getDimensionPixelSize(R.dimen.car_screen_margin_size); + mGutterSize = res.getDimensionPixelSize(R.dimen.car_screen_gutter_size); + mNumOfColumns = res.getInteger(R.integer.car_screen_num_of_columns); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, String.format("marginSize: %d; numOfColumns: %d; gutterSize: %d", + marginSize, mNumOfColumns, mGutterSize)); + } + + // The gutters appear between each column. As a result, the number of gutters is one less + // than the number of columns. + mNumOfGutters = mNumOfColumns - 1; + + // Determine the spacing that is allowed to be filled by the columns by subtracting margins + // on both size of the screen and the space taken up by the gutters. + int spaceForColumns = sScreenWidth - (2 * marginSize) - (mNumOfGutters * mGutterSize); + + mColumnWidth = spaceForColumns / mNumOfColumns; + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "mColumnWidth: " + mColumnWidth); + } + } + + /** + * Returns the total number of columns that fit on the current screen. + * + * @return The total number of columns that fit on the screen. + */ + public int getNumOfColumns() { + return mNumOfColumns; + } + + /** + * Returns the size in pixels of each column. The column width is determined by the size of the + * screen divided by the number of columns, size of gutters and margins. + * + * @return The width of a single column in pixels. + */ + public int getColumnWidth() { + return mColumnWidth; + } + + /** + * Returns the total number of gutters that fit on screen. A gutter is the space between each + * column. This value is always one less than the number of columns. + * + * @return The number of gutters on screen. + */ + public int getNumOfGutters() { + return mNumOfGutters; + } + + /** + * Returns the size of each gutter in pixels. A gutter is the space between each column. + * + * @return The size of a single gutter in pixels. + */ + public int getGutterSize() { + return mGutterSize; + } + + /** + * Returns the size in pixels for the given number of columns. This value takes into account + * the size of the gutter between the columns as well. For example, for a column span of four, + * the size returned is the sum of four columns and three gutters. + * + * @return The size in pixels for a given column span. + */ + public int getSizeForColumnSpan(int columnSpan) { + int gutterSpan = columnSpan - 1; + return columnSpan * mColumnWidth + gutterSpan * mGutterSize; + } +} diff --git a/android/support/car/widget/CarItemAnimator.java b/android/support/car/widget/CarItemAnimator.java new file mode 100644 index 00000000..4dd32127 --- /dev/null +++ b/android/support/car/widget/CarItemAnimator.java @@ -0,0 +1,70 @@ +/* + * 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.widget; + +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.RecyclerView; + +/** {@link DefaultItemAnimator} with a few minor changes where it had undesired behavior. */ +public class CarItemAnimator extends DefaultItemAnimator { + + private final CarLayoutManager mLayoutManager; + + public CarItemAnimator(CarLayoutManager layoutManager) { + mLayoutManager = layoutManager; + } + + @Override + public boolean animateChange(RecyclerView.ViewHolder oldHolder, + RecyclerView.ViewHolder newHolder, + int fromX, + int fromY, + int toX, + int toY) { + // The default behavior will cross fade the old view and the new one. However, if we + // have a card on a colored background, it will make it appear as if a changing card + // fades in and out. + float alpha = 0f; + if (newHolder != null) { + alpha = newHolder.itemView.getAlpha(); + } + boolean ret = super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY); + if (newHolder != null) { + newHolder.itemView.setAlpha(alpha); + } + return ret; + } + + @Override + public void onMoveFinished(RecyclerView.ViewHolder item) { + // The item animator uses translation heavily internally. However, we also use translation + // to create the paging affect. When an item's move is animated, it will mess up the + // translation we have set on it so we must re-offset the rows once the animations finish. + + // isRunning(ItemAnimationFinishedListener) is the awkward API used to determine when all + // animations have finished. + isRunning(mFinishedListener); + } + + private final ItemAnimatorFinishedListener mFinishedListener = + new ItemAnimatorFinishedListener() { + @Override + public void onAnimationsFinished() { + mLayoutManager.offsetRows(); + } + }; +} diff --git a/android/support/car/widget/CarLayoutManager.java b/android/support/car/widget/CarLayoutManager.java new file mode 100644 index 00000000..d0d3a9e1 --- /dev/null +++ b/android/support/car/widget/CarLayoutManager.java @@ -0,0 +1,1636 @@ +/* + * 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.widget; + +import android.content.Context; +import android.graphics.PointF; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.car.R; +import android.support.v7.widget.LinearSmoothScroller; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Recycler; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.LruCache; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.Transformation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +/** + * Custom {@link RecyclerView.LayoutManager} that behaves similar to LinearLayoutManager except that + * it has a few tricks up its sleeve. + * + * <ol> + * <li>In a normal ListView, when views reach the top of the list, they are clipped. In + * CarLayoutManager, views have the option of flying off of the top of the screen as the next + * row settles in to place. This functionality can be enabled or disabled with {@link + * #setOffsetRows(boolean)}. + * <li>Standard list physics is disabled. Instead, when the user scrolls, it will settle on the + * next page. + * <li>Items can scroll past the bottom edge of the screen. This helps with pagination so that the + * last page can be properly aligned. + * </ol> + * + * This LayoutManger should be used with {@link CarRecyclerView}. + */ +public class CarLayoutManager extends RecyclerView.LayoutManager { + private static final String TAG = "CarLayoutManager"; + + /** + * Any fling below the threshold will just scroll to the top fully visible row. The units is + * whatever {@link android.widget.Scroller} would return. + * + * <p>A reasonable value is ~200 + * + * <p>This can be disabled by setting the threshold to -1. + */ + private static final int FLING_THRESHOLD_TO_PAGINATE = -1; + + /** + * Any fling shorter than this threshold (in px) will just scroll to the top fully visible row. + * + * <p>A reasonable value is 15. + * + * <p>This can be disabled by setting the distance to -1. + */ + private static final int DRAG_DISTANCE_TO_PAGINATE = -1; + + /** + * If you scroll really quickly, you can hit the end of the laid out rows before Android has a + * chance to layout more. To help counter this, we can layout a number of extra rows past + * wherever the focus is if necessary. + */ + private static final int NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS = 2; + + /** + * Scroll bar calculation is a bit complicated. This basically defines the granularity we want + * our scroll bar to move. Set this to 1 means our scrollbar will have really jerky movement. + * Setting it too big will risk an overflow (although there is no performance impact). Ideally + * we want to set this higher than the height of our list view. We can't use our list view + * height directly though because we might run into situations where getHeight() returns 0, + * for example, when the view is not yet measured. + */ + private static final int SCROLL_RANGE = 1000; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({BEFORE, AFTER}) + private @interface LayoutDirection {} + + private static final int BEFORE = 0; + private static final int AFTER = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ROW_OFFSET_MODE_INDIVIDUAL, ROW_OFFSET_MODE_PAGE}) + public @interface RowOffsetMode {} + + public static final int ROW_OFFSET_MODE_INDIVIDUAL = 0; + public static final int ROW_OFFSET_MODE_PAGE = 1; + + private final AccelerateInterpolator mDanglingRowInterpolator = new AccelerateInterpolator(2); + private final Context mContext; + + /** Determines whether or not rows will be offset as they slide off screen * */ + private boolean mOffsetRows; + + /** Determines whether rows will be offset individually or a page at a time * */ + @RowOffsetMode private int mRowOffsetMode = ROW_OFFSET_MODE_PAGE; + + /** + * The LayoutManager only gets {@link #onScrollStateChanged(int)} updates. This enables the + * scroll state to be used anywhere. + */ + private int mScrollState = RecyclerView.SCROLL_STATE_IDLE; + + /** Used to inspect the current scroll state to help with the various calculations. */ + private CarSmoothScroller mSmoothScroller; + + private PagedListView.OnScrollListener mOnScrollListener; + + /** The distance that the list has actually scrolled in the most recent drag gesture. */ + private int mLastDragDistance = 0; + + /** {@code True} if the current drag was limited/capped because it was at some boundary. */ + private boolean mReachedLimitOfDrag; + + /** The index of the first item on the current page. */ + private int mAnchorPageBreakPosition = 0; + + /** The index of the first item on the previous page. */ + private int mUpperPageBreakPosition = -1; + + /** The index of the first item on the next page. */ + private int mLowerPageBreakPosition = -1; + + /** Used in the bookkeeping of mario style scrolling to prevent extra calculations. */ + private int mLastChildPositionToRequestFocus = -1; + + private int mSampleViewHeight = -1; + + /** Used for onPageUp and onPageDown */ + private int mViewsPerPage = 1; + + private int mCurrentPage = 0; + + private static final int MAX_ANIMATIONS_IN_CACHE = 30; + /** + * Cache of TranslateAnimation per child view. These are needed since using a single animation + * for all children doesn't apply the animation effect multiple times. Key = the view the + * animation will transform. + */ + private LruCache<View, TranslateAnimation> mFlyOffscreenAnimations; + + /** Set the anchor to the following position on the next layout pass. */ + private int mPendingScrollPosition = -1; + + public CarLayoutManager(Context context) { + mContext = context; + } + + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new RecyclerView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + } + + @Override + public boolean canScrollVertically() { + return true; + } + + /** + * onLayoutChildren is sort of like a "reset" for the layout state. At a high level, it should: + * + * <ol> + * <li>Check the current views to get the current state of affairs + * <li>Detach all views from the window (a lightweight operation) so that rows not re-added + * will be removed after onLayoutChildren. + * <li>Re-add rows as necessary. + * </ol> + * + * @see super#onLayoutChildren(RecyclerView.Recycler, RecyclerView.State) + */ + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + /* + * The anchor view is the first fully visible view on screen at the beginning of + * onLayoutChildren (or 0 if there is none). This row will be laid out first. After that, + * layoutNextRow will layout rows above and below it until the boundaries of what should be + * laid out have been reached. See shouldLayoutNextRow(View, int) for more info. + */ + int anchorPosition = 0; + int anchorTop = -1; + if (mPendingScrollPosition == -1) { + View anchor = getFirstFullyVisibleChild(); + if (anchor != null) { + anchorPosition = getPosition(anchor); + anchorTop = getDecoratedTop(anchor); + } + } else { + anchorPosition = mPendingScrollPosition; + mPendingScrollPosition = -1; + mAnchorPageBreakPosition = anchorPosition; + mUpperPageBreakPosition = -1; + mLowerPageBreakPosition = -1; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v( + TAG, + String.format( + ":: onLayoutChildren anchorPosition:%s, anchorTop:%s," + + " mPendingScrollPosition: %s, mAnchorPageBreakPosition:%s," + + " mUpperPageBreakPosition:%s, mLowerPageBreakPosition:%s", + anchorPosition, + anchorTop, + mPendingScrollPosition, + mAnchorPageBreakPosition, + mUpperPageBreakPosition, + mLowerPageBreakPosition)); + } + + /* + * Detach all attached view for 2 reasons: + * + * 1) So that views are put in the scrap heap. This enables us to call {@link + * RecyclerView.Recycler#getViewForPosition(int)} which will either return one of these + * detached views if it is in the scrap heap, one from the recycled pool (will only call + * onBind in the adapter), or create an entirely new row if needed (will call onCreate + * and onBind in the adapter). + * 2) So that views are automatically removed if they are not manually re-added. + */ + detachAndScrapAttachedViews(recycler); + + /* + * Layout the views recursively. + * + * It's possible that this re-layout is triggered because an item gets removed. If the + * anchor view is at the end of the list, the anchor view position will be bigger than the + * number of available items. Correct that, and only start the layout if the anchor + * position is valid. + */ + anchorPosition = Math.min(anchorPosition, getItemCount() - 1); + if (anchorPosition >= 0) { + View anchor = layoutAnchor(recycler, anchorPosition, anchorTop); + View adjacentRow = anchor; + while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) { + adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE); + } + adjacentRow = anchor; + while (shouldLayoutNextRow(state, adjacentRow, AFTER)) { + adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER); + } + } + + updatePageBreakPositions(); + offsetRows(); + + if (Log.isLoggable(TAG, Log.VERBOSE) && getChildCount() > 1) { + Log.v(TAG, "Currently showing " + + getChildCount() + + " views " + + getPosition(getChildAt(0)) + + " to " + + getPosition(getChildAt(getChildCount() - 1)) + + " anchor " + + anchorPosition); + } + // Should be at least 1 + mViewsPerPage = + Math.max(getLastFullyVisibleChildIndex() + 1 - getFirstFullyVisibleChildIndex(), 1); + mCurrentPage = getFirstFullyVisibleChildPosition() / mViewsPerPage; + Log.v(TAG, "viewsPerPage " + mViewsPerPage); + } + + /** + * scrollVerticallyBy does the work of what should happen when the list scrolls in addition to + * handling cases where the list hits the end. It should be lighter weight than + * onLayoutChildren. It doesn't have to detach all views. It only looks at the end of the list + * and removes views that have gone out of bounds and lays out new ones that scroll in. + * + * @param dy The amount that the list is supposed to scroll. > 0 means the list is scrolling + * down. < 0 means the list is scrolling up. + * @param recycler The recycler that enables views to be reused or created as they scroll in. + * @param state Various information about the current state of affairs. + * @return The amount the list actually scrolled. + * @see super#scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State) + */ + @Override + public int scrollVerticallyBy( + int dy, @NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) { + // If the list is empty, we can prevent the overscroll glow from showing by just + // telling RecycerView that we scrolled. + if (getItemCount() == 0) { + return dy; + } + + // Prevent redundant computations if there is definitely nowhere to scroll to. + if (getChildCount() <= 1 || dy == 0) { + mReachedLimitOfDrag = true; + return 0; + } + + View firstChild = getChildAt(0); + if (firstChild == null) { + mReachedLimitOfDrag = true; + return 0; + } + int firstChildPosition = getPosition(firstChild); + RecyclerView.LayoutParams firstChildParams = getParams(firstChild); + int firstChildTopWithMargin = getDecoratedTop(firstChild) - firstChildParams.topMargin; + + View lastFullyVisibleView = getChildAt(getLastFullyVisibleChildIndex()); + if (lastFullyVisibleView == null) { + mReachedLimitOfDrag = true; + return 0; + } + boolean isLastViewVisible = getPosition(lastFullyVisibleView) == getItemCount() - 1; + + View firstFullyVisibleChild = getFirstFullyVisibleChild(); + if (firstFullyVisibleChild == null) { + mReachedLimitOfDrag = true; + return 0; + } + int firstFullyVisiblePosition = getPosition(firstFullyVisibleChild); + RecyclerView.LayoutParams firstFullyVisibleChildParams = getParams(firstFullyVisibleChild); + int topRemainingSpace = + getDecoratedTop(firstFullyVisibleChild) + - firstFullyVisibleChildParams.topMargin + - getPaddingTop(); + + if (isLastViewVisible + && firstFullyVisiblePosition == mAnchorPageBreakPosition + && dy > topRemainingSpace + && dy > 0) { + // Prevent dragging down more than 1 page. As a side effect, this also prevents you + // from dragging past the bottom because if you are on the second to last page, it + // prevents you from dragging past the last page. + dy = topRemainingSpace; + mReachedLimitOfDrag = true; + } else if (dy < 0 + && firstChildPosition == 0 + && firstChildTopWithMargin + Math.abs(dy) > getPaddingTop()) { + // Prevent scrolling past the beginning + dy = firstChildTopWithMargin - getPaddingTop(); + mReachedLimitOfDrag = true; + } else { + mReachedLimitOfDrag = false; + } + + boolean isDragging = mScrollState == RecyclerView.SCROLL_STATE_DRAGGING; + if (isDragging) { + mLastDragDistance += dy; + } + // We offset by -dy because the views translate in the opposite direction that the + // list scrolls (think about it.) + offsetChildrenVertical(-dy); + + // The last item in the layout should never scroll above the viewport + View view = getChildAt(getChildCount() - 1); + if (view.getTop() < 0) { + view.setTop(0); + } + + // This is the meat of this function. We remove views on the trailing edge of the scroll + // and add views at the leading edge as necessary. + View adjacentRow; + if (dy > 0) { + recycleChildrenFromStart(recycler); + adjacentRow = getChildAt(getChildCount() - 1); + while (shouldLayoutNextRow(state, adjacentRow, AFTER)) { + adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER); + } + } else { + recycleChildrenFromEnd(recycler); + adjacentRow = getChildAt(0); + while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) { + adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE); + } + } + // Now that the correct views are laid out, offset rows as necessary so we can do whatever + // fancy animation we want such as having the top view fly off the screen as the next one + // settles in to place. + updatePageBreakPositions(); + offsetRows(); + + if (getChildCount() > 1) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v( + TAG, + String.format( + "Currently showing %d views (%d to %d)", + getChildCount(), + getPosition(getChildAt(0)), + getPosition(getChildAt(getChildCount() - 1)))); + } + } + updatePagedState(); + return dy; + } + + private void updatePagedState() { + int page = getFirstFullyVisibleChildPosition() / mViewsPerPage; + if (mOnScrollListener != null) { + if (page > mCurrentPage) { + mOnScrollListener.onPageDown(); + } else if (page < mCurrentPage) { + mOnScrollListener.onPageUp(); + } + } + mCurrentPage = page; + } + + @Override + public void scrollToPosition(int position) { + mPendingScrollPosition = position; + requestLayout(); + } + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, + int position) { + /* + * startSmoothScroll will handle stopping the old one if there is one. We only keep a copy + * of it to handle the translation of rows as they slide off the screen in + * offsetRowsWithPageBreak(). + */ + mSmoothScroller = new CarSmoothScroller(mContext, position); + mSmoothScroller.setTargetPosition(position); + startSmoothScroll(mSmoothScroller); + } + + /** Miscellaneous bookkeeping. */ + @Override + public void onScrollStateChanged(int state) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, ":: onScrollStateChanged " + state); + } + if (state == RecyclerView.SCROLL_STATE_IDLE) { + // If the focused view is off screen, give focus to one that is. + // If the first fully visible view is first in the list, focus the first item. + // Otherwise, focus the second so that you have the first item as scrolling context. + View focusedChild = getFocusedChild(); + if (focusedChild != null + && (getDecoratedTop(focusedChild) >= getHeight() - getPaddingBottom() + || getDecoratedBottom(focusedChild) <= getPaddingTop())) { + focusedChild.clearFocus(); + requestLayout(); + } + + } else if (state == RecyclerView.SCROLL_STATE_DRAGGING) { + mLastDragDistance = 0; + } + + if (state != RecyclerView.SCROLL_STATE_SETTLING) { + mSmoothScroller = null; + } + + mScrollState = state; + updatePageBreakPositions(); + } + + @Override + public void onItemsChanged(RecyclerView recyclerView) { + super.onItemsChanged(recyclerView); + // When item changed, our sample view height is no longer accurate, and need to be + // recomputed. + mSampleViewHeight = -1; + } + + /** + * Gives us the opportunity to override the order of the focused views. By default, it will just + * go from top to bottom. However, if there is no focused views, we take over the logic and + * start the focused views from the middle of what is visible and move from there until the + * end of the laid out views in the specified direction. + */ + @Override + public boolean onAddFocusables( + RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) { + View focusedChild = getFocusedChild(); + if (focusedChild != null) { + // If there is a view that already has focus, we can just return false and the normal + // Android addFocusables will work fine. + return false; + } + + // Now we know that there isn't a focused view. We need to set up focusables such that + // instead of just focusing the first item that has been laid out, it focuses starting + // from a visible item. + + int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); + if (firstFullyVisibleChildIndex == -1) { + // Somehow there is a focused view but there is no fully visible view. There shouldn't + // be a way for this to happen but we'd better stop here and return instead of + // continuing on with -1. + Log.w(TAG, "There is a focused child but no first fully visible child."); + return false; + } + View firstFullyVisibleChild = getChildAt(firstFullyVisibleChildIndex); + int firstFullyVisibleChildPosition = getPosition(firstFullyVisibleChild); + + int firstFocusableChildIndex = firstFullyVisibleChildIndex; + if (firstFullyVisibleChildPosition > 0 && firstFocusableChildIndex + 1 < getItemCount()) { + // We are somewhere in the middle of the list. Instead of starting focus on the first + // item, start focus on the second item to give some context that we aren't at + // the beginning. + firstFocusableChildIndex++; + } + + if (direction == View.FOCUS_FORWARD) { + // Iterate from the first focusable view to the end. + for (int i = firstFocusableChildIndex; i < getChildCount(); i++) { + views.add(getChildAt(i)); + } + return true; + } else if (direction == View.FOCUS_BACKWARD) { + // Iterate from the first focusable view to the beginning. + for (int i = firstFocusableChildIndex; i >= 0; i--) { + views.add(getChildAt(i)); + } + return true; + } else if (direction == View.FOCUS_DOWN) { + // Framework calls onAddFocusables with FOCUS_DOWN direction when the focus is first + // gained. Thereafter, it calls onAddFocusables with FOCUS_FORWARD or FOCUS_BACKWARD. + // First we try to put the focus back on the last focused item, if it is visible + int lastFocusedVisibleChildIndex = getLastFocusedChildIndexIfVisible(); + if (lastFocusedVisibleChildIndex != -1) { + views.add(getChildAt(lastFocusedVisibleChildIndex)); + return true; + } + } + return false; + } + + @Override + public View onFocusSearchFailed( + View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) { + // This doesn't seem to get called the way focus is handled in gearhead... + return null; + } + + /** + * This is the function that decides where to scroll to when a new view is focused. You can get + * the position of the currently focused child through the child parameter. Once you have that, + * determine where to smooth scroll to and scroll there. + * + * @param parent The RecyclerView hosting this LayoutManager + * @param state Current state of RecyclerView + * @param child Direct child of the RecyclerView containing the newly focused view + * @param focused The newly focused view. This may be the same view as child or it may be null + * @return {@code true} if the default scroll behavior should be suppressed + */ + @Override + public boolean onRequestChildFocus( + RecyclerView parent, RecyclerView.State state, View child, View focused) { + if (child == null) { + Log.w(TAG, "onRequestChildFocus with a null child!"); + return true; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, String.format(":: onRequestChildFocus child: %s, focused: %s", child, + focused)); + } + + return onRequestChildFocusMarioStyle(parent, child); + } + + /** + * Goal: the scrollbar maintains the same size throughout scrolling and that the scrollbar + * reaches the bottom of the screen when the last item is fully visible. This is because there + * are multiple points that could be considered the bottom since the last item can scroll past + * the bottom edge of the screen. + * + * <p>To find the extent, we divide the number of items that can fit on screen by the number of + * items in total. + */ + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + if (getChildCount() <= 1) { + return 0; + } + + int sampleViewHeight = getSampleViewHeight(); + int availableHeight = getAvailableHeight(); + int sampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight; + + if (state.getItemCount() <= sampleViewsThatCanFitOnScreen) { + return SCROLL_RANGE; + } else { + return SCROLL_RANGE * sampleViewsThatCanFitOnScreen / state.getItemCount(); + } + } + + /** + * The scrolling offset is calculated by determining what position is at the top of the list. + * However, instead of using fixed integer positions for each row, the scroll position is + * factored in and the position is recalculated as a float that takes in to account the + * current scroll state. This results in a smooth animation for the scrollbar when the user + * scrolls the list. + */ + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + View firstChild = getFirstFullyVisibleChild(); + if (firstChild == null) { + return 0; + } + + RecyclerView.LayoutParams params = getParams(firstChild); + int firstChildPosition = getPosition(firstChild); + float previousChildHieght = (float) (getDecoratedMeasuredHeight(firstChild) + + params.topMargin + params.bottomMargin); + + // Assume the previous view is the same height as the current one. + float percentOfPreviousViewShowing = (getDecoratedTop(firstChild) - params.topMargin) + / previousChildHieght; + // If the previous view is actually larger than the current one then this the percent + // can be greater than 1. + percentOfPreviousViewShowing = Math.min(percentOfPreviousViewShowing, 1); + + float currentPosition = (float) firstChildPosition - percentOfPreviousViewShowing; + + int sampleViewHeight = getSampleViewHeight(); + int availableHeight = getAvailableHeight(); + int numberOfSampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight; + int positionWhenLastItemIsVisible = + state.getItemCount() - numberOfSampleViewsThatCanFitOnScreen; + + if (positionWhenLastItemIsVisible <= 0) { + return 0; + } + + if (currentPosition >= positionWhenLastItemIsVisible) { + return SCROLL_RANGE; + } + + return (int) (SCROLL_RANGE * currentPosition / positionWhenLastItemIsVisible); + } + + /** + * The range of the scrollbar can be understood as the granularity of how we want the scrollbar + * to scroll. + */ + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + return SCROLL_RANGE; + } + + @Override + public void onAttachedToWindow(RecyclerView view) { + super.onAttachedToWindow(view); + // The purpose of calling this is so that any animation offsets are re-applied. These are + // cleared in View.onDetachedFromWindow(). + // This fixes b/27672379 + updatePageBreakPositions(); + offsetRows(); + } + + @Override + public void onDetachedFromWindow(RecyclerView recyclerView, Recycler recycler) { + super.onDetachedFromWindow(recyclerView, recycler); + } + + /** + * @return The first view that starts on screen. It assumes that it fully fits on the screen + * though. If the first fully visible child is also taller than the screen then it will + * still be returned. However, since the LayoutManager snaps to view starts, having a row + * that tall would lead to a broken experience anyways. + */ + public int getFirstFullyVisibleChildIndex() { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + RecyclerView.LayoutParams params = getParams(child); + if (getDecoratedTop(child) - params.topMargin >= getPaddingTop()) { + return i; + } + } + return -1; + } + + /** + * @return The position of first visible child in the list. -1 will be returned if there is no + * child. + */ + public int getFirstFullyVisibleChildPosition() { + View child = getFirstFullyVisibleChild(); + if (child == null) { + return -1; + } + return getPosition(child); + } + + /** + * @return The position of last visible child in the list. -1 will be returned if there is no + * child. + */ + public int getLastFullyVisibleChildPosition() { + View child = getLastFullyVisibleChild(); + if (child == null) { + return -1; + } + return getPosition(child); + } + + /** @return The first View that is completely visible on-screen. */ + public View getFirstFullyVisibleChild() { + int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); + View firstChild = null; + if (firstFullyVisibleChildIndex != -1) { + firstChild = getChildAt(firstFullyVisibleChildIndex); + } + return firstChild; + } + + /** @return The last View that is completely visible on-screen. */ + public View getLastFullyVisibleChild() { + int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex(); + View lastChild = null; + if (lastFullyVisibleChildIndex != -1) { + lastChild = getChildAt(lastFullyVisibleChildIndex); + } + return lastChild; + } + + /** + * @return The last view that ends on screen. It assumes that the start is also on screen + * though. If the last fully visible child is also taller than the screen then it will + * still be returned. However, since the LayoutManager snaps to view starts, having a row + * that tall would lead to a broken experience anyways. + */ + public int getLastFullyVisibleChildIndex() { + for (int i = getChildCount() - 1; i >= 0; i--) { + View child = getChildAt(i); + RecyclerView.LayoutParams params = getParams(child); + int childBottom = getDecoratedBottom(child) + params.bottomMargin; + int listBottom = getHeight() - getPaddingBottom(); + if (childBottom <= listBottom) { + return i; + } + } + return -1; + } + + /** + * Returns the index of the child in the list that was last focused and is currently visible to + * the user. If no child is found, returns -1. + */ + public int getLastFocusedChildIndexIfVisible() { + if (mLastChildPositionToRequestFocus == -1) { + return -1; + } + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (getPosition(child) == mLastChildPositionToRequestFocus) { + RecyclerView.LayoutParams params = getParams(child); + int childBottom = getDecoratedBottom(child) + params.bottomMargin; + int listBottom = getHeight() - getPaddingBottom(); + if (childBottom <= listBottom) { + return i; + } + break; + } + } + return -1; + } + + /** @return Whether or not the first view is fully visible. */ + public boolean isAtTop() { + // getFirstFullyVisibleChildIndex() can return -1 which indicates that there are no views + // and also means that the list is at the top. + return getFirstFullyVisibleChildIndex() <= 0; + } + + /** @return Whether or not the last view is fully visible. */ + public boolean isAtBottom() { + int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex(); + if (lastFullyVisibleChildIndex == -1) { + return true; + } + View lastFullyVisibleChild = getChildAt(lastFullyVisibleChildIndex); + return getPosition(lastFullyVisibleChild) == getItemCount() - 1; + } + + /** + * Sets whether or not the rows have an offset animation when it scrolls off-screen. The type + * of offset is determined by {@link #setRowOffsetMode(int)}. + * + * <p>A row being offset means that when they reach the top of the screen, the row is flung off + * respectively to the rest of the list. This creates a gap between the offset row(s) and the + * list. + * + * @param offsetRows {@code true} if the rows should be offset. + */ + public void setOffsetRows(boolean offsetRows) { + mOffsetRows = offsetRows; + if (offsetRows) { + // Card animation offsets are only needed when we use the flying off the screen effect + if (mFlyOffscreenAnimations == null) { + mFlyOffscreenAnimations = new LruCache<>(MAX_ANIMATIONS_IN_CACHE); + } + offsetRows(); + } else { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + setCardFlyingEffectOffset(getChildAt(i), 0); + } + mFlyOffscreenAnimations = null; + } + } + + /** + * Sets the manner of offsetting the rows when they are scrolled off-screen. The rows are either + * offset individually or the entire page being scrolled off is offset. + * + * @param mode One of {@link #ROW_OFFSET_MODE_INDIVIDUAL} or {@link #ROW_OFFSET_MODE_PAGE}. + */ + public void setRowOffsetMode(@RowOffsetMode int mode) { + if (mode == mRowOffsetMode) { + return; + } + + mRowOffsetMode = mode; + offsetRows(); + } + + /** + * Sets the listener that will be notified of various scroll events in the list. + * + * @param listener The on-scroll listener. + */ + public void setOnScrollListener(PagedListView.OnScrollListener listener) { + mOnScrollListener = listener; + } + + /** + * Finish the pagination taking into account where the gesture started (not where we are now). + * + * @return Whether the list was scrolled as a result of the fling. + */ + public boolean settleScrollForFling(RecyclerView parent, int flingVelocity) { + if (getChildCount() == 0) { + return false; + } + + if (mReachedLimitOfDrag) { + return false; + } + + // If the fling was too slow or too short, settle on the first fully visible row instead. + if (Math.abs(flingVelocity) <= FLING_THRESHOLD_TO_PAGINATE + || Math.abs(mLastDragDistance) <= DRAG_DISTANCE_TO_PAGINATE) { + int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); + if (firstFullyVisibleChildIndex != -1) { + int scrollPosition = getPosition(getChildAt(firstFullyVisibleChildIndex)); + parent.smoothScrollToPosition(scrollPosition); + return true; + } + return false; + } + + // Finish the pagination taking into account where the gesture + // started (not where we are now). + boolean isDownGesture = flingVelocity > 0 || (flingVelocity == 0 && mLastDragDistance >= 0); + boolean isUpGesture = flingVelocity < 0 || (flingVelocity == 0 && mLastDragDistance < 0); + if (isDownGesture && mLowerPageBreakPosition != -1) { + // If the last view is fully visible then only settle on the first fully visible view + // instead of the original page down position. However, don't page down if the last + // item has come fully into view. + parent.smoothScrollToPosition(mAnchorPageBreakPosition); + if (mOnScrollListener != null) { + mOnScrollListener.onGestureDown(); + } + return true; + } else if (isUpGesture && mUpperPageBreakPosition != -1) { + parent.smoothScrollToPosition(mUpperPageBreakPosition); + if (mOnScrollListener != null) { + mOnScrollListener.onGestureUp(); + } + return true; + } else { + Log.e( + TAG, + "Error setting scroll for fling! flingVelocity: \t" + + flingVelocity + + "\tlastDragDistance: " + + mLastDragDistance + + "\tpageUpAtStartOfDrag: " + + mUpperPageBreakPosition + + "\tpageDownAtStartOfDrag: " + + mLowerPageBreakPosition); + // As a last resort, at the last smooth scroller target position if there is one. + if (mSmoothScroller != null) { + parent.smoothScrollToPosition(mSmoothScroller.getTargetPosition()); + return true; + } + } + return false; + } + + /** @return The position that paging up from the current position would settle at. */ + public int getPageUpPosition() { + return mUpperPageBreakPosition; + } + + /** @return The position that paging down from the current position would settle at. */ + public int getPageDownPosition() { + return mLowerPageBreakPosition; + } + + /** + * Layout the anchor row. The anchor row is the first fully visible row. + * + * @param anchorTop The decorated top of the anchor. If it is not known or should be reset to + * the top, pass -1. + */ + private View layoutAnchor(RecyclerView.Recycler recycler, int anchorPosition, int anchorTop) { + View anchor = recycler.getViewForPosition(anchorPosition); + RecyclerView.LayoutParams params = getParams(anchor); + measureChildWithMargins(anchor, 0, 0); + int left = getPaddingLeft() + params.leftMargin; + int top = (anchorTop == -1) ? params.topMargin : anchorTop; + int right = left + getDecoratedMeasuredWidth(anchor); + int bottom = top + getDecoratedMeasuredHeight(anchor); + layoutDecorated(anchor, left, top, right, bottom); + addView(anchor); + return anchor; + } + + /** + * Lays out the next row in the specified direction next to the specified adjacent row. + * + * @param recycler The recycler from which a new view can be created. + * @param adjacentRow The View of the adjacent row which will be used to position the new one. + * @param layoutDirection The side of the adjacent row that the new row will be laid out on. + * @return The new row that was laid out. + */ + private View layoutNextRow(RecyclerView.Recycler recycler, View adjacentRow, + @LayoutDirection int layoutDirection) { + int adjacentRowPosition = getPosition(adjacentRow); + int newRowPosition = adjacentRowPosition; + if (layoutDirection == BEFORE) { + newRowPosition = adjacentRowPosition - 1; + } else if (layoutDirection == AFTER) { + newRowPosition = adjacentRowPosition + 1; + } + + // Because we detach all rows in onLayoutChildren, this will often just return a view from + // the scrap heap. + View newRow = recycler.getViewForPosition(newRowPosition); + + measureChildWithMargins(newRow, 0, 0); + RecyclerView.LayoutParams newRowParams = + (RecyclerView.LayoutParams) newRow.getLayoutParams(); + RecyclerView.LayoutParams adjacentRowParams = + (RecyclerView.LayoutParams) adjacentRow.getLayoutParams(); + int left = getPaddingLeft() + newRowParams.leftMargin; + int right = left + getDecoratedMeasuredWidth(newRow); + int top; + int bottom; + if (layoutDirection == BEFORE) { + bottom = adjacentRow.getTop() - adjacentRowParams.topMargin - newRowParams.bottomMargin; + top = bottom - getDecoratedMeasuredHeight(newRow); + } else { + top = getDecoratedBottom(adjacentRow) + adjacentRowParams.bottomMargin + + newRowParams.topMargin; + bottom = top + getDecoratedMeasuredHeight(newRow); + } + layoutDecorated(newRow, left, top, right, bottom); + + if (layoutDirection == BEFORE) { + addView(newRow, 0); + } else { + addView(newRow); + } + + return newRow; + } + + /** @return Whether another row should be laid out in the specified direction. */ + private boolean shouldLayoutNextRow( + RecyclerView.State state, View adjacentRow, @LayoutDirection int layoutDirection) { + int adjacentRowPosition = getPosition(adjacentRow); + + if (layoutDirection == BEFORE) { + if (adjacentRowPosition == 0) { + // We already laid out the first row. + return false; + } + } else if (layoutDirection == AFTER) { + if (adjacentRowPosition >= state.getItemCount() - 1) { + // We already laid out the last row. + return false; + } + } + + // If we are scrolling layout views until the target position. + if (mSmoothScroller != null) { + if (layoutDirection == BEFORE + && adjacentRowPosition >= mSmoothScroller.getTargetPosition()) { + return true; + } else if (layoutDirection == AFTER + && adjacentRowPosition <= mSmoothScroller.getTargetPosition()) { + return true; + } + } + + View focusedRow = getFocusedChild(); + if (focusedRow != null) { + int focusedRowPosition = getPosition(focusedRow); + if (layoutDirection == BEFORE && adjacentRowPosition + >= focusedRowPosition - NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) { + return true; + } else if (layoutDirection == AFTER && adjacentRowPosition + <= focusedRowPosition + NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) { + return true; + } + } + + RecyclerView.LayoutParams params = getParams(adjacentRow); + int adjacentRowTop = getDecoratedTop(adjacentRow) - params.topMargin; + int adjacentRowBottom = getDecoratedBottom(adjacentRow) - params.bottomMargin; + if (layoutDirection == BEFORE && adjacentRowTop < getPaddingTop() - getHeight()) { + // View is more than 1 page past the top of the screen and also past where the user has + // scrolled to. We want to keep one page past the top to make the scroll up calculation + // easier and scrolling smoother. + return false; + } else if (layoutDirection == AFTER + && adjacentRowBottom > getHeight() - getPaddingBottom()) { + // View is off of the bottom and also past where the user has scrolled to. + return false; + } + + return true; + } + + /** Remove and recycle views that are no longer needed. */ + private void recycleChildrenFromStart(RecyclerView.Recycler recycler) { + // Start laying out children one page before the top of the viewport. + int childrenStart = getPaddingTop() - getHeight(); + + int focusedChildPosition = Integer.MAX_VALUE; + View focusedChild = getFocusedChild(); + if (focusedChild != null) { + focusedChildPosition = getPosition(focusedChild); + } + + // Count the number of views that should be removed. + int detachedCount = 0; + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + int childEnd = getDecoratedBottom(child); + int childPosition = getPosition(child); + + if (childEnd >= childrenStart || childPosition >= focusedChildPosition - 1) { + break; + } + + detachedCount++; + } + + // Remove the number of views counted above. Done by removing the first child n times. + while (--detachedCount >= 0) { + final View child = getChildAt(0); + removeAndRecycleView(child, recycler); + } + } + + /** Remove and recycle views that are no longer needed. */ + private void recycleChildrenFromEnd(RecyclerView.Recycler recycler) { + // Layout views until the end of the viewport. + int childrenEnd = getHeight(); + + int focusedChildPosition = Integer.MIN_VALUE + 1; + View focusedChild = getFocusedChild(); + if (focusedChild != null) { + focusedChildPosition = getPosition(focusedChild); + } + + // Count the number of views that should be removed. + int firstDetachedPos = 0; + int detachedCount = 0; + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = getChildAt(i); + int childStart = getDecoratedTop(child); + int childPosition = getPosition(child); + + if (childStart <= childrenEnd || childPosition <= focusedChildPosition - 1) { + break; + } + + firstDetachedPos = i; + detachedCount++; + } + + while (--detachedCount >= 0) { + final View child = getChildAt(firstDetachedPos); + removeAndRecycleView(child, recycler); + } + } + + /** + * Offset rows to do fancy animations. If offset rows was not enabled with + * {@link #setOffsetRows}, this will do nothing. + * + * @see #offsetRowsIndividually + * @see #offsetRowsByPage + * @see #setOffsetRows + */ + public void offsetRows() { + if (!mOffsetRows) { + return; + } + + if (mRowOffsetMode == ROW_OFFSET_MODE_PAGE) { + offsetRowsByPage(); + } else if (mRowOffsetMode == ROW_OFFSET_MODE_INDIVIDUAL) { + offsetRowsIndividually(); + } + } + + /** + * Offset the single row that is scrolling off the screen such that by the time the next row + * reaches the top, it will have accelerated completely off of the screen. + */ + private void offsetRowsIndividually() { + if (getChildCount() == 0) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, ":: offsetRowsIndividually getChildCount=0"); + } + return; + } + + // Identify the dangling row. It will be the first row that is at the top of the + // list or above. + int danglingChildIndex = -1; + for (int i = getChildCount() - 1; i >= 0; i--) { + View child = getChildAt(i); + if (getDecoratedTop(child) - getParams(child).topMargin <= getPaddingTop()) { + danglingChildIndex = i; + break; + } + } + + mAnchorPageBreakPosition = danglingChildIndex; + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, ":: offsetRowsIndividually danglingChildIndex: " + danglingChildIndex); + } + + // Calculate the total amount that the view will need to scroll in order to go completely + // off screen. + RecyclerView rv = (RecyclerView) getChildAt(0).getParent(); + int[] locs = new int[2]; + rv.getLocationInWindow(locs); + int listTopInWindow = locs[1] + rv.getPaddingTop(); + int maxDanglingViewTranslation; + + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + RecyclerView.LayoutParams params = getParams(child); + + maxDanglingViewTranslation = listTopInWindow; + // If the child has a negative margin, we'll actually need to translate the view a + // little but further to get it completely off screen. + if (params.topMargin < 0) { + maxDanglingViewTranslation -= params.topMargin; + } + if (params.bottomMargin < 0) { + maxDanglingViewTranslation -= params.bottomMargin; + } + + if (i < danglingChildIndex) { + child.setAlpha(0f); + } else if (i > danglingChildIndex) { + child.setAlpha(1f); + setCardFlyingEffectOffset(child, 0); + } else { + int totalScrollDistance = + getDecoratedMeasuredHeight(child) + params.topMargin + params.bottomMargin; + + int distanceLeftInScroll = + getDecoratedBottom(child) + params.bottomMargin - getPaddingTop(); + float percentageIntoScroll = 1 - distanceLeftInScroll / (float) totalScrollDistance; + float interpolatedPercentage = + mDanglingRowInterpolator.getInterpolation(percentageIntoScroll); + + child.setAlpha(1f); + setCardFlyingEffectOffset(child, -(maxDanglingViewTranslation + * interpolatedPercentage)); + } + } + } + + /** + * When the list scrolls, the entire page of rows will offset in one contiguous block. This + * significantly reduces the amount of extra motion at the top of the screen. + */ + private void offsetRowsByPage() { + View anchorView = findViewByPosition(mAnchorPageBreakPosition); + if (anchorView == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, ":: offsetRowsByPage anchorView null"); + } + return; + } + int anchorViewTop = getDecoratedTop(anchorView) - getParams(anchorView).topMargin; + + View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition); + int upperViewTop = + getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin; + + int scrollDistance = upperViewTop - anchorViewTop; + + int distanceLeft = anchorViewTop - getPaddingTop(); + float scrollPercentage = + (Math.abs(scrollDistance) - distanceLeft) / (float) Math.abs(scrollDistance); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, String.format(":: offsetRowsByPage scrollDistance:%s, distanceLeft:%s, " + + "scrollPercentage:%s", + scrollDistance, distanceLeft, scrollPercentage)); + } + + // Calculate the total amount that the view will need to scroll in order to go completely + // off screen. + RecyclerView rv = (RecyclerView) getChildAt(0).getParent(); + int[] locs = new int[2]; + rv.getLocationInWindow(locs); + int listTopInWindow = locs[1] + rv.getPaddingTop(); + + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + int position = getPosition(child); + if (position < mUpperPageBreakPosition) { + child.setAlpha(0f); + setCardFlyingEffectOffset(child, -listTopInWindow); + } else if (position < mAnchorPageBreakPosition) { + // If the child has a negative margin, we need to offset the row by a little bit + // extra so that it moves completely off screen. + RecyclerView.LayoutParams params = getParams(child); + int extraTranslation = 0; + if (params.topMargin < 0) { + extraTranslation -= params.topMargin; + } + if (params.bottomMargin < 0) { + extraTranslation -= params.bottomMargin; + } + int translation = (int) ((listTopInWindow + extraTranslation) + * mDanglingRowInterpolator.getInterpolation(scrollPercentage)); + child.setAlpha(1f); + setCardFlyingEffectOffset(child, -translation); + } else { + child.setAlpha(1f); + setCardFlyingEffectOffset(child, 0); + } + } + } + + /** + * Apply an offset to this view. This offset is applied post-layout so it doesn't affect when + * views are recycled + * + * @param child The view to apply this to + * @param verticalOffset The offset for this child. + */ + private void setCardFlyingEffectOffset(View child, float verticalOffset) { + // Ideally instead of doing all this, we could use View.setTranslationY(). However, the + // default RecyclerView.ItemAnimator also uses this method which causes layout issues. + // See: http://b/25977087 + TranslateAnimation anim = mFlyOffscreenAnimations.get(child); + if (anim == null) { + anim = new TranslateAnimation(); + anim.setFillEnabled(true); + anim.setFillAfter(true); + anim.setDuration(0); + mFlyOffscreenAnimations.put(child, anim); + } else if (anim.verticalOffset == verticalOffset) { + return; + } + + anim.reset(); + anim.verticalOffset = verticalOffset; + anim.setStartTime(Animation.START_ON_FIRST_FRAME); + child.setAnimation(anim); + anim.startNow(); + } + + /** + * Update the page break positions based on the position of the views on screen. This should be + * called whenever view move or change such as during a scroll or layout. + */ + private void updatePageBreakPositions() { + if (getChildCount() == 0) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, ":: updatePageBreakPosition getChildCount: 0"); + } + return; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, String.format(":: #BEFORE updatePageBreakPositions " + + "mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s, " + + "mLowerPageBreakPosition:%s", + mAnchorPageBreakPosition, mUpperPageBreakPosition, + mLowerPageBreakPosition)); + } + + mAnchorPageBreakPosition = getPosition(getFirstFullyVisibleChild()); + + if (mAnchorPageBreakPosition == -1) { + Log.w(TAG, "Unable to update anchor positions. There is no anchor position."); + return; + } + + View anchorPageBreakView = findViewByPosition(mAnchorPageBreakPosition); + if (anchorPageBreakView == null) { + return; + } + int topMargin = getParams(anchorPageBreakView).topMargin; + int anchorTop = getDecoratedTop(anchorPageBreakView) - topMargin; + View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition); + int upperPageBreakTop = upperPageBreakView == null + ? Integer.MIN_VALUE + : getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin; + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, String.format(":: #MID updatePageBreakPositions topMargin:%s, anchorTop:%s" + + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s," + + " mLowerPageBreakPosition:%s", + topMargin, + anchorTop, + mAnchorPageBreakPosition, + mUpperPageBreakPosition, + mLowerPageBreakPosition)); + } + + if (anchorTop < getPaddingTop()) { + // The anchor has moved above the viewport. We are now on the next page. Shift the page + // break positions and calculate a new lower one. + mUpperPageBreakPosition = mAnchorPageBreakPosition; + mAnchorPageBreakPosition = mLowerPageBreakPosition; + mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition); + } else if (mAnchorPageBreakPosition > 0 && upperPageBreakTop >= getPaddingTop()) { + // The anchor has moved below the viewport. We are now on the previous page. Shift + // the page break positions and calculate a new upper one. + mLowerPageBreakPosition = mAnchorPageBreakPosition; + mAnchorPageBreakPosition = mUpperPageBreakPosition; + mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition); + } else { + mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition); + mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition); + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, String.format(":: #AFTER updatePageBreakPositions" + + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s," + + " mLowerPageBreakPosition:%s", + mAnchorPageBreakPosition, mUpperPageBreakPosition, + mLowerPageBreakPosition)); + } + } + + /** + * @return The page break position of the page before the anchor page break position. However, + * if it reaches the end of the laid out children or position 0, it will just return that. + */ + @VisibleForTesting + int calculatePreviousPageBreakPosition(int position) { + if (position == -1) { + return -1; + } + View referenceView = findViewByPosition(position); + int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin; + + int previousPagePosition = position; + while (previousPagePosition > 0) { + previousPagePosition--; + View child = findViewByPosition(previousPagePosition); + if (child == null) { + // View has not been laid out yet. + return previousPagePosition + 1; + } + + int childTop = getDecoratedTop(child) - getParams(child).topMargin; + if (childTop < referenceViewTop - getHeight()) { + return previousPagePosition + 1; + } + } + // Beginning of the list. + return 0; + } + + /** + * @return The page break position of the next page after the anchor page break position. + * However, if it reaches the end of the laid out children or end of the list, it will just + * return that. + */ + @VisibleForTesting + int calculateNextPageBreakPosition(int position) { + if (position == -1) { + return -1; + } + + View referenceView = findViewByPosition(position); + if (referenceView == null) { + return position; + } + int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin; + + int nextPagePosition = position; + + // Search for the first child item after the referenceView that didn't fully fit on to the + // screen. The next page should start from the item before this child, so that users have + // a visual anchoring point of the page change. + while (nextPagePosition < getItemCount() - 1) { + nextPagePosition++; + View child = findViewByPosition(nextPagePosition); + if (child == null) { + // The next view has not been laid out yet. + return nextPagePosition - 1; + } + + int childTop = getDecoratedTop(child) - getParams(child).topMargin; + if (childTop > referenceViewTop + getHeight()) { + // If choosing the previous child causes the view to snap back to the referenceView + // position, then skip that and go directly to the child. This avoids the case + // where a tall card in the layout causes the view to constantly snap back to + // the top when scrolled. + return nextPagePosition - 1 == position ? nextPagePosition : nextPagePosition - 1; + } + } + // End of the list. + return nextPagePosition; + } + + /** + * In this style, the focus will scroll down to the middle of the screen and lock there so that + * moving in either direction will move the entire list by 1. + */ + private boolean onRequestChildFocusMarioStyle(RecyclerView parent, View child) { + int focusedPosition = getPosition(child); + if (focusedPosition == mLastChildPositionToRequestFocus) { + return true; + } + mLastChildPositionToRequestFocus = focusedPosition; + + int availableHeight = getAvailableHeight(); + int focusedChildTop = getDecoratedTop(child); + int focusedChildBottom = getDecoratedBottom(child); + + int childIndex = parent.indexOfChild(child); + // Iterate through children starting at the focused child to find the child above it to + // smooth scroll to such that the focused child will be as close to the middle of the screen + // as possible. + for (int i = childIndex; i >= 0; i--) { + View childAtI = getChildAt(i); + if (childAtI == null) { + Log.e(TAG, "Child is null at index " + i); + continue; + } + // We haven't found a view that is more than half of the recycler view height above it + // but we've reached the top so we can't go any further. + if (i == 0) { + parent.smoothScrollToPosition(getPosition(childAtI)); + break; + } + + // Because we want to scroll to the first view that is less than half of the screen + // away from the focused view, we "look ahead" one view. When the look ahead view + // is more than availableHeight / 2 away, the current child at i is the one we want to + // scroll to. However, sometimes, that view can be null (ie, if the view is in + // transition). In that case, just skip that view. + + View childBefore = getChildAt(i - 1); + if (childBefore == null) { + continue; + } + int distanceToChildBeforeFromTop = focusedChildTop - getDecoratedTop(childBefore); + int distanceToChildBeforeFromBottom = focusedChildBottom - getDecoratedTop(childBefore); + + if (distanceToChildBeforeFromTop > availableHeight / 2 + || distanceToChildBeforeFromBottom > availableHeight) { + parent.smoothScrollToPosition(getPosition(childAtI)); + break; + } + } + return true; + } + + /** + * We don't actually know the size of every single view, only what is currently laid out. This + * makes it difficult to do accurate scrollbar calculations. However, lists in the car often + * consist of views with identical heights. Because of that, we can use a single sample view to + * do our calculations for. The main exceptions are in the first items of a list (hero card, + * last call card, etc) so if the first view is at position 0, we pick the next one. + * + * @return The decorated measured height of the sample view plus its margins. + */ + private int getSampleViewHeight() { + if (mSampleViewHeight != -1) { + return mSampleViewHeight; + } + int sampleViewIndex = getFirstFullyVisibleChildIndex(); + View sampleView = getChildAt(sampleViewIndex); + if (getPosition(sampleView) == 0 && sampleViewIndex < getChildCount() - 1) { + sampleView = getChildAt(++sampleViewIndex); + } + RecyclerView.LayoutParams params = getParams(sampleView); + int height = getDecoratedMeasuredHeight(sampleView) + params.topMargin + + params.bottomMargin; + if (height == 0) { + // This can happen if the view isn't measured yet. + Log.w( + TAG, + "The sample view has a height of 0. Returning a dummy value for now " + + "that won't be cached."); + height = mContext.getResources().getDimensionPixelSize(R.dimen.car_sample_row_height); + } else { + mSampleViewHeight = height; + } + return height; + } + + /** @return The height of the RecyclerView excluding padding. */ + private int getAvailableHeight() { + return getHeight() - getPaddingTop() - getPaddingBottom(); + } + + /** + * @return {@link RecyclerView.LayoutParams} for the given view or null if it isn't a child of + * {@link RecyclerView}. + */ + private static RecyclerView.LayoutParams getParams(View view) { + return (RecyclerView.LayoutParams) view.getLayoutParams(); + } + + /** + * Custom {@link LinearSmoothScroller} that has: a) Custom control over the speed of scrolls. b) + * Scrolling snaps to start. All of our scrolling logic depends on that. c) Keeps track of some + * state of the current scroll so that can aid in things like the scrollbar calculations. + */ + private final class CarSmoothScroller extends LinearSmoothScroller { + /** This value (150) was hand tuned by UX for what felt right. * */ + private static final float MILLISECONDS_PER_INCH = 150f; + /** This value (0.45) was hand tuned by UX for what felt right. * */ + private static final float DECELERATION_TIME_DIVISOR = 0.45f; + + /** This value (1.8) was hand tuned by UX for what felt right. * */ + private final Interpolator mInterpolator = new DecelerateInterpolator(1.8f); + + private final int mTargetPosition; + + CarSmoothScroller(Context context, int targetPosition) { + super(context); + mTargetPosition = targetPosition; + } + + @Override + public PointF computeScrollVectorForPosition(int i) { + if (getChildCount() == 0) { + return null; + } + final int firstChildPos = getPosition(getChildAt(getFirstFullyVisibleChildIndex())); + final int direction = (mTargetPosition < firstChildPos) ? -1 : 1; + return new PointF(0, direction); + } + + @Override + protected int getVerticalSnapPreference() { + // This is key for most of the scrolling logic that guarantees that scrolling + // will settle with a view aligned to the top. + return LinearSmoothScroller.SNAP_TO_START; + } + + @Override + protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { + int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START); + if (dy == 0) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Scroll distance is 0"); + } + return; + } + + final int time = calculateTimeForDeceleration(dy); + if (time > 0) { + action.update(0, -dy, time, mInterpolator); + } + } + + @Override + protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { + return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; + } + + @Override + protected int calculateTimeForDeceleration(int dx) { + return (int) Math.ceil(calculateTimeForScrolling(dx) / DECELERATION_TIME_DIVISOR); + } + + @Override + public int getTargetPosition() { + return mTargetPosition; + } + } + + /** + * Animation that translates a view by the specified amount. Used for card flying off the screen + * effect. + */ + private static class TranslateAnimation extends Animation { + public float verticalOffset; + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + super.applyTransformation(interpolatedTime, t); + t.getMatrix().setTranslate(0, verticalOffset); + } + } +} diff --git a/android/support/car/widget/CarRecyclerView.java b/android/support/car/widget/CarRecyclerView.java new file mode 100644 index 00000000..edc32415 --- /dev/null +++ b/android/support/car/widget/CarRecyclerView.java @@ -0,0 +1,204 @@ +/* + * 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.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Custom {@link RecyclerView} that helps {@link CarLayoutManager} properly fling and paginate. + * + * <p>It also has the ability to fade children as they scroll off screen that can be set with {@link + * #setFadeLastItem(boolean)}. + */ +public class CarRecyclerView extends RecyclerView { + private static final String PARCEL_CLASS = "android.os.Parcel"; + private static final String SAVED_STATE_CLASS = + "android.support.v7.widget.RecyclerView.SavedState"; + private boolean mFadeLastItem; + private Constructor<?> mSavedStateConstructor; + /** + * If the user releases the list with a velocity of 0, {@link #fling(int, int)} will not be + * called. However, we want to make sure that the list still snaps to the next page when this + * happens. + */ + private boolean mWasFlingCalledForGesture; + + public CarRecyclerView(Context context) { + this(context, null); + } + + public CarRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CarRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setFocusableInTouchMode(false); + setFocusable(false); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state.getClass().getClassLoader() != getClass().getClassLoader()) { + if (mSavedStateConstructor == null) { + mSavedStateConstructor = getSavedStateConstructor(); + } + // Class loader mismatch, recreate from parcel. + Parcel obtain = Parcel.obtain(); + state.writeToParcel(obtain, 0); + try { + Parcelable newState = (Parcelable) mSavedStateConstructor.newInstance(obtain); + super.onRestoreInstanceState(newState); + } catch (InstantiationException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + // Fail loudy here. + throw new RuntimeException(e); + } + } else { + super.onRestoreInstanceState(state); + } + } + + @Override + public boolean fling(int velocityX, int velocityY) { + mWasFlingCalledForGesture = true; + return ((CarLayoutManager) getLayoutManager()).settleScrollForFling(this, velocityY); + } + + @Override + public boolean onTouchEvent(MotionEvent e) { + // We want the parent to handle all touch events. There's a lot going on there, + // and there is no reason to overwrite that functionality. If we do, bad things will happen. + final boolean ret = super.onTouchEvent(e); + + int action = e.getActionMasked(); + if (action == MotionEvent.ACTION_UP) { + if (!mWasFlingCalledForGesture) { + ((CarLayoutManager) getLayoutManager()).settleScrollForFling(this, 0); + } + mWasFlingCalledForGesture = false; + } + + return ret; + } + + @Override + public boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) { + if (mFadeLastItem) { + float onScreen = 1f; + if ((child.getTop() < getBottom() && child.getBottom() > getBottom())) { + onScreen = ((float) (getBottom() - child.getTop())) / (float) child.getHeight(); + } else if ((child.getTop() < getTop() && child.getBottom() > getTop())) { + onScreen = ((float) (child.getBottom() - getTop())) / (float) child.getHeight(); + } + float alpha = 1 - (1 - onScreen) * (1 - onScreen); + fadeChild(child, alpha); + } + + return super.drawChild(canvas, child, drawingTime); + } + + public void setFadeLastItem(boolean fadeLastItem) { + mFadeLastItem = fadeLastItem; + } + + /** + * Scrolls the contents of this {@link CarRecyclerView} up one page. A page is defined as the + * number of items that fit completely on the screen. + */ + public void pageUp() { + CarLayoutManager lm = (CarLayoutManager) getLayoutManager(); + int pageUpPosition = lm.getPageUpPosition(); + if (pageUpPosition == -1) { + return; + } + + smoothScrollToPosition(pageUpPosition); + } + + /** + * Scrolls the contents of this {@link CarRecyclerView} down one page. A page is defined as the + * number of items that fit completely on the screen. + */ + public void pageDown() { + CarLayoutManager lm = (CarLayoutManager) getLayoutManager(); + int pageDownPosition = lm.getPageDownPosition(); + if (pageDownPosition == -1) { + return; + } + + smoothScrollToPosition(pageDownPosition); + } + + /** Sets {@link #mSavedStateConstructor} to private SavedState constructor. */ + private Constructor<?> getSavedStateConstructor() { + Class<?> savedStateClass = null; + // Find package private subclass RecyclerView$SavedState. + for (Class<?> c : RecyclerView.class.getDeclaredClasses()) { + if (c.getCanonicalName().equals(SAVED_STATE_CLASS)) { + savedStateClass = c; + break; + } + } + if (savedStateClass == null) { + throw new RuntimeException("RecyclerView$SavedState not found!"); + } + // Find constructor that takes a {@link Parcel}. + for (Constructor<?> c : savedStateClass.getDeclaredConstructors()) { + Class<?>[] parameterTypes = c.getParameterTypes(); + if (parameterTypes.length == 1 + && parameterTypes[0].getCanonicalName().equals(PARCEL_CLASS)) { + mSavedStateConstructor = c; + mSavedStateConstructor.setAccessible(true); + break; + } + } + if (mSavedStateConstructor == null) { + throw new RuntimeException("RecyclerView$SavedState constructor not found!"); + } + return mSavedStateConstructor; + } + + /** + * Fades child by alpha. If child is a {@link ViewGroup} then it will recursively fade its + * children instead. + */ + private void fadeChild(@NonNull View child, float alpha) { + if (child instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) child; + for (int i = 0; i < vg.getChildCount(); i++) { + fadeChild(vg.getChildAt(i), alpha); + } + } else { + child.setAlpha(alpha); + } + } +} diff --git a/android/support/car/widget/ColumnCardView.java b/android/support/car/widget/ColumnCardView.java new file mode 100644 index 00000000..06f85536 --- /dev/null +++ b/android/support/car/widget/ColumnCardView.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.car.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.car.R; +import android.support.car.utils.ColumnCalculator; +import android.support.v7.widget.CardView; +import android.util.AttributeSet; +import android.util.Log; + +/** + * A {@link CardView} whose width can be specified by the number of columns that it will span. + * + * <p>The {@code ColumnCardView} works similarly to a regular {@link CardView}, except that + * its {@code layout_width} attribute is always ignored. Instead, its width is automatically + * calculated based on a specified {@code columnSpan} attribute. Alternatively, a user can call + * {@link #setColumnSpan(int)}. If no column span is given, the {@code ColumnCardView} will have + * a default span value that it uses. + * + * <pre> + * <android.support.car.widget.ColumnCardView + * android:layout_width="wrap_content" + * android:layout_height="wrap_content" + * app:columnSpan="4" /> + * </pre> + * + * @see ColumnCalculator + */ +public final class ColumnCardView extends CardView { + private static final String TAG = "ColumnCardView"; + + private ColumnCalculator mColumnCalculator; + private int mColumnSpan; + + public ColumnCardView(Context context) { + super(context); + init(context, null, 0 /* defStyleAttrs */); + } + + public ColumnCardView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0 /* defStyleAttrs */); + } + + public ColumnCardView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttrs) { + mColumnCalculator = ColumnCalculator.getInstance(context); + + int defaultColumnSpan = getResources().getInteger( + R.integer.column_card_default_column_span); + + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColumnCardView, + defStyleAttrs, 0 /* defStyleRes */); + mColumnSpan = ta.getInteger(R.styleable.ColumnCardView_columnSpan, defaultColumnSpan); + ta.recycle(); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Column span: " + mColumnSpan); + } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Override any specified width so that the width is one that is calculated based on + // column and gutter span. + int width = mColumnCalculator.getSizeForColumnSpan(mColumnSpan); + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + heightMeasureSpec); + } + + /** + * Sets the number of columns that this {@code ColumnCardView} will span. The given span is + * ignored if it is less than 0 or greater than the number of columns that fit on screen. + * + * @param columnSpan The number of columns this {@code ColumnCardView} will span across. + */ + public void setColumnSpan(int columnSpan) { + if (columnSpan <= 0 || columnSpan > mColumnCalculator.getNumOfColumns()) { + return; + } + + mColumnSpan = columnSpan; + requestLayout(); + } + + /** + * Returns the currently number of columns that this {@code ColumnCardView} spans. + * + * @return The number of columns this {@code ColumnCardView} spans across. + */ + public int getColumnSpan() { + return mColumnSpan; + } +} diff --git a/android/support/car/widget/DayNightStyle.java b/android/support/car/widget/DayNightStyle.java new file mode 100644 index 00000000..ff5a1b33 --- /dev/null +++ b/android/support/car/widget/DayNightStyle.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.car.widget; + +import android.support.annotation.IntDef; + +/** + * Specifies how the system UI should respond to day/night mode events. + * + * <p>By default, the Android Auto system UI assumes the app content background is light during the + * day and dark during the night. The system UI updates the foreground color (such as status bar + * icon colors) to be dark during day mode and light during night mode. By setting the + * DayNightStyle, the app can specify how the system should respond to a day/night mode event. For + * example, if the app has a dark content background for both day and night time, the app can tell + * the system to use {@link #FORCE_NIGHT} style so the foreground color is locked to light color for + * both cases. + * + * <p>Note: Not all system UI elements can be customized with a DayNightStyle. + */ +@IntDef({ + DayNightStyle.AUTO, + DayNightStyle.AUTO_INVERSE, + DayNightStyle.FORCE_NIGHT, + DayNightStyle.FORCE_DAY, +}) +public @interface DayNightStyle { + /** + * Sets the foreground color to be automatically changed based on day/night mode, assuming the + * app content background is light during the day and dark during the night. + * + * <p>This is the default behavior. + */ + int AUTO = 0; + + /** + * Sets the foreground color to be automatically changed based on day/night mode, assuming the + * app content background is dark during the day and light during the night. + */ + int AUTO_INVERSE = 1; + + /** + * Sets the foreground color to be locked to the night version, which assumes the app content + * background is always dark during both day and night. + */ + int FORCE_NIGHT = 2; + + /** + * Sets the foreground color to be locked to the day version, which assumes the app content + * background is always light during both day and night. + */ + int FORCE_DAY = 3; +} diff --git a/android/support/car/widget/PagedListView.java b/android/support/car/widget/PagedListView.java new file mode 100644 index 00000000..8527c659 --- /dev/null +++ b/android/support/car/widget/PagedListView.java @@ -0,0 +1,854 @@ +/* + * 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.widget; + +import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +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; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +/** + * Custom {@link android.support.v7.widget.RecyclerView} that displays a list of items that + * resembles a {@link android.widget.ListView} but also has page up and page down arrows on the + * right side. + */ +public class PagedListView extends FrameLayout { + /** Default maximum number of clicks allowed on a list */ + public static final int DEFAULT_MAX_CLICKS = 6; + + /** + * The amount of time after settling to wait before autoscrolling to the next page when the user + * holds down a pagination button. + */ + protected static final int PAGINATION_HOLD_DELAY_MS = 400; + + private static final String TAG = "PagedListView"; + private static final int INVALID_RESOURCE_ID = -1; + + protected final CarRecyclerView mRecyclerView; + 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; + protected RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter; + + /** Maximum number of pages to show. Values < 0 show all pages. */ + private int mMaxPages = -1; + + protected OnScrollListener mOnScrollListener; + + /** Number of visible rows per page */ + private int mDefaultMaxPages = DEFAULT_MAX_CLICKS; + + /** Used to check if there are more items added to the list. */ + private int mLastItemCount = 0; + + private boolean mNeedsFocus; + + /** + * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to cap the number of + * items. + * + * <p>NOTE: it is still up to the adapter to use maxItems in {@link + * android.support.v7.widget.RecyclerView.Adapter#getItemCount()}. + * + * <p>the recommended way would be with: + * + * <pre>{@code + * {@literal@}Override + * public int getItemCount() { + * return Math.min(super.getItemCount(), mMaxItems); + * } + * }</pre> + */ + public interface ItemCap { + /** + * Sets the maximum number of items available in the adapter. A value less than '0' means + * the list should not be capped. + */ + void setMaxItems(int maxItems); + } + + /** + * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to set the position + * offset for the adapter to load the data. + * + * <p>For example in the adapter, if the positionOffset is 20, then for position 0 it will show + * the item in position 20 instead, for position 1 it will show the item in position 21 instead + * and so on. + */ + // TODO(b/28003781): ItemPositionOffset and ItemCap interfaces should be merged once + // we enable AlphaJump outside drawer. + public interface ItemPositionOffset { + /** Sets the position offset for the adapter. */ + void setPositionOffset(int positionOffset); + } + + public PagedListView(Context context, AttributeSet attrs) { + this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/); + } + + public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) { + this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/); + } + + public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { + this(context, attrs, defStyleAttrs, defStyleRes, 0); + } + + public PagedListView( + Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes, int layoutId) { + super(context, attrs, defStyleAttrs, defStyleRes); + if (layoutId == 0) { + layoutId = R.layout.car_paged_recycler_view; + } + 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); + boolean fadeLastItem = a.getBoolean(R.styleable.PagedListView_fadeLastItem, false); + mRecyclerView.setFadeLastItem(fadeLastItem); + boolean offsetRows = a.getBoolean(R.styleable.PagedListView_offsetRows, false); + + mMaxPages = getDefaultMaxPages(); + + mLayoutManager = new CarLayoutManager(context); + mLayoutManager.setOffsetRows(offsetRows); + mRecyclerView.setLayoutManager(mLayoutManager); + mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener); + mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12); + mRecyclerView.setItemAnimator(new CarItemAnimator(mLayoutManager)); + + if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) { + int dividerStartMargin = a.getDimensionPixelSize( + R.styleable.PagedListView_dividerStartMargin, 0); + int dividerStartId = a.getResourceId( + R.styleable.PagedListView_alignDividerStartTo, INVALID_RESOURCE_ID); + int dividerEndId = a.getResourceId( + R.styleable.PagedListView_alignDividerEndTo, INVALID_RESOURCE_ID); + + mRecyclerView.addItemDecoration(new DividerDecoration(context, dividerStartMargin, + dividerStartId, dividerEndId)); + } + + // Set this to true so that this view consumes clicks events and views underneath + // don't receive this click event. Without this it's possible to click places in the + // view that don't capture the event, and as a result, elements visually hidden consume + // the event. + setClickable(true); + + // Set focusable false explicitly to handle the behavior change in Android O where + // clickable view becomes focusable by default. + setFocusable(false); + + mScrollBarEnabled = a.getBoolean(R.styleable.PagedListView_scrollBarEnabled, true); + mScrollBarView = (PagedScrollBarView) findViewById(R.id.paged_scroll_view); + mScrollBarView.setPaginationListener( + new PagedScrollBarView.PaginationListener() { + @Override + public void onPaginate(int direction) { + if (direction == PagedScrollBarView.PaginationListener.PAGE_UP) { + mRecyclerView.pageUp(); + if (mOnScrollListener != null) { + mOnScrollListener.onScrollUpButtonClicked(); + } + } else if (direction == PagedScrollBarView.PaginationListener.PAGE_DOWN) { + mRecyclerView.pageDown(); + if (mOnScrollListener != null) { + mOnScrollListener.onScrollDownButtonClicked(); + } + } else { + Log.e(TAG, "Unknown pagination direction (" + direction + ")"); + } + } + }); + 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); + } + + 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(); + mHandler.removeCallbacks(mUpdatePaginationRunnable); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_DOWN) { + // The user has interacted with the list using touch. All movements will now paginate + // the list. + mLayoutManager.setRowOffsetMode(CarLayoutManager.ROW_OFFSET_MODE_PAGE); + } + return super.onInterceptTouchEvent(e); + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + // The user has interacted with the list using the controller. Movements through the list + // will now be one row at a time. + mLayoutManager.setRowOffsetMode(CarLayoutManager.ROW_OFFSET_MODE_INDIVIDUAL); + } + + /** + * Returns the position of the given View in the list. + * + * @param v The View to check for. + * @return The position or -1 if the given View is {@code null} or not in the list. + */ + public int positionOf(@Nullable View v) { + if (v == null || v.getParent() != mRecyclerView) { + return -1; + } + return mLayoutManager.getPosition(v); + } + + private void scroll(int direction) { + View focusedView = mRecyclerView.getFocusedChild(); + if (focusedView != null) { + int position = mLayoutManager.getPosition(focusedView); + int newPosition = + Math.max(Math.min(position + direction, mLayoutManager.getItemCount() - 1), 0); + if (newPosition != position) { + // newPosition/position are adapter positions. + // Convert to layout position by subtracting adapter position of view at layout + // position 0. + View childAt = mRecyclerView.getChildAt( + newPosition - mLayoutManager.getPosition(mLayoutManager.getChildAt(0))); + if (childAt != null) { + childAt.requestFocus(); + } + } + } + } + + private boolean canScroll(int direction) { + View focusedView = mRecyclerView.getFocusedChild(); + if (focusedView != null) { + int position = mLayoutManager.getPosition(focusedView); + int newPosition = + Math.max(Math.min(position + direction, mLayoutManager.getItemCount() - 1), 0); + if (newPosition != position) { + return true; + } + } + return false; + } + + @NonNull + public CarRecyclerView getRecyclerView() { + return mRecyclerView; + } + + /** + * Scrolls to the given position in the PagedListView. + * + * @param position The position in the list to scroll to. + */ + public void scrollToPosition(int position) { + mLayoutManager.scrollToPosition(position); + + // Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure + // the pagination arrows actually get updated. See b/http://b/15801119 + mHandler.post(mUpdatePaginationRunnable); + } + + /** + * Sets the adapter for the list. + * + * <p>It <em>must</em> implement {@link ItemCap}, otherwise, will throw an {@link + * IllegalArgumentException}. + */ + public void setAdapter( + @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) { + if (!(adapter instanceof ItemCap)) { + throw new IllegalArgumentException("ERROR: adapter [" + + adapter.getClass().getCanonicalName() + "] MUST implement ItemCap"); + } + + mAdapter = adapter; + mRecyclerView.setAdapter(adapter); + updateMaxItems(); + } + + /** @hide */ + @RestrictTo(LIBRARY_GROUP) + @NonNull + public CarLayoutManager getLayoutManager() { + return mLayoutManager; + } + + @Nullable + @SuppressWarnings("unchecked") + public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() { + return mRecyclerView.getAdapter(); + } + + /** + * Sets the maximum number of the pages that can be shown in the PagedListView. The size of a + * page is defined as the number of items that fit completely on the screen at once. + * + * @param maxPages The maximum number of pages that fit on the screen. Should be positive. + */ + public void setMaxPages(int maxPages) { + if (maxPages < 0) { + return; + } + mMaxPages = maxPages; + updateMaxItems(); + } + + /** + * Returns the maximum number of pages allowed in the PagedListView. This number is set by + * {@link #setMaxPages(int)}. If that method has not been called, then this value should match + * the default value. + * + * @return The maximum number of pages to be shown. + */ + public int getMaxPages() { + return mMaxPages; + } + + /** + * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of + * CarLayoutManager is null or the height of the first child is 0, it will return 1. + */ + public int getRowsPerPage() { + return mRowsPerPage; + } + + /** Resets the maximum number of pages to be shown to be the default. */ + public void resetMaxPages() { + mMaxPages = getDefaultMaxPages(); + updateMaxItems(); + } + + /** + * @return The position of first visible child in the list. -1 will be returned if there is no + * child. + */ + public int getFirstFullyVisibleChildPosition() { + return mLayoutManager.getFirstFullyVisibleChildPosition(); + } + + /** + * @return The position of last visible child in the list. -1 will be returned if there is no + * child. + */ + public int getLastFullyVisibleChildPosition() { + return mLayoutManager.getLastFullyVisibleChildPosition(); + } + + /** + * Adds an {@link android.support.v7.widget.RecyclerView.ItemDecoration} to this PagedListView. + * + * @param decor The decoration to add. + * @see RecyclerView#addItemDecoration(RecyclerView.ItemDecoration) + */ + public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) { + mRecyclerView.addItemDecoration(decor); + } + + /** + * Removes the given {@link android.support.v7.widget.RecyclerView.ItemDecoration} from this + * PagedListView. + * + * <p>The decoration will function the same as the item decoration for a {@link RecyclerView}. + * + * @param decor The decoration to remove. + * @see RecyclerView#removeItemDecoration(RecyclerView.ItemDecoration) + */ + public void removeItemDecoration(@NonNull RecyclerView.ItemDecoration decor) { + mRecyclerView.removeItemDecoration(decor); + } + + /** + * Adds an {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} to this + * PagedListView. + * + * <p>The listener will function the same as the listener for a regular {@link RecyclerView}. + * + * @param touchListener The touch listener to add. + * @see RecyclerView#addOnItemTouchListener(RecyclerView.OnItemTouchListener) + */ + public void addOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) { + mRecyclerView.addOnItemTouchListener(touchListener); + } + + /** + * Removes the given {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} from + * the PagedListView. + * + * @param touchListener The touch listener to remove. + * @see RecyclerView#removeOnItemTouchListener(RecyclerView.OnItemTouchListener) + */ + public void removeOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) { + mRecyclerView.removeOnItemTouchListener(touchListener); + } + /** + * Sets how this {@link PagedListView} responds to day/night configuration changes. By + * default, the PagedListView is darker in the day and lighter at night. + * + * @param dayNightStyle A value from {@link DayNightStyle}. + * @see DayNightStyle + */ + public void setDayNightStyle(@DayNightStyle int dayNightStyle) { + // Update the scrollbar + mScrollBarView.setDayNightStyle(dayNightStyle); + + int decorCount = mRecyclerView.getItemDecorationCount(); + for (int i = 0; i < decorCount; i++) { + RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i); + if (decor instanceof DividerDecoration) { + ((DividerDecoration) decor).updateDividerColor(); + } + } + } + + /** + * Returns the {@link android.support.v7.widget.RecyclerView.ViewHolder} that corresponds to the + * last child in the PagedListView that is fully visible. + * + * @return The corresponding ViewHolder or {@code null} if none exists. + */ + @Nullable + public RecyclerView.ViewHolder getLastViewHolder() { + View lastFullyVisible = mLayoutManager.getLastFullyVisibleChild(); + if (lastFullyVisible == null) { + return null; + } + int lastFullyVisibleAdapterPosition = mLayoutManager.getPosition(lastFullyVisible); + RecyclerView.ViewHolder lastViewHolder = getRecyclerView() + .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition + 1); + // We want to get the very last ViewHolder in the list, even if it's only fully visible + // If it doesn't exist, return the last fully visible ViewHolder. + if (lastViewHolder == null) { + lastViewHolder = getRecyclerView() + .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition); + } + return lastViewHolder; + } + + /** + * Sets the {@link OnScrollListener} that will be notified of scroll events within the + * PagedListView. + * + * @param listener The scroll listener to set. + */ + public void setOnScrollListener(OnScrollListener listener) { + mOnScrollListener = listener; + mLayoutManager.setOnScrollListener(mOnScrollListener); + } + + /** Returns the page the given position is on, starting with page 0. */ + public int getPage(int position) { + if (mRowsPerPage == -1) { + return -1; + } + if (mRowsPerPage == 0) { + return 0; + } + return position / mRowsPerPage; + } + + /** + * Sets the default number of pages that this PagedListView is limited to. + * + * @param newDefault The default number of pages. Should be positive. + */ + public void setDefaultMaxPages(int newDefault) { + if (newDefault < 0) { + return; + } + mDefaultMaxPages = newDefault; + } + + /** Returns the default number of pages the list should have */ + protected int getDefaultMaxPages() { + // assume list shown in response to a click, so, reduce number of clicks by one + return mDefaultMaxPages - 1; + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + // if a late item is added to the top of the layout after the layout is stabilized, causing + // the former top item to be pushed to the 2nd page, the focus will still be on the former + // top item. Since our car layout manager tries to scroll the viewport so that the focused + // item is visible, the view port will be on the 2nd page. That means the newly added item + // will not be visible, on the first page. + + // what we want to do is: if the formerly focused item is the first one in the list, any + // item added above it will make the focus to move to the new first item. + // if the focus is not on the formerly first item, then we don't need to do anything. Let + // the layout manager do the job and scroll the viewport so the currently focused item + // is visible. + + // we need to calculate whether we want to request focus here, before the super call, + // because after the super call, the first born might be changed. + View focusedChild = mLayoutManager.getFocusedChild(); + View firstBorn = mLayoutManager.getChildAt(0); + + super.onLayout(changed, left, top, right, bottom); + + if (mAdapter != null) { + int itemCount = mAdapter.getItemCount(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, String.format( + "onLayout hasFocus: %s, mLastItemCount: %s, itemCount: %s, " + + "focusedChild: %s, firstBorn: %s, isInTouchMode: %s, " + + "mNeedsFocus: %s", + hasFocus(), + mLastItemCount, + itemCount, + focusedChild, + firstBorn, + isInTouchMode(), + mNeedsFocus)); + } + updateMaxItems(); + // This is a workaround for missing focus because isInTouchMode() is not always + // returning the right value. + // This is okay for the Engine release since focus is always showing. + // However, in Tala and Fender, we want to show focus only when the user uses + // hardware controllers, so we need to revisit this logic. b/22990605. + if (mNeedsFocus && itemCount > 0) { + if (focusedChild == null) { + requestFocus(); + } + mNeedsFocus = false; + } + if (itemCount > mLastItemCount && focusedChild == firstBorn) { + requestFocus(); + } + mLastItemCount = itemCount; + } + // We need to update the scroll buttons after layout has happened. + // Determining if a scrollbar is necessary requires looking at the layout of the child + // views. Therefore, this determination can only be done after layout has happened. + // Note: don't animate here to prevent b/26849677 + updatePaginationButtons(false /*animate*/); + } + + /** + * Returns the View at the given position within the list. + * + * @param position A position within the list. + * @return The View or {@code null} if no View exists at the given position. + */ + @Nullable + public View findViewByPosition(int position) { + return mLayoutManager.findViewByPosition(position); + } + + /** + * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is + * being called as a result of adapter changes, it should be called after the new layout has + * been calculated because the method of determining scrollbar visibility uses the current + * layout. If this is called after an adapter change but before the new layout, the visibility + * determination may not be correct. + * + * @param animate {@code true} if the scrollbar should animate to its new position. + * {@code false} if no animation is used + */ + protected void updatePaginationButtons(boolean animate) { + if (!mScrollBarEnabled) { + // Don't change the visibility of the ScrollBar unless it's enabled. + return; + } + + if ((mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) + || mLayoutManager.getItemCount() == 0) { + mScrollBarView.setVisibility(View.INVISIBLE); + } else { + mScrollBarView.setVisibility(View.VISIBLE); + } + mScrollBarView.setUpEnabled(shouldEnablePageUpButton()); + mScrollBarView.setDownEnabled(shouldEnablePageDownButton()); + + mScrollBarView.setParameters( + mRecyclerView.computeVerticalScrollRange(), + mRecyclerView.computeVerticalScrollOffset(), + mRecyclerView.computeVerticalScrollExtent(), + animate); + invalidate(); + } + + protected boolean shouldEnablePageUpButton() { + return !mLayoutManager.isAtTop(); + } + + protected boolean shouldEnablePageDownButton() { + return !mLayoutManager.isAtBottom(); + } + + @UiThread + protected void updateMaxItems() { + if (mAdapter == null) { + return; + } + + final int originalCount = mAdapter.getItemCount(); + updateRowsPerPage(); + ((ItemCap) mAdapter).setMaxItems(calculateMaxItemCount()); + final int newCount = mAdapter.getItemCount(); + if (newCount == originalCount) { + return; + } + + if (newCount < originalCount) { + mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount); + } else { + mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount); + } + } + + protected int calculateMaxItemCount() { + final View firstChild = mLayoutManager.getChildAt(0); + if (firstChild == null || firstChild.getHeight() == 0) { + return -1; + } else { + return (mMaxPages < 0) ? -1 : mRowsPerPage * mMaxPages; + } + } + + /** + * Updates the rows number per current page, which is used for calculating how many items we + * want to show. + */ + protected void updateRowsPerPage() { + final View firstChild = mLayoutManager.getChildAt(0); + if (firstChild == null || firstChild.getHeight() == 0) { + mRowsPerPage = 1; + } else { + mRowsPerPage = Math.max(1, (getHeight() - getPaddingTop()) / firstChild.getHeight()); + } + } + + private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener = + new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (mOnScrollListener != null) { + mOnScrollListener.onScrolled(recyclerView, dx, dy); + if (!mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) { + mOnScrollListener.onReachBottom(); + } else if (mLayoutManager.isAtTop() || !mLayoutManager.isAtBottom()) { + mOnScrollListener.onLeaveBottom(); + } + } + updatePaginationButtons(false); + } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(recyclerView, newState); + } + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS); + } + } + }; + + protected final Runnable mPaginationRunnable = + new Runnable() { + @Override + public void run() { + boolean upPressed = mScrollBarView.isUpPressed(); + boolean downPressed = mScrollBarView.isDownPressed(); + if (upPressed && downPressed) { + return; + } + if (upPressed) { + mRecyclerView.pageUp(); + } else if (downPressed) { + mRecyclerView.pageDown(); + } + } + }; + + private final Runnable mUpdatePaginationRunnable = + new Runnable() { + @Override + public void run() { + updatePaginationButtons(true /*animate*/); + } + }; + + /** Used to listen for {@code PagedListView} scroll events. */ + public abstract static class OnScrollListener { + /** Called when menu reaches the bottom */ + public void onReachBottom() {} + /** Called when menu leaves the bottom */ + public void onLeaveBottom() {} + /** Called when scroll up button is clicked */ + public void onScrollUpButtonClicked() {} + /** Called when scroll down button is clicked */ + public void onScrollDownButtonClicked() {} + /** Called when scrolling to the previous page via up gesture */ + public void onGestureUp() {} + /** Called when scrolling to the next page via down gesture */ + public void onGestureDown() {} + + /** + * Called when RecyclerView.OnScrollListener#onScrolled is called. See + * RecyclerView.OnScrollListener + */ + public void onScrolled(RecyclerView recyclerView, int dx, int dy) {} + + /** See RecyclerView.OnScrollListener */ + public void onScrollStateChanged(RecyclerView recyclerView, int newState) {} + + /** Called when the view scrolls up a page */ + public void onPageUp() {} + + /** Called when the view scrolls down a page */ + public void onPageDown() {} + } + + /** + * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will draw a dividing + * line between each item in the RecyclerView that it is added to. + */ + public static class DividerDecoration extends RecyclerView.ItemDecoration { + private final Context mContext; + private final Paint mPaint; + private final int mDividerHeight; + private final int mDividerStartMargin; + @IdRes private final int mDividerStartId; + @IdRes private final int mDvidierEndId; + + /** + * @param dividerStartMargin The start offset of the dividing line. This offset will be + * relative to {@code dividerStartId} if that value is given. + * @param dividerStartId A child view id whose starting edge will be used as the starting + * edge of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, the top + * container of each child view will be used. + * @param dividerEndId A child view id whose ending edge will be used as the starting edge + * of the dividing lin.e If this value is {@link #INVALID_RESOURCE_ID}, then the top + * container view of each child will be used. + */ + private DividerDecoration(Context context, int dividerStartMargin, + @IdRes int dividerStartId, @IdRes int dividerEndId) { + mContext = context; + mDividerStartMargin = dividerStartMargin; + mDividerStartId = dividerStartId; + mDvidierEndId = dividerEndId; + + Resources res = context.getResources(); + mPaint = new Paint(); + mPaint.setColor(res.getColor(R.color.car_list_divider)); + mDividerHeight = res.getDimensionPixelSize(R.dimen.car_divider_height); + } + + /** Updates the list divider color which may have changed due to a day night transition. */ + public void updateDividerColor() { + mPaint.setColor(mContext.getResources().getColor(R.color.car_list_divider)); + } + + @Override + public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { + for (int i = 0, childCount = parent.getChildCount(); i < childCount; i++) { + View container = parent.getChildAt(i); + View startChild = + mDividerStartId != INVALID_RESOURCE_ID + ? container.findViewById(mDividerStartId) + : container; + + View endChild = + mDvidierEndId != INVALID_RESOURCE_ID + ? container.findViewById(mDvidierEndId) + : container; + + if (startChild == null || endChild == null) { + continue; + } + + int left = mDividerStartMargin + startChild.getLeft(); + int right = endChild.getRight(); + int bottom = container.getBottom(); + int top = bottom - mDividerHeight; + + // Draw a divider line between each item. No need to draw the line for the last + // item. + if (i != childCount - 1) { + c.drawRect(left, top, right, bottom, mPaint); + } + } + } + } +} diff --git a/android/support/car/widget/PagedScrollBarView.java b/android/support/car/widget/PagedScrollBarView.java new file mode 100644 index 00000000..125b354c --- /dev/null +++ b/android/support/car/widget/PagedScrollBarView.java @@ -0,0 +1,253 @@ +/* + * 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.widget; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.support.car.R; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; + +/** A custom view to provide list scroll behaviour -- up/down buttons and scroll indicator. */ +public class PagedScrollBarView extends FrameLayout + implements View.OnClickListener, View.OnLongClickListener { + private static final float BUTTON_DISABLED_ALPHA = 0.2f; + + @DayNightStyle private int mDayNightStyle; + + /** Listener for when the list should paginate. */ + public interface PaginationListener { + int PAGE_UP = 0; + int PAGE_DOWN = 1; + + /** Called when the linked view should be paged in the given direction */ + void onPaginate(int direction); + } + + private final ImageView mUpButton; + private final ImageView mDownButton; + private final ImageView mScrollThumb; + /** The "filler" view between the up and down buttons */ + private final View mFiller; + + private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator(); + private final int mMinThumbLength; + private final int mMaxThumbLength; + private PaginationListener mPaginationListener; + + public PagedScrollBarView(Context context, AttributeSet attrs) { + this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/); + } + + public PagedScrollBarView(Context context, AttributeSet attrs, int defStyleAttrs) { + this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/); + } + + public PagedScrollBarView( + Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { + super(context, attrs, defStyleAttrs, defStyleRes); + + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.car_paged_scrollbar_buttons, this /* root */, + true /* attachToRoot */); + + mUpButton = (ImageView) findViewById(R.id.page_up); + mUpButton.setOnClickListener(this); + mUpButton.setOnLongClickListener(this); + mDownButton = (ImageView) findViewById(R.id.page_down); + mDownButton.setOnClickListener(this); + mDownButton.setOnLongClickListener(this); + + mScrollThumb = (ImageView) findViewById(R.id.scrollbar_thumb); + mFiller = findViewById(R.id.filler); + + mMinThumbLength = getResources().getDimensionPixelSize(R.dimen.min_thumb_height); + mMaxThumbLength = getResources().getDimensionPixelSize(R.dimen.max_thumb_height); + } + + @Override + public void onClick(View v) { + dispatchPageClick(v); + } + + @Override + public boolean onLongClick(View v) { + dispatchPageClick(v); + return true; + } + + /** + * Sets the listener that will be notified when the up and down buttons have been pressed. + * + * @param listener The listener to set. + */ + public void setPaginationListener(PaginationListener listener) { + mPaginationListener = listener; + } + + /** Returns {@code true} if the "up" button is pressed */ + public boolean isUpPressed() { + return mUpButton.isPressed(); + } + + /** Returns {@code true} if the "down" button is pressed */ + public boolean isDownPressed() { + return mDownButton.isPressed(); + } + + /** Sets the range, offset and extent of the scroll bar. See {@link View}. */ + public void setParameters(int range, int offset, int extent, boolean animate) { + // This method is where we take the computed parameters from the CarLayoutManager and + // render it within the specified constraints ({@link #mMaxThumbLength} and + // {@link #mMinThumbLength}). + final int size = mFiller.getHeight() - mFiller.getPaddingTop() - mFiller.getPaddingBottom(); + + int thumbLength = extent * size / range; + thumbLength = Math.max(Math.min(thumbLength, mMaxThumbLength), mMinThumbLength); + + int thumbOffset = size - thumbLength; + if (isDownEnabled()) { + // We need to adjust the offset so that it fits into the possible space inside the + // filler with regarding to the constraints set by mMaxThumbLength and mMinThumbLength. + thumbOffset = (size - thumbLength) * offset / range; + } + + // Sets the size of the thumb and request a redraw if needed. + final ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams(); + if (lp.height != thumbLength) { + lp.height = thumbLength; + mScrollThumb.requestLayout(); + } + + moveY(mScrollThumb, thumbOffset, animate); + } + + /** + * Sets how this {@link PagedScrollBarView} responds to day/night configuration changes. By + * default, the PagedScrollBarView is darker in the day and lighter at night. + * + * @param dayNightStyle A value from {@link DayNightStyle}. + * @see DayNightStyle + */ + public void setDayNightStyle(@DayNightStyle int dayNightStyle) { + mDayNightStyle = dayNightStyle; + reloadColors(); + } + + /** + * Sets whether or not the up button on the scroll bar is clickable. + * + * @param enabled {@code true} if the up button is enabled. + */ + public void setUpEnabled(boolean enabled) { + mUpButton.setEnabled(enabled); + mUpButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA); + } + + /** + * Sets whether or not the down button on the scroll bar is clickable. + * + * @param enabled {@code true} if the down button is enabled. + */ + public void setDownEnabled(boolean enabled) { + mDownButton.setEnabled(enabled); + mDownButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA); + } + + /** + * Returns whether or not the down button on the scroll bar is clickable. + * + * @return {@code true} if the down button is enabled. {@code false} otherwise. + */ + public boolean isDownEnabled() { + return mDownButton.isEnabled(); + } + + /** Reload the colors for the current {@link DayNightStyle}. */ + private void reloadColors() { + int tint; + int thumbBackground; + int upDownBackgroundResId; + + switch (mDayNightStyle) { + case DayNightStyle.AUTO: + tint = ContextCompat.getColor(getContext(), R.color.car_tint); + thumbBackground = ContextCompat.getColor(getContext(), + R.color.car_scrollbar_thumb); + upDownBackgroundResId = R.drawable.car_pagination_background; + break; + case DayNightStyle.AUTO_INVERSE: + tint = ContextCompat.getColor(getContext(), R.color.car_tint_inverse); + thumbBackground = ContextCompat.getColor(getContext(), + R.color.car_scrollbar_thumb_inverse); + upDownBackgroundResId = R.drawable.car_pagination_background_inverse; + break; + case DayNightStyle.FORCE_NIGHT: + tint = ContextCompat.getColor(getContext(), R.color.car_tint_light); + thumbBackground = ContextCompat.getColor(getContext(), + R.color.car_scrollbar_thumb_light); + upDownBackgroundResId = R.drawable.car_pagination_background_night; + break; + case DayNightStyle.FORCE_DAY: + tint = ContextCompat.getColor(getContext(), R.color.car_tint_dark); + thumbBackground = ContextCompat.getColor(getContext(), + R.color.car_scrollbar_thumb_dark); + upDownBackgroundResId = R.drawable.car_pagination_background_day; + break; + default: + throw new IllegalArgumentException("Unknown DayNightStyle: " + mDayNightStyle); + } + + mScrollThumb.setBackgroundColor(thumbBackground); + + mUpButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN); + mUpButton.setBackgroundResource(upDownBackgroundResId); + + mDownButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN); + mDownButton.setBackgroundResource(upDownBackgroundResId); + } + + private void dispatchPageClick(View v) { + final PaginationListener listener = mPaginationListener; + if (listener == null) { + return; + } + + int direction = v.getId() == R.id.page_up + ? PaginationListener.PAGE_UP + : PaginationListener.PAGE_DOWN; + listener.onPaginate(direction); + } + + /** Moves the given view to the specified 'y' position. */ + private void moveY(final View view, float newPosition, boolean animate) { + final int duration = animate ? 200 : 0; + view.animate() + .y(newPosition) + .setDuration(duration) + .setInterpolator(mPaginationInterpolator) + .start(); + } +} diff --git a/android/support/customtabs/CustomTabsCallback.java b/android/support/customtabs/CustomTabsCallback.java index 818118a0..f8d349a8 100644 --- a/android/support/customtabs/CustomTabsCallback.java +++ b/android/support/customtabs/CustomTabsCallback.java @@ -16,7 +16,9 @@ package android.support.customtabs; +import android.net.Uri; import android.os.Bundle; +import android.support.customtabs.CustomTabsService.Relation; /** * A callback class for custom tabs client to get messages regarding events in their custom tabs. In @@ -98,4 +100,18 @@ public class CustomTabsCallback { * @param extras Reserved for future use. */ public void onPostMessage(String message, Bundle extras) {} + + /** + * Called when a relationship validation result is available. + * + * @param relation Relation for which the result is available. Value previously passed to + * {@link CustomTabsSession#validateRelationship(int, Uri, Bundle)}. Must be one + * of the {@code CustomTabsService#RELATION_* } constants. + * @param requestedOrigin Origin requested. Value previously passed to + * {@link CustomTabsSession#validateRelationship(int, Uri, Bundle)}. + * @param result Whether the relation was validated. + * @param extras Reserved for future use. + */ + public void onRelationshipValidationResult(@Relation int relation, Uri requestedOrigin, + boolean result, Bundle extras) {} } diff --git a/android/support/customtabs/CustomTabsClient.java b/android/support/customtabs/CustomTabsClient.java index 09f31109..2e955cbe 100644 --- a/android/support/customtabs/CustomTabsClient.java +++ b/android/support/customtabs/CustomTabsClient.java @@ -31,6 +31,7 @@ import android.os.Looper; import android.os.RemoteException; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; +import android.support.customtabs.CustomTabsService.Relation; import android.text.TextUtils; import java.util.ArrayList; @@ -234,6 +235,20 @@ public class CustomTabsClient { } }); } + + @Override + public void onRelationshipValidationResult( + final @Relation int relation, final Uri requestedOrigin, final boolean result, + final @Nullable Bundle extras) throws RemoteException { + if (callback == null) return; + mHandler.post(new Runnable() { + @Override + public void run() { + callback.onRelationshipValidationResult( + relation, requestedOrigin, result, extras); + } + }); + } }; try { diff --git a/android/support/customtabs/CustomTabsService.java b/android/support/customtabs/CustomTabsService.java index 5a940cf4..aad174c0 100644 --- a/android/support/customtabs/CustomTabsService.java +++ b/android/support/customtabs/CustomTabsService.java @@ -78,6 +78,23 @@ public abstract class CustomTabsService extends Service { */ public static final int RESULT_FAILURE_MESSAGING_ERROR = -3; + @Retention(RetentionPolicy.SOURCE) + @IntDef({RELATION_USE_AS_ORIGIN, RELATION_HANDLE_ALL_URLS}) + public @interface Relation { + } + + /** + * Used for {@link CustomTabsSession#validateRelationship(int, Uri, Bundle)}. For + * App -> Web transitions, requests the app to use the declared origin to be used as origin for + * the client app in the web APIs context. + */ + public static final int RELATION_USE_AS_ORIGIN = 1; + /** + * Used for {@link CustomTabsSession#validateRelationship(int, Uri, Bundle)}. Requests the + * ability to handle all URLs from a given origin. + */ + public static final int RELATION_HANDLE_ALL_URLS = 2; + private final Map<IBinder, DeathRecipient> mDeathRecipientMap = new ArrayMap<>(); private ICustomTabsService.Stub mBinder = new ICustomTabsService.Stub() { @@ -137,6 +154,13 @@ public abstract class CustomTabsService extends Service { return CustomTabsService.this.postMessage( new CustomTabsSessionToken(callback), message, extras); } + + @Override + public boolean validateRelationship( + ICustomTabsCallback callback, @Relation int relation, Uri origin, Bundle extras) { + return CustomTabsService.this.validateRelationship( + new CustomTabsSessionToken(callback), relation, origin, extras); + } }; @Override @@ -268,4 +292,23 @@ public abstract class CustomTabsService extends Service { @Result protected abstract int postMessage( CustomTabsSessionToken sessionToken, String message, Bundle extras); + + /** + * Request to validate a relationship between the application and an origin. + * + * If this method returns true, the validation result will be provided through + * {@link CustomTabsCallback#onRelationshipValidationResult(int, Uri, boolean, Bundle)}. + * Otherwise the request didn't succeed. The client must call + * {@link CustomTabsClient#warmup(long)} before this. + * + * @param sessionToken The unique identifier for the session. Can not be null. + * @param relation Relation to check, must be one of the {@code CustomTabsService#RELATION_* } + * constants. + * @param origin Origin for the relation query. + * @param extras Reserved for future use. + * @return true if the request has been submitted successfully. + */ + protected abstract boolean validateRelationship( + CustomTabsSessionToken sessionToken, @Relation int relation, Uri origin, + Bundle extras); } diff --git a/android/support/customtabs/CustomTabsSession.java b/android/support/customtabs/CustomTabsSession.java index cad897c8..a84d63c7 100644 --- a/android/support/customtabs/CustomTabsSession.java +++ b/android/support/customtabs/CustomTabsSession.java @@ -25,6 +25,8 @@ import android.os.IBinder; import android.os.RemoteException; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.customtabs.CustomTabsService.Relation; import android.support.customtabs.CustomTabsService.Result; import android.view.View; import android.widget.RemoteViews; @@ -42,6 +44,21 @@ public final class CustomTabsSession { private final ICustomTabsCallback mCallback; private final ComponentName mComponentName; + /** + * Provides browsers a way to generate a mock {@link CustomTabsSession} for testing + * purposes. + * + * @param componentName The component the session should be created for. + * @return A mock session with no functionality. + */ + @VisibleForTesting + @NonNull + public static CustomTabsSession createMockSessionForTesting( + @NonNull ComponentName componentName) { + return new CustomTabsSession( + null, new CustomTabsSessionToken.MockCallback(), componentName); + } + /* package */ CustomTabsSession( ICustomTabsService service, ICustomTabsCallback callback, ComponentName componentName) { mService = service; @@ -185,6 +202,39 @@ public final class CustomTabsSession { } } + /** + * Requests to validate a relationship between the application and an origin. + * + * <p> + * See <a href="https://developers.google.com/digital-asset-links/v1/getting-started">here</a> + * for documentation about Digital Asset Links. This methods requests the browser to verify + * a relation with the calling application, to grant the associated rights. + * + * <p> + * If this method returns {@code true}, the validation result will be provided through + * {@link CustomTabsCallback#onRelationshipValidationResult(int, Uri, boolean, Bundle)}. + * Otherwise the request didn't succeed. The client must call + * {@link CustomTabsClient#warmup(long)} before this. + * + * @param relation Relation to check, must be one of the {@code CustomTabsService#RELATION_* } + * constants. + * @param origin Origin. + * @param extras Reserved for future use. + * @return {@code true} if the request has been submitted successfully. + */ + public boolean validateRelationship(@Relation int relation, @NonNull Uri origin, + @Nullable Bundle extras) { + if (relation < CustomTabsService.RELATION_USE_AS_ORIGIN + || relation > CustomTabsService.RELATION_HANDLE_ALL_URLS) { + return false; + } + try { + return mService.validateRelationship(mCallback, relation, origin, extras); + } catch (RemoteException e) { + return false; + } + } + /* package */ IBinder getBinder() { return mCallback.asBinder(); } diff --git a/android/support/customtabs/CustomTabsSessionToken.java b/android/support/customtabs/CustomTabsSessionToken.java index adfadd92..5a9e1b66 100644 --- a/android/support/customtabs/CustomTabsSessionToken.java +++ b/android/support/customtabs/CustomTabsSessionToken.java @@ -17,9 +17,12 @@ package android.support.customtabs; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.support.annotation.NonNull; +import android.support.customtabs.CustomTabsService.Relation; import android.support.v4.app.BundleCompat; import android.util.Log; @@ -32,6 +35,29 @@ public class CustomTabsSessionToken { private final ICustomTabsCallback mCallbackBinder; private final CustomTabsCallback mCallback; + /* package */ static class MockCallback extends ICustomTabsCallback.Stub { + @Override + public void onNavigationEvent(int navigationEvent, Bundle extras) {} + + @Override + public void extraCallback(String callbackName, Bundle args) {} + + @Override + public void onMessageChannelReady(Bundle extras) {} + + @Override + public void onPostMessage(String message, Bundle extras) {} + + @Override + public void onRelationshipValidationResult(@Relation int relation, Uri requestedOrigin, + boolean result, Bundle extras) {} + + @Override + public IBinder asBinder() { + return this; + } + } + /** * Obtain a {@link CustomTabsSessionToken} from an intent. See {@link CustomTabsIntent.Builder} * for ways to generate an intent for custom tabs. @@ -46,6 +72,17 @@ public class CustomTabsSessionToken { return new CustomTabsSessionToken(ICustomTabsCallback.Stub.asInterface(binder)); } + /** + * Provides browsers a way to generate a mock {@link CustomTabsSessionToken} for testing + * purposes. + * + * @return A mock token with no functionality. + */ + @NonNull + public static CustomTabsSessionToken createMockSessionTokenForTesting() { + return new CustomTabsSessionToken(new MockCallback()); + } + CustomTabsSessionToken(ICustomTabsCallback callbackBinder) { mCallbackBinder = callbackBinder; mCallback = new CustomTabsCallback() { @@ -85,6 +122,18 @@ public class CustomTabsSessionToken { Log.e(TAG, "RemoteException during ICustomTabsCallback transaction"); } } + + @Override + public void onRelationshipValidationResult(@Relation int relation, Uri origin, + boolean result, Bundle extras) { + try { + mCallbackBinder.onRelationshipValidationResult( + relation, origin, result, extras); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException during ICustomTabsCallback transaction"); + } + } + }; } diff --git a/android/support/customtabs/TrustedWebUtils.java b/android/support/customtabs/TrustedWebUtils.java new file mode 100644 index 00000000..e9a22332 --- /dev/null +++ b/android/support/customtabs/TrustedWebUtils.java @@ -0,0 +1,82 @@ +/* + * 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.customtabs; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.BundleCompat; + +/** + * Class for utilities and convenience calls for opening a qualifying web page as a + * Trusted Web Activity. + * + * Trusted Web Activity is a fullscreen UI with no visible browser controls that hosts web pages + * meeting certain criteria. The full list of qualifications is at the implementing browser's + * discretion, but minimum recommended set is for the web page : + * <ul> + * <li>To have declared delegate_permission/common.handle_all_urls relationship with the + * launching client application ensuring 1:1 trust between the Android native and web + * components. See https://developers.google.com/digital-asset-links/ for details.</li> + * <li>To work as a reliable, fast and engaging standalone component within the launching app's + * flow.</li> + * <li>To be accessible and operable even when offline.</li> + * </ul> + * + * Fallback behaviors may also differ with implementation. Possibilities are launching the page in + * a custom tab, or showing it in browser UI. Browsers are encouraged to use + * {@link CustomTabsCallback#onRelationshipValidationResult(int, Uri, boolean, Bundle)} + * for sending details of the verification results. + */ +public class TrustedWebUtils { + + /** + * Boolean extra that triggers a {@link CustomTabsIntent} launch to be in a fullscreen UI with + * no browser controls. + * + * @see TrustedWebUtils#launchAsTrustedWebActivity(Context, CustomTabsIntent, Uri). + */ + public static final String EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY = + "android.support.customtabs.extra.LAUNCH_AS_TRUSTED_WEB_ACTIVITY"; + + private TrustedWebUtils() {} + + /** + * Launch the given {@link CustomTabsIntent} as a Trusted Web Activity. The given + * {@link CustomTabsIntent} should have a valid {@link CustomTabsSession} associated with it + * during construction. Once the Trusted Web Activity is launched, browser side implementations + * may have their own fallback behavior (e.g. Showing the page in a custom tab UI with toolbar) + * based on qualifications listed above or more. + * + * @param context {@link Context} to use while launching the {@link CustomTabsIntent}. + * @param customTabsIntent The {@link CustomTabsIntent} to use for launching the + * Trusted Web Activity. Note that all customizations in the given + * associated with browser toolbar controls will be ignored. + * @param uri The web page to launch as Trusted Web Activity. + */ + public static void launchAsTrustedWebActivity(@NonNull Context context, + @NonNull CustomTabsIntent customTabsIntent, @NonNull Uri uri) { + if (BundleCompat.getBinder( + customTabsIntent.intent.getExtras(), CustomTabsIntent.EXTRA_SESSION) == null) { + throw new IllegalArgumentException( + "Given CustomTabsIntent should be associated with a valid CustomTabsSession"); + } + customTabsIntent.intent.putExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true); + customTabsIntent.launchUrl(context, uri); + } +} diff --git a/android/support/media/ExifInterface.java b/android/support/media/ExifInterface.java index b790cd27..72b61cb7 100644 --- a/android/support/media/ExifInterface.java +++ b/android/support/media/ExifInterface.java @@ -22,6 +22,7 @@ import android.graphics.BitmapFactory; import android.location.Location; import android.support.annotation.IntDef; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; @@ -3699,7 +3700,7 @@ public class ExifInterface { /** * Reads Exif tags from the specified image file. */ - public ExifInterface(String filename) throws IOException { + public ExifInterface(@NonNull String filename) throws IOException { if (filename == null) { throw new IllegalArgumentException("filename cannot be null"); } @@ -3720,7 +3721,7 @@ public class ExifInterface { * should close the input stream after use. This constructor is not intended to be used with * an input stream that performs any networking operations. */ - public ExifInterface(InputStream inputStream) throws IOException { + public ExifInterface(@NonNull InputStream inputStream) throws IOException { if (inputStream == null) { throw new IllegalArgumentException("inputStream cannot be null"); } @@ -3739,7 +3740,8 @@ public class ExifInterface { * * @param tag the name of the tag. */ - private ExifAttribute getExifAttribute(String tag) { + @Nullable + private ExifAttribute getExifAttribute(@NonNull String tag) { if (TAG_ISO_SPEED_RATINGS.equals(tag)) { if (DEBUG) { Log.d(TAG, "getExifAttribute: Replacing TAG_ISO_SPEED_RATINGS with " @@ -3764,7 +3766,8 @@ public class ExifInterface { * * @param tag the name of the tag. */ - public String getAttribute(String tag) { + @Nullable + public String getAttribute(@NonNull String tag) { ExifAttribute attribute = getExifAttribute(tag); if (attribute != null) { if (!sTagSetForCompatibility.contains(tag)) { @@ -3804,7 +3807,7 @@ public class ExifInterface { * @param tag the name of the tag. * @param defaultValue the value to return if the tag is not available. */ - public int getAttributeInt(String tag, int defaultValue) { + public int getAttributeInt(@NonNull String tag, int defaultValue) { ExifAttribute exifAttribute = getExifAttribute(tag); if (exifAttribute == null) { return defaultValue; @@ -3825,7 +3828,7 @@ public class ExifInterface { * @param tag the name of the tag. * @param defaultValue the value to return if the tag is not available. */ - public double getAttributeDouble(String tag, double defaultValue) { + public double getAttributeDouble(@NonNull String tag, double defaultValue) { ExifAttribute exifAttribute = getExifAttribute(tag); if (exifAttribute == null) { return defaultValue; @@ -3844,7 +3847,7 @@ public class ExifInterface { * @param tag the name of the tag. * @param value the value of the tag. */ - public void setAttribute(String tag, String value) { + public void setAttribute(@NonNull String tag, @Nullable String value) { if (TAG_ISO_SPEED_RATINGS.equals(tag)) { if (DEBUG) { Log.d(TAG, "setAttribute: Replacing TAG_ISO_SPEED_RATINGS with " @@ -4320,6 +4323,7 @@ public class ExifInterface { * The returned data can be decoded using * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)} */ + @Nullable public byte[] getThumbnail() { if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) { return getThumbnailBytes(); @@ -4331,6 +4335,7 @@ public class ExifInterface { * Returns the thumbnail bytes inside the image file, regardless of the compression type of the * thumbnail image. */ + @Nullable public byte[] getThumbnailBytes() { if (!mHasThumbnail) { return null; @@ -4379,6 +4384,7 @@ public class ExifInterface { * Creates and returns a Bitmap object of the thumbnail image based on the byte array and the * thumbnail compression value, or {@code null} if the compression type is unsupported. */ + @Nullable public Bitmap getThumbnailBitmap() { if (!mHasThumbnail) { return null; @@ -4425,6 +4431,7 @@ public class ExifInterface { * @return two-element array, the offset in the first value, and length in * the second, or {@code null} if no thumbnail was found. */ + @Nullable public long[] getThumbnailRange() { if (!mHasThumbnail) { return null; @@ -4462,6 +4469,7 @@ public class ExifInterface { * array where the first element is the latitude and the second element is the longitude. * Otherwise, it returns null. */ + @Nullable public double[] getLatLong() { String latValue = getAttribute(TAG_GPS_LATITUDE); String latRef = getAttribute(TAG_GPS_LATITUDE_REF); diff --git a/android/support/percent/PercentFrameLayout.java b/android/support/percent/PercentFrameLayout.java index b9abd39f..41908585 100644 --- a/android/support/percent/PercentFrameLayout.java +++ b/android/support/percent/PercentFrameLayout.java @@ -126,6 +126,7 @@ import android.widget.FrameLayout; * app:layout_constraintBottom_toBottomOf="@+id/bottom_guideline" /> * * </android.support.constraint.ConstraintLayout> + * </pre> */ @Deprecated public class PercentFrameLayout extends FrameLayout { diff --git a/android/support/testutils/AppCompatActivityUtils.java b/android/support/testutils/AppCompatActivityUtils.java new file mode 100644 index 00000000..49ccc1be --- /dev/null +++ b/android/support/testutils/AppCompatActivityUtils.java @@ -0,0 +1,95 @@ +/* + * 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.testutils; + +import static org.junit.Assert.assertTrue; + +import android.os.Looper; +import android.support.test.rule.ActivityTestRule; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Utility methods for testing AppCompat activities. + */ +public class AppCompatActivityUtils { + private static final Runnable DO_NOTHING = new Runnable() { + @Override + public void run() { + } + }; + + /** + * Waits for the execution of the provided activity test rule. + * + * @param rule Activity test rule to wait for. + */ + public static void waitForExecution( + final ActivityTestRule<? extends RecreatedAppCompatActivity> rule) { + // Wait for two cycles. When starting a postponed transition, it will post to + // the UI thread and then the execution will be added onto the queue after that. + // The two-cycle wait makes sure fragments have the opportunity to complete both + // before returning. + try { + rule.runOnUiThread(DO_NOTHING); + rule.runOnUiThread(DO_NOTHING); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + private static void runOnUiThreadRethrow( + ActivityTestRule<? extends RecreatedAppCompatActivity> rule, Runnable r) { + if (Looper.getMainLooper() == Looper.myLooper()) { + r.run(); + } else { + try { + rule.runOnUiThread(r); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + } + + /** + * Restarts the RecreatedAppCompatActivity and waits for the new activity to be resumed. + * + * @return The newly-restarted RecreatedAppCompatActivity + */ + public static <T extends RecreatedAppCompatActivity> T recreateActivity( + ActivityTestRule<? extends RecreatedAppCompatActivity> rule, final T activity) + throws InterruptedException { + // Now switch the orientation + RecreatedAppCompatActivity.sResumed = new CountDownLatch(1); + RecreatedAppCompatActivity.sDestroyed = new CountDownLatch(1); + + runOnUiThreadRethrow(rule, new Runnable() { + @Override + public void run() { + activity.recreate(); + } + }); + assertTrue(RecreatedAppCompatActivity.sResumed.await(1, TimeUnit.SECONDS)); + assertTrue(RecreatedAppCompatActivity.sDestroyed.await(1, TimeUnit.SECONDS)); + T newActivity = (T) RecreatedAppCompatActivity.sActivity; + + waitForExecution(rule); + + RecreatedAppCompatActivity.clearState(); + return newActivity; + } +} diff --git a/android/support/testutils/FragmentActivityUtils.java b/android/support/testutils/FragmentActivityUtils.java new file mode 100644 index 00000000..7d12deb8 --- /dev/null +++ b/android/support/testutils/FragmentActivityUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.testutils; + +import static org.junit.Assert.assertTrue; + +import android.app.Activity; +import android.os.Looper; +import android.support.test.rule.ActivityTestRule; +import android.support.v4.app.FragmentActivity; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Utility methods for testing fragment activities. + */ +public class FragmentActivityUtils { + private static final Runnable DO_NOTHING = new Runnable() { + @Override + public void run() { + } + }; + + private static void waitForExecution(final ActivityTestRule<? extends FragmentActivity> rule) { + // Wait for two cycles. When starting a postponed transition, it will post to + // the UI thread and then the execution will be added onto the queue after that. + // The two-cycle wait makes sure fragments have the opportunity to complete both + // before returning. + try { + rule.runOnUiThread(DO_NOTHING); + rule.runOnUiThread(DO_NOTHING); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + private static void runOnUiThreadRethrow(ActivityTestRule<? extends Activity> rule, + Runnable r) { + if (Looper.getMainLooper() == Looper.myLooper()) { + r.run(); + } else { + try { + rule.runOnUiThread(r); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + } + + /** + * Restarts the RecreatedActivity and waits for the new activity to be resumed. + * + * @return The newly-restarted Activity + */ + public static <T extends RecreatedActivity> T recreateActivity( + ActivityTestRule<? extends RecreatedActivity> rule, final T activity) + throws InterruptedException { + // Now switch the orientation + RecreatedActivity.sResumed = new CountDownLatch(1); + RecreatedActivity.sDestroyed = new CountDownLatch(1); + + runOnUiThreadRethrow(rule, new Runnable() { + @Override + public void run() { + activity.recreate(); + } + }); + assertTrue(RecreatedActivity.sResumed.await(1, TimeUnit.SECONDS)); + assertTrue(RecreatedActivity.sDestroyed.await(1, TimeUnit.SECONDS)); + T newActivity = (T) RecreatedActivity.sActivity; + + waitForExecution(rule); + + RecreatedActivity.clearState(); + return newActivity; + } +} diff --git a/android/support/testutils/RecreatedActivity.java b/android/support/testutils/RecreatedActivity.java new file mode 100644 index 00000000..aaea3a9f --- /dev/null +++ b/android/support/testutils/RecreatedActivity.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.support.testutils; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.test.rule.ActivityTestRule; +import android.support.v4.app.FragmentActivity; + +import java.util.concurrent.CountDownLatch; + +/** + * Extension of {@link FragmentActivity} that keeps track of when it is recreated. + * In order to use this class, have your activity extend it and call + * {@link FragmentActivityUtils#recreateActivity(ActivityTestRule, RecreatedActivity)} API. + */ +public class RecreatedActivity extends FragmentActivity { + // These must be cleared after each test using clearState() + public static RecreatedActivity sActivity; + public static CountDownLatch sResumed; + public static CountDownLatch sDestroyed; + + static void clearState() { + sActivity = null; + sResumed = null; + sDestroyed = null; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sActivity = this; + } + + @Override + protected void onResume() { + super.onResume(); + if (sResumed != null) { + sResumed.countDown(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (sDestroyed != null) { + sDestroyed.countDown(); + } + } +} diff --git a/android/support/testutils/RecreatedAppCompatActivity.java b/android/support/testutils/RecreatedAppCompatActivity.java new file mode 100644 index 00000000..d5645a30 --- /dev/null +++ b/android/support/testutils/RecreatedAppCompatActivity.java @@ -0,0 +1,65 @@ +/* + * 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.testutils; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.test.rule.ActivityTestRule; +import android.support.v7.app.AppCompatActivity; + +import java.util.concurrent.CountDownLatch; + +/** + * Extension of {@link AppCompatActivity} that keeps track of when it is recreated. + * In order to use this class, have your activity extend it and call + * {@link AppCompatActivityUtils#recreateActivity(ActivityTestRule, RecreatedAppCompatActivity)} + * API. + */ +public class RecreatedAppCompatActivity extends AppCompatActivity { + // These must be cleared after each test using clearState() + public static RecreatedAppCompatActivity sActivity; + public static CountDownLatch sResumed; + public static CountDownLatch sDestroyed; + + static void clearState() { + sActivity = null; + sResumed = null; + sDestroyed = null; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sActivity = this; + } + + @Override + protected void onResume() { + super.onResume(); + if (sResumed != null) { + sResumed.countDown(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (sDestroyed != null) { + sDestroyed.countDown(); + } + } +} diff --git a/android/support/text/emoji/widget/EmojiEditTextHelper.java b/android/support/text/emoji/widget/EmojiEditTextHelper.java index edc511f5..a999e342 100644 --- a/android/support/text/emoji/widget/EmojiEditTextHelper.java +++ b/android/support/text/emoji/widget/EmojiEditTextHelper.java @@ -20,6 +20,7 @@ import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.os.Build; import android.support.annotation.IntRange; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.text.emoji.EmojiCompat; @@ -130,7 +131,8 @@ public final class EmojiEditTextHelper { /** * Updates the InputConnection with emoji support. Should be called from {@link * TextView#onCreateInputConnection(EditorInfo)}. When used on devices running API 18 or below, - * this method returns {@code inputConnection} that is given as a parameter. + * this method returns {@code inputConnection} that is given as a parameter. If + * {@code inputConnection} is {@code null}, returns {@code null}. * * @param inputConnection InputConnection instance created by TextView * @param outAttrs EditorInfo passed into @@ -138,10 +140,10 @@ public final class EmojiEditTextHelper { * * @return a new InputConnection instance that wraps {@code inputConnection} */ - @NonNull - public InputConnection onCreateInputConnection(@NonNull final InputConnection inputConnection, + @Nullable + public InputConnection onCreateInputConnection(@Nullable final InputConnection inputConnection, @NonNull final EditorInfo outAttrs) { - Preconditions.checkNotNull(inputConnection, "inputConnection cannot be null"); + if (inputConnection == null) return null; return mHelper.onCreateInputConnection(inputConnection, outAttrs); } diff --git a/android/support/v13/app/FragmentCompat.java b/android/support/v13/app/FragmentCompat.java index 4f210525..31c2343e 100644 --- a/android/support/v13/app/FragmentCompat.java +++ b/android/support/v13/app/FragmentCompat.java @@ -24,6 +24,7 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; +import android.support.annotation.RestrictTo; import java.util.Arrays; @@ -37,6 +38,38 @@ public class FragmentCompat { boolean shouldShowRequestPermissionRationale(Fragment fragment, String permission); } + /** + * Customizable delegate that allows delegating permission related compatibility methods + * to a custom implementation. + * + * <p> + * To delegate fragment compatibility methods to a custom class, implement this interface, + * and call {@code FragmentCompat.setPermissionCompatDelegate(delegate);}. All future calls + * to the compatibility methods in this class will first check whether the delegate can + * handle the method call, and invoke the corresponding method if it can. + * </p> + */ + public interface PermissionCompatDelegate { + + /** + * Determines whether the delegate should handle + * {@link FragmentCompat#requestPermissions(Fragment, String[], int)}, and request + * permissions if applicable. If this method returns true, it means that permission + * request is successfully handled by the delegate, and platform should not perform any + * further requests for permission. + * + * @param fragment The target fragment. + * @param permissions The requested permissions. + * @param requestCode Application specific request code to match with a result + * reported to {@link OnRequestPermissionsResultCallback#onRequestPermissionsResult( + * int, String[], int[])}. + * + * @return Whether the delegate has handled the permission request. + * @see FragmentCompat#requestPermissions(Fragment, String[], int) + */ + boolean requestPermissions(Fragment fragment, String[] permissions, int requestCode); + } + static class FragmentCompatBaseImpl implements FragmentCompatImpl { @Override public void setUserVisibleHint(Fragment f, boolean deferStart) { @@ -117,6 +150,26 @@ public class FragmentCompat { } } + private static PermissionCompatDelegate sDelegate; + + /** + * Sets the permission delegate for {@code FragmentCompat}. Replaces the previously set + * delegate. + * + * @param delegate The delegate to be set. {@code null} to clear the set delegate. + */ + public static void setPermissionCompatDelegate(PermissionCompatDelegate delegate) { + sDelegate = delegate; + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static PermissionCompatDelegate getPermissionCompatDelegate() { + return sDelegate; + } + /** * This interface is the contract for receiving the results for permission requests. */ @@ -212,6 +265,11 @@ public class FragmentCompat { */ public static void requestPermissions(@NonNull Fragment fragment, @NonNull String[] permissions, int requestCode) { + if (sDelegate != null && sDelegate.requestPermissions(fragment, permissions, requestCode)) { + // Delegate has handled the request. + return; + } + IMPL.requestPermissions(fragment, permissions, requestCode); } diff --git a/android/support/v13/app/FragmentPagerAdapter.java b/android/support/v13/app/FragmentPagerAdapter.java index 082f883f..e0b788ab 100644 --- a/android/support/v13/app/FragmentPagerAdapter.java +++ b/android/support/v13/app/FragmentPagerAdapter.java @@ -48,18 +48,18 @@ import android.view.ViewGroup; * <p>Here is an example implementation of a pager containing fragments of * lists: * - * {@sample frameworks/support/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentPagerSupport.java + * {@sample frameworks/support/samples/Support13Demos/src/main/java/com/example/android/supportv13/app/FragmentPagerSupport.java * complete} * * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is: * - * {@sample frameworks/support/samples/Support13Demos/res/layout/fragment_pager.xml + * {@sample frameworks/support/samples/Support13Demos/src/main/res/layout/fragment_pager.xml * complete} * * <p>The <code>R.layout.fragment_pager_list</code> resource containing each * individual fragment's layout is: * - * {@sample frameworks/support/samples/Support13Demos/res/layout/fragment_pager_list.xml + * {@sample frameworks/support/samples/Support13Demos/src/main/res/layout/fragment_pager_list.xml * complete} */ public abstract class FragmentPagerAdapter extends PagerAdapter { diff --git a/android/support/v13/app/FragmentStatePagerAdapter.java b/android/support/v13/app/FragmentStatePagerAdapter.java index 8907fec5..45a6bf53 100644 --- a/android/support/v13/app/FragmentStatePagerAdapter.java +++ b/android/support/v13/app/FragmentStatePagerAdapter.java @@ -51,18 +51,18 @@ import java.util.ArrayList; * <p>Here is an example implementation of a pager containing fragments of * lists: * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentStatePagerSupport.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentStatePagerSupport.java * complete} * * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is: * - * {@sample frameworks/support/samples/Support4Demos/res/layout/fragment_pager.xml + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager.xml * complete} * * <p>The <code>R.layout.fragment_pager_list</code> resource containing each * individual fragment's layout is: * - * {@sample frameworks/support/samples/Support4Demos/res/layout/fragment_pager_list.xml + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager_list.xml * complete} */ public abstract class FragmentStatePagerAdapter extends PagerAdapter { diff --git a/android/support/v14/preference/PreferenceFragment.java b/android/support/v14/preference/PreferenceFragment.java index d1d9987a..24210505 100644 --- a/android/support/v14/preference/PreferenceFragment.java +++ b/android/support/v14/preference/PreferenceFragment.java @@ -103,13 +103,13 @@ import android.view.ViewGroup; * <p>The following sample code shows a simple preference fragment that is * populated from a resource. The resource it loads is:</p> * - * {@sample frameworks/support/samples/SupportPreferenceDemos/res/xml/preferences.xml preferences} + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml preferences} * * <p>The fragment implementation itself simply populates the preferences * when created. Note that the preferences framework takes care of loading * the current values out of the app preferences and writing them when changed:</p> * - * {@sample frameworks/support/samples/SupportPreferenceDemos/src/com/example/android/supportpreference/FragmentSupportPreferences.java + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferences.java * support_fragment} * * @see Preference diff --git a/android/support/v17/leanback/app/BaseFragment.java b/android/support/v17/leanback/app/BaseFragment.java index 7686c5c8..bdb213f2 100644 --- a/android/support/v17/leanback/app/BaseFragment.java +++ b/android/support/v17/leanback/app/BaseFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from BaseSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -15,6 +18,8 @@ package android.support.v17.leanback.app; import android.annotation.SuppressLint; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.transition.TransitionHelper; import android.support.v17.leanback.transition.TransitionListener; import android.support.v17.leanback.util.StateMachine; @@ -177,7 +182,7 @@ public class BaseFragment extends BrandedFragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mStateMachine.fireEvent(EVT_ON_CREATEVIEW); } @@ -267,6 +272,10 @@ public class BaseFragment extends BrandedFragment { void onExecuteEntranceTransition() { // wait till views get their initial position before start transition final View view = getView(); + if (view == null) { + // fragment view destroyed, transition not needed + return; + } view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { diff --git a/android/support/v17/leanback/app/BaseRowFragment.java b/android/support/v17/leanback/app/BaseRowFragment.java index 5a83b478..2d79f3e1 100644 --- a/android/support/v17/leanback/app/BaseRowFragment.java +++ b/android/support/v17/leanback/app/BaseRowFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from BaseRowSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -13,8 +16,9 @@ */ package android.support.v17.leanback.app; -import android.app.Fragment; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.widget.ItemBridgeAdapter; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ObjectAdapter; @@ -22,6 +26,7 @@ import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.VerticalGridView; +import android.app.Fragment; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; @@ -75,7 +80,7 @@ abstract class BaseRowFragment extends Fragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1); } diff --git a/android/support/v17/leanback/app/BaseRowSupportFragment.java b/android/support/v17/leanback/app/BaseRowSupportFragment.java index bf49295c..dba78daf 100644 --- a/android/support/v17/leanback/app/BaseRowSupportFragment.java +++ b/android/support/v17/leanback/app/BaseRowSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from BaseRowFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * @@ -16,8 +13,9 @@ */ package android.support.v17.leanback.app; -import android.support.v4.app.Fragment; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.widget.ItemBridgeAdapter; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ObjectAdapter; @@ -25,6 +23,7 @@ import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.VerticalGridView; +import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; @@ -78,7 +77,7 @@ abstract class BaseRowSupportFragment extends Fragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1); } diff --git a/android/support/v17/leanback/app/BaseSupportFragment.java b/android/support/v17/leanback/app/BaseSupportFragment.java index 213ed834..d89cf39f 100644 --- a/android/support/v17/leanback/app/BaseSupportFragment.java +++ b/android/support/v17/leanback/app/BaseSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from BaseFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * @@ -18,6 +15,8 @@ package android.support.v17.leanback.app; import android.annotation.SuppressLint; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.transition.TransitionHelper; import android.support.v17.leanback.transition.TransitionListener; import android.support.v17.leanback.util.StateMachine; @@ -180,7 +179,7 @@ public class BaseSupportFragment extends BrandedSupportFragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mStateMachine.fireEvent(EVT_ON_CREATEVIEW); } @@ -270,6 +269,10 @@ public class BaseSupportFragment extends BrandedSupportFragment { void onExecuteEntranceTransition() { // wait till views get their initial position before start transition final View view = getView(); + if (view == null) { + // fragment view destroyed, transition not needed + return; + } view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { diff --git a/android/support/v17/leanback/app/BrandedFragment.java b/android/support/v17/leanback/app/BrandedFragment.java index 35350e41..1f6ad299 100644 --- a/android/support/v17/leanback/app/BrandedFragment.java +++ b/android/support/v17/leanback/app/BrandedFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from BrandedSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -13,13 +16,15 @@ */ package android.support.v17.leanback.app; -import android.app.Fragment; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.SearchOrbView; import android.support.v17.leanback.widget.TitleHelper; import android.support.v17.leanback.widget.TitleViewAdapter; +import android.app.Fragment; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; @@ -143,7 +148,7 @@ public class BrandedFragment extends Fragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (savedInstanceState != null) { mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW); diff --git a/android/support/v17/leanback/app/BrandedSupportFragment.java b/android/support/v17/leanback/app/BrandedSupportFragment.java index 9c42780a..306e1f11 100644 --- a/android/support/v17/leanback/app/BrandedSupportFragment.java +++ b/android/support/v17/leanback/app/BrandedSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from BrandedFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * @@ -16,13 +13,15 @@ */ package android.support.v17.leanback.app; -import android.support.v4.app.Fragment; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.SearchOrbView; import android.support.v17.leanback.widget.TitleHelper; import android.support.v17.leanback.widget.TitleViewAdapter; +import android.support.v4.app.Fragment; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; @@ -146,7 +145,7 @@ public class BrandedSupportFragment extends Fragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (savedInstanceState != null) { mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW); diff --git a/android/support/v17/leanback/app/BrowseFragment.java b/android/support/v17/leanback/app/BrowseFragment.java index 8edaab67..f3773895 100644 --- a/android/support/v17/leanback/app/BrowseFragment.java +++ b/android/support/v17/leanback/app/BrowseFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from BrowseSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -15,10 +18,6 @@ package android.support.v17.leanback.app; import static android.support.v7.widget.RecyclerView.NO_POSITION; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentManager.BackStackEntry; -import android.app.FragmentTransaction; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; @@ -45,6 +44,10 @@ import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.ScaleFrameLayout; import android.support.v17.leanback.widget.TitleViewAdapter; import android.support.v17.leanback.widget.VerticalGridView; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentManager.BackStackEntry; +import android.app.FragmentTransaction; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -1110,7 +1113,7 @@ public class BrowseFragment extends BaseFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final Context context = FragmentUtil.getContext(this); + final Context context = FragmentUtil.getContext(BrowseFragment.this); TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme); mContainerListMarginStart = (int) ta.getDimension( R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources() @@ -1278,7 +1281,7 @@ public class BrowseFragment extends BaseFragment { } void createHeadersTransition() { - mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(this), + mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this), mShowingHeaders ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); @@ -1710,7 +1713,7 @@ public class BrowseFragment extends BaseFragment { @Override protected Object createEntranceTransition() { - return TransitionHelper.loadTransition(FragmentUtil.getContext(this), + return TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this), R.transition.lb_browse_entrance_transition); } diff --git a/android/support/v17/leanback/app/BrowseSupportFragment.java b/android/support/v17/leanback/app/BrowseSupportFragment.java index 2665b2a8..03b3c8a6 100644 --- a/android/support/v17/leanback/app/BrowseSupportFragment.java +++ b/android/support/v17/leanback/app/BrowseSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from BrowseFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * @@ -18,10 +15,6 @@ package android.support.v17.leanback.app; import static android.support.v7.widget.RecyclerView.NO_POSITION; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentManager.BackStackEntry; -import android.support.v4.app.FragmentTransaction; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; @@ -48,6 +41,10 @@ import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.ScaleFrameLayout; import android.support.v17.leanback.widget.TitleViewAdapter; import android.support.v17.leanback.widget.VerticalGridView; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentManager.BackStackEntry; +import android.support.v4.app.FragmentTransaction; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.util.Log; diff --git a/android/support/v17/leanback/app/DetailsFragment.java b/android/support/v17/leanback/app/DetailsFragment.java index 2c4e24ac..36559637 100644 --- a/android/support/v17/leanback/app/DetailsFragment.java +++ b/android/support/v17/leanback/app/DetailsFragment.java @@ -1,3 +1,9 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from DetailsSupportFragment.java. DO NOT MODIFY. */ + +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from DetailsFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -771,7 +777,7 @@ public class DetailsFragment extends BaseFragment { @Override protected Object createEntranceTransition() { - return TransitionHelper.loadTransition(FragmentUtil.getContext(this), + return TransitionHelper.loadTransition(FragmentUtil.getContext(DetailsFragment.this), R.transition.lb_details_enter_transition); } diff --git a/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java index 05dfb3a2..223b8ef2 100644 --- a/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java +++ b/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from {}DetailsSupportFragmentBackgroundController.java. DO NOT MODIFY. */ + /* * Copyright (C) 2017 The Android Open Source Project * @@ -16,7 +19,6 @@ package android.support.v17.leanback.app; import android.animation.PropertyValuesHolder; -import android.app.Fragment; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; @@ -30,6 +32,7 @@ import android.support.v17.leanback.media.PlaybackGlue; import android.support.v17.leanback.media.PlaybackGlueHost; import android.support.v17.leanback.widget.DetailsParallaxDrawable; import android.support.v17.leanback.widget.ParallaxTarget; +import android.app.Fragment; /** * Controller for DetailsFragment parallax background and embedded video play. diff --git a/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java index 4a0be6ec..aac74ba0 100644 --- a/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java +++ b/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from VideoDetailsFragmentBackgroundController.java. DO NOT MODIFY. */ - /* * Copyright (C) 2017 The Android Open Source Project * @@ -19,7 +16,6 @@ package android.support.v17.leanback.app; import android.animation.PropertyValuesHolder; -import android.support.v4.app.Fragment; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; @@ -33,6 +29,7 @@ import android.support.v17.leanback.media.PlaybackGlue; import android.support.v17.leanback.media.PlaybackGlueHost; import android.support.v17.leanback.widget.DetailsParallaxDrawable; import android.support.v17.leanback.widget.ParallaxTarget; +import android.support.v4.app.Fragment; /** * Controller for DetailsSupportFragment parallax background and embedded video play. diff --git a/android/support/v17/leanback/app/ErrorFragment.java b/android/support/v17/leanback/app/ErrorFragment.java index c35fcdcc..2896d0f4 100644 --- a/android/support/v17/leanback/app/ErrorFragment.java +++ b/android/support/v17/leanback/app/ErrorFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from ErrorSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * diff --git a/android/support/v17/leanback/app/ErrorSupportFragment.java b/android/support/v17/leanback/app/ErrorSupportFragment.java index 179e2e97..55e7d929 100644 --- a/android/support/v17/leanback/app/ErrorSupportFragment.java +++ b/android/support/v17/leanback/app/ErrorSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from ErrorFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * diff --git a/android/support/v17/leanback/app/GuidedStepFragment.java b/android/support/v17/leanback/app/GuidedStepFragment.java index a01cf263..2b7f2d0d 100644 --- a/android/support/v17/leanback/app/GuidedStepFragment.java +++ b/android/support/v17/leanback/app/GuidedStepFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from GuidedStepSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2015 The Android Open Source Project * @@ -17,11 +20,6 @@ import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.animation.Animator; import android.animation.AnimatorSet; -import android.app.Activity; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentManager.BackStackEntry; -import android.app.FragmentTransaction; import android.content.Context; import android.os.Build; import android.os.Bundle; @@ -37,6 +35,11 @@ import android.support.v17.leanback.widget.GuidedActionAdapterGroup; import android.support.v17.leanback.widget.GuidedActionsStylist; import android.support.v17.leanback.widget.NonOverlappingLinearLayout; import android.support.v4.app.ActivityCompat; +import android.app.Fragment; +import android.app.Activity; +import android.app.FragmentManager; +import android.app.FragmentManager.BackStackEntry; +import android.app.FragmentTransaction; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.util.TypedValue; @@ -1131,7 +1134,7 @@ public class GuidedStepFragment extends Fragment implements GuidedActionAdapter. } else { // when there are two actions panel, we need adjust the weight of action to // guidedActionContentWidthWeightTwoPanels. - Context ctx = mThemeWrapper != null ? mThemeWrapper : FragmentUtil.getContext(this); + Context ctx = mThemeWrapper != null ? mThemeWrapper : FragmentUtil.getContext(GuidedStepFragment.this); TypedValue typedValue = new TypedValue(); if (ctx.getTheme().resolveAttribute(R.attr.guidedActionContentWidthWeightTwoPanels, typedValue, true)) { @@ -1338,7 +1341,7 @@ public class GuidedStepFragment extends Fragment implements GuidedActionAdapter. private void resolveTheme() { // Look up the guidedStepTheme in the currently specified theme. If it exists, // replace the theme with its value. - Context context = FragmentUtil.getContext(this); + Context context = FragmentUtil.getContext(GuidedStepFragment.this); int theme = onProvideTheme(); if (theme == -1 && !isGuidedStepTheme(context)) { // Look up the guidedStepTheme in the activity's currently specified theme. If it diff --git a/android/support/v17/leanback/app/GuidedStepSupportFragment.java b/android/support/v17/leanback/app/GuidedStepSupportFragment.java index ed345482..aeb2d334 100644 --- a/android/support/v17/leanback/app/GuidedStepSupportFragment.java +++ b/android/support/v17/leanback/app/GuidedStepSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from GuidedStepFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2015 The Android Open Source Project * @@ -20,11 +17,6 @@ import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.animation.Animator; import android.animation.AnimatorSet; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentManager.BackStackEntry; -import android.support.v4.app.FragmentTransaction; import android.content.Context; import android.os.Build; import android.os.Bundle; @@ -40,6 +32,11 @@ import android.support.v17.leanback.widget.GuidedActionAdapterGroup; import android.support.v17.leanback.widget.GuidedActionsStylist; import android.support.v17.leanback.widget.NonOverlappingLinearLayout; import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentManager.BackStackEntry; +import android.support.v4.app.FragmentTransaction; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.util.TypedValue; diff --git a/android/support/v17/leanback/app/HeadersFragment.java b/android/support/v17/leanback/app/HeadersFragment.java index 724fa411..dd037d2f 100644 --- a/android/support/v17/leanback/app/HeadersFragment.java +++ b/android/support/v17/leanback/app/HeadersFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from HeadersSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -20,6 +23,8 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.DividerPresenter; @@ -160,7 +165,7 @@ public class HeadersFragment extends BaseRowFragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); final VerticalGridView listView = getVerticalGridView(); if (listView == null) { diff --git a/android/support/v17/leanback/app/HeadersSupportFragment.java b/android/support/v17/leanback/app/HeadersSupportFragment.java index be867cb9..56c85af6 100644 --- a/android/support/v17/leanback/app/HeadersSupportFragment.java +++ b/android/support/v17/leanback/app/HeadersSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from HeadersFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * @@ -23,6 +20,8 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.DividerPresenter; @@ -163,7 +162,7 @@ public class HeadersSupportFragment extends BaseRowSupportFragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); final VerticalGridView listView = getVerticalGridView(); if (listView == null) { diff --git a/android/support/v17/leanback/app/MediaControllerGlue.java b/android/support/v17/leanback/app/MediaControllerGlue.java deleted file mode 100644 index 7949bfb4..00000000 --- a/android/support/v17/leanback/app/MediaControllerGlue.java +++ /dev/null @@ -1,236 +0,0 @@ -package android.support.v17.leanback.app; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.session.MediaControllerCompat; -import android.support.v4.media.session.PlaybackStateCompat; -import android.util.Log; - -/** - * A helper class for implementing a glue layer between a - * {@link PlaybackOverlayFragment} and a - * {@link android.support.v4.media.session.MediaControllerCompat}. - * @deprecated Use {@link android.support.v17.leanback.media.MediaControllerGlue}. - */ -@Deprecated -public abstract class MediaControllerGlue extends PlaybackControlGlue { - static final String TAG = "MediaControllerGlue"; - static final boolean DEBUG = false; - - MediaControllerCompat mMediaController; - - private final MediaControllerCompat.Callback mCallback = new MediaControllerCompat.Callback() { - @Override - public void onMetadataChanged(MediaMetadataCompat metadata) { - if (DEBUG) Log.v(TAG, "onMetadataChanged"); - MediaControllerGlue.this.onMetadataChanged(); - } - @Override - public void onPlaybackStateChanged(PlaybackStateCompat state) { - if (DEBUG) Log.v(TAG, "onPlaybackStateChanged"); - onStateChanged(); - } - @Override - public void onSessionDestroyed() { - if (DEBUG) Log.v(TAG, "onSessionDestroyed"); - mMediaController = null; - } - @Override - public void onSessionEvent(String event, Bundle extras) { - if (DEBUG) Log.v(TAG, "onSessionEvent"); - } - }; - - /** - * Constructor for the glue. - * - * <p>The {@link PlaybackOverlayFragment} must be passed in. - * A {@link android.support.v17.leanback.widget.OnItemViewClickedListener} and - * {@link android.support.v17.leanback.app.PlaybackControlGlue.InputEventHandler} - * will be set on the fragment. - * </p> - * - * @param context - * @param fragment - * @param seekSpeeds Array of seek speeds for fast forward and rewind. - */ - public MediaControllerGlue(Context context, - PlaybackOverlayFragment fragment, - int[] seekSpeeds) { - super(context, fragment, seekSpeeds); - } - - /** - * Constructor for the glue. - * - * <p>The {@link PlaybackOverlayFragment} must be passed in. - * A {@link android.support.v17.leanback.widget.OnItemViewClickedListener} and - * {@link android.support.v17.leanback.app.PlaybackControlGlue.InputEventHandler} - * will be set on the fragment. - * </p> - * - * @param context - * @param fragment - * @param fastForwardSpeeds Array of seek speeds for fast forward. - * @param rewindSpeeds Array of seek speeds for rewind. - */ - public MediaControllerGlue(Context context, - PlaybackOverlayFragment fragment, - int[] fastForwardSpeeds, - int[] rewindSpeeds) { - super(context, fragment, fastForwardSpeeds, rewindSpeeds); - } - - /** - * Attaches to the given media controller. - */ - public void attachToMediaController(MediaControllerCompat mediaController) { - if (mediaController != mMediaController) { - if (DEBUG) Log.v(TAG, "New media controller " + mediaController); - detach(); - mMediaController = mediaController; - if (mMediaController != null) { - mMediaController.registerCallback(mCallback); - } - onMetadataChanged(); - onStateChanged(); - } - } - - /** - * Detaches from the media controller. Must be called when the object is no longer - * needed. - */ - public void detach() { - if (mMediaController != null) { - mMediaController.unregisterCallback(mCallback); - } - mMediaController = null; - } - - /** - * Returns the media controller currently attached. - */ - public final MediaControllerCompat getMediaController() { - return mMediaController; - } - - @Override - public boolean hasValidMedia() { - return mMediaController != null && mMediaController.getMetadata() != null; - } - - @Override - public boolean isMediaPlaying() { - return mMediaController.getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING; - } - - @Override - public int getCurrentSpeedId() { - int speed = (int) mMediaController.getPlaybackState().getPlaybackSpeed(); - if (speed == 0) { - return PLAYBACK_SPEED_PAUSED; - } else if (speed == 1) { - return PLAYBACK_SPEED_NORMAL; - } else if (speed > 0) { - int[] seekSpeeds = getFastForwardSpeeds(); - for (int index = 0; index < seekSpeeds.length; index++) { - if (speed == seekSpeeds[index]) { - return PLAYBACK_SPEED_FAST_L0 + index; - } - } - } else { - int[] seekSpeeds = getRewindSpeeds(); - for (int index = 0; index < seekSpeeds.length; index++) { - if (-speed == seekSpeeds[index]) { - return -PLAYBACK_SPEED_FAST_L0 - index; - } - } - } - Log.w(TAG, "Couldn't find index for speed " + speed); - return PLAYBACK_SPEED_INVALID; - } - - @Override - public CharSequence getMediaTitle() { - return mMediaController.getMetadata().getDescription().getTitle(); - } - - @Override - public CharSequence getMediaSubtitle() { - return mMediaController.getMetadata().getDescription().getSubtitle(); - } - - @Override - public int getMediaDuration() { - return (int) mMediaController.getMetadata().getLong( - MediaMetadataCompat.METADATA_KEY_DURATION); - } - - @Override - public int getCurrentPosition() { - return (int) mMediaController.getPlaybackState().getPosition(); - } - - @Override - public Drawable getMediaArt() { - Bitmap bitmap = mMediaController.getMetadata().getDescription().getIconBitmap(); - return bitmap == null ? null : new BitmapDrawable(getContext().getResources(), bitmap); - } - - @Override - public long getSupportedActions() { - long result = 0; - long actions = mMediaController.getPlaybackState().getActions(); - if ((actions & PlaybackStateCompat.ACTION_PLAY_PAUSE) != 0) { - result |= ACTION_PLAY_PAUSE; - } - if ((actions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { - result |= ACTION_SKIP_TO_NEXT; - } - if ((actions & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { - result |= ACTION_SKIP_TO_PREVIOUS; - } - if ((actions & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0) { - result |= ACTION_FAST_FORWARD; - } - if ((actions & PlaybackStateCompat.ACTION_REWIND) != 0) { - result |= ACTION_REWIND; - } - return result; - } - - @Override - protected void startPlayback(int speed) { - if (DEBUG) Log.v(TAG, "startPlayback speed " + speed); - if (speed == PLAYBACK_SPEED_NORMAL) { - mMediaController.getTransportControls().play(); - } else if (speed > 0) { - mMediaController.getTransportControls().fastForward(); - } else { - mMediaController.getTransportControls().rewind(); - } - } - - @Override - protected void pausePlayback() { - if (DEBUG) Log.v(TAG, "pausePlayback"); - mMediaController.getTransportControls().pause(); - } - - @Override - protected void skipToNext() { - if (DEBUG) Log.v(TAG, "skipToNext"); - mMediaController.getTransportControls().skipToNext(); - } - - @Override - protected void skipToPrevious() { - if (DEBUG) Log.v(TAG, "skipToPrevious"); - mMediaController.getTransportControls().skipToPrevious(); - } -} diff --git a/android/support/v17/leanback/app/OnboardingFragment.java b/android/support/v17/leanback/app/OnboardingFragment.java index 22dd2111..b69d5a72 100644 --- a/android/support/v17/leanback/app/OnboardingFragment.java +++ b/android/support/v17/leanback/app/OnboardingFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from OnboardingSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2015 The Android Open Source Project * @@ -22,14 +25,15 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; -import android.app.Fragment; import android.content.Context; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.PagingIndicator; +import android.app.Fragment; import android.util.Log; import android.util.TypedValue; import android.view.ContextThemeWrapper; @@ -340,7 +344,7 @@ abstract public class OnboardingFragment extends Fragment { if (mStartButtonTextSet) { ((Button) mStartButton).setText(mStartButtonText); } - final Context context = FragmentUtil.getContext(this); + final Context context = FragmentUtil.getContext(OnboardingFragment.this); if (sSlideDistance == 0) { sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources() .getDisplayMetrics().scaledDensity); @@ -350,7 +354,7 @@ abstract public class OnboardingFragment extends Fragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (savedInstanceState == null) { mCurrentPageIndex = 0; @@ -538,7 +542,7 @@ abstract public class OnboardingFragment extends Fragment { } private void resolveTheme() { - final Context context = FragmentUtil.getContext(this); + final Context context = FragmentUtil.getContext(OnboardingFragment.this); int theme = onProvideTheme(); if (theme == -1) { // Look up the onboardingTheme in the activity's currently specified theme. If it @@ -592,7 +596,7 @@ abstract public class OnboardingFragment extends Fragment { } boolean startLogoAnimation() { - final Context context = FragmentUtil.getContext(this); + final Context context = FragmentUtil.getContext(OnboardingFragment.this); if (context == null) { return false; } @@ -655,7 +659,7 @@ abstract public class OnboardingFragment extends Fragment { View container = getView(); // Create custom views. LayoutInflater inflater = getThemeInflater(LayoutInflater.from( - FragmentUtil.getContext(this))); + FragmentUtil.getContext(OnboardingFragment.this))); ViewGroup backgroundContainer = (ViewGroup) container.findViewById( R.id.background_container); View background = onCreateBackgroundView(inflater, backgroundContainer); @@ -716,7 +720,7 @@ abstract public class OnboardingFragment extends Fragment { * been done in the past, {@code false} otherwise */ protected final void startEnterAnimation(boolean force) { - final Context context = FragmentUtil.getContext(this); + final Context context = FragmentUtil.getContext(OnboardingFragment.this); if (context == null) { return; } @@ -772,7 +776,7 @@ abstract public class OnboardingFragment extends Fragment { * default fade and slide animation. Returning null will disable the animation. */ protected Animator onCreateDescriptionAnimator() { - return AnimatorInflater.loadAnimator(FragmentUtil.getContext(this), + return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this), R.animator.lb_onboarding_description_enter); } @@ -781,7 +785,7 @@ abstract public class OnboardingFragment extends Fragment { * default fade and slide animation. Returning null will disable the animation. */ protected Animator onCreateTitleAnimator() { - return AnimatorInflater.loadAnimator(FragmentUtil.getContext(this), + return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this), R.animator.lb_onboarding_title_enter); } @@ -919,7 +923,7 @@ abstract public class OnboardingFragment extends Fragment { } }); - final Context context = FragmentUtil.getContext(this); + final Context context = FragmentUtil.getContext(OnboardingFragment.this); // Animator for switching between page indicator and button. if (getCurrentPageIndex() == getPageCount() - 1) { mStartButton.setVisibility(View.VISIBLE); diff --git a/android/support/v17/leanback/app/OnboardingSupportFragment.java b/android/support/v17/leanback/app/OnboardingSupportFragment.java index a24ea4d9..51cb2dea 100644 --- a/android/support/v17/leanback/app/OnboardingSupportFragment.java +++ b/android/support/v17/leanback/app/OnboardingSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from OnboardingFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2015 The Android Open Source Project * @@ -25,14 +22,15 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; -import android.support.v4.app.Fragment; import android.content.Context; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.PagingIndicator; +import android.support.v4.app.Fragment; import android.util.Log; import android.util.TypedValue; import android.view.ContextThemeWrapper; @@ -353,7 +351,7 @@ abstract public class OnboardingSupportFragment extends Fragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (savedInstanceState == null) { mCurrentPageIndex = 0; diff --git a/android/support/v17/leanback/app/PlaybackControlGlue.java b/android/support/v17/leanback/app/PlaybackControlGlue.java deleted file mode 100644 index d74fd114..00000000 --- a/android/support/v17/leanback/app/PlaybackControlGlue.java +++ /dev/null @@ -1,337 +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.support.v17.leanback.app; - -import android.content.Context; -import android.support.v17.leanback.media.PlaybackGlueHost; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.OnItemViewClickedListener; -import android.support.v17.leanback.widget.PlaybackControlsRow; -import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; -import android.support.v17.leanback.widget.PlaybackRowPresenter; -import android.support.v17.leanback.widget.Presenter; -import android.support.v17.leanback.widget.PresenterSelector; -import android.support.v17.leanback.widget.Row; -import android.support.v17.leanback.widget.RowPresenter; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.view.InputEvent; -import android.view.KeyEvent; -import android.view.View; - -/** - * A helper class for managing a {@link android.support.v17.leanback.widget.PlaybackControlsRow} - * and {@link PlaybackGlueHost} that implements a - * recommended approach to handling standard playback control actions such as play/pause, - * fast forward/rewind at progressive speed levels, and skip to next/previous. This helper class - * is a glue layer in that manages the configuration of and interaction between the - * leanback UI components by defining a functional interface to the media player. - * - * <p>You can instantiate a concrete subclass such as MediaPlayerGlue or you must - * subclass this abstract helper. To create a subclass you must implement all of the - * abstract methods and the subclass must invoke {@link #onMetadataChanged()} and - * {@link #onStateChanged()} appropriately. - * </p> - * - * <p>To use an instance of the glue layer, first construct an instance. Constructor parameters - * inform the glue what speed levels are supported for fast forward/rewind. - * </p> - * - * <p>If you have your own controls row you must pass it to {@link #setControlsRow}. - * The row will be updated by the glue layer based on the media metadata and playback state. - * Alternatively, you may call {@link #createControlsRowAndPresenter()} which will set a controls - * row and return a row presenter you can use to present the row. - * </p> - * - * <p>The helper sets a {@link android.support.v17.leanback.widget.SparseArrayObjectAdapter} - * on the controls row as the primary actions adapter, and adds actions to it. You can provide - * additional actions by overriding {@link #createPrimaryActionsAdapter}. This helper does not - * deal in secondary actions so those you may add separately. - * </p> - * - * <p>Provide a click listener on your fragment and if an action is clicked, call - * {@link #onActionClicked}. If you set a listener by calling {@link #setOnItemViewClickedListener}, - * your listener will be called for all unhandled actions. - * </p> - * - * <p>This helper implements a key event handler. If you pass a - * {@link PlaybackOverlayFragment}, it will configure its - * fragment to intercept all key events. Otherwise, you should set the glue object as key event - * handler to the ViewHolder when bound by your row presenter; see - * {@link RowPresenter.ViewHolder#setOnKeyListener(android.view.View.OnKeyListener)}. - * </p> - * - * <p>To update the controls row progress during playback, override {@link #enableProgressUpdating} - * to manage the lifecycle of a periodic callback to {@link #updateProgress()}. - * {@link #getUpdatePeriod()} provides a recommended update period. - * </p> - * @deprecated Use {@link android.support.v17.leanback.media.PlaybackControlGlue} - */ -@Deprecated -public abstract class PlaybackControlGlue extends - android.support.v17.leanback.media.PlaybackControlGlue { - - OnItemViewClickedListener mExternalOnItemViewClickedListener; - - /** - * Constructor for the glue. - * - * @param context - * @param seekSpeeds Array of seek speeds for fast forward and rewind. - */ - public PlaybackControlGlue(Context context, int[] seekSpeeds) { - super(context, seekSpeeds, seekSpeeds); - } - - /** - * Constructor for the glue. - * - * @param context - * @param fastForwardSpeeds Array of seek speeds for fast forward. - * @param rewindSpeeds Array of seek speeds for rewind. - */ - public PlaybackControlGlue(Context context, - int[] fastForwardSpeeds, - int[] rewindSpeeds) { - super(context, fastForwardSpeeds, rewindSpeeds); - } - - /** - * Constructor for the glue. - * - * @param context - * @param fragment Optional; if using a {@link PlaybackOverlayFragment}, pass it in. - * @param seekSpeeds Array of seek speeds for fast forward and rewind. - */ - public PlaybackControlGlue(Context context, - PlaybackOverlayFragment fragment, - int[] seekSpeeds) { - this(context, fragment, seekSpeeds, seekSpeeds); - } - - /** - * Constructor for the glue. - * - * @param context - * @param fragment Optional; if using a {@link PlaybackOverlayFragment}, pass it in. - * @param fastForwardSpeeds Array of seek speeds for fast forward. - * @param rewindSpeeds Array of seek speeds for rewind. - */ - public PlaybackControlGlue(Context context, - PlaybackOverlayFragment fragment, - int[] fastForwardSpeeds, - int[] rewindSpeeds) { - super(context, fastForwardSpeeds, rewindSpeeds); - setHost(fragment == null ? (PlaybackGlueHost) null : new PlaybackGlueHostOld(fragment)); - } - - @Override - protected void onAttachedToHost(PlaybackGlueHost host) { - super.onAttachedToHost(host); - if (host instanceof PlaybackGlueHostOld) { - ((PlaybackGlueHostOld) host).mGlue = this; - } - } - - /** - * Returns the fragment. - */ - public PlaybackOverlayFragment getFragment() { - if (getHost() instanceof PlaybackGlueHostOld) { - return ((PlaybackGlueHostOld)getHost()).mFragment; - } - return null; - } - - /** - * Start playback at the given speed. - * @deprecated use {@link #play()} instead. - * - * @param speed The desired playback speed. For normal playback this will be - * {@link #PLAYBACK_SPEED_NORMAL}; higher positive values for fast forward, - * and negative values for rewind. - */ - @Deprecated - protected void startPlayback(int speed) {} - - /** - * Pause playback. - * @deprecated use {@link #pause()} instead. - */ - @Deprecated - protected void pausePlayback() {} - - /** - * Skip to the next track. - * @deprecated use {@link #next()} instead. - */ - @Deprecated - protected void skipToNext() {} - - /** - * Skip to the previous track. - * @deprecated use {@link #previous()} instead. - */ - @Deprecated - protected void skipToPrevious() {} - - @Override - public final void next() { - skipToNext(); - } - - @Override - public final void previous() { - skipToPrevious(); - } - - @Override - public final void play(int speed) { - startPlayback(speed); - } - - @Override - public final void pause() { - pausePlayback(); - } - - /** - * This method invoked when the playback controls row has changed. The adapter - * containing this row should be notified. - */ - protected void onRowChanged(PlaybackControlsRow row) { - } - - /** - * Set the {@link OnItemViewClickedListener} to be called if the click event - * is not handled internally. - * @param listener - * @deprecated Don't call this. Instead use the listener on the fragment yourself. - */ - @Deprecated - public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { - mExternalOnItemViewClickedListener = listener; - } - - /** - * Returns the {@link OnItemViewClickedListener}. - * @deprecated Don't call this. Instead use the listener on the fragment yourself. - */ - @Deprecated - public OnItemViewClickedListener getOnItemViewClickedListener() { - return mExternalOnItemViewClickedListener; - } - - @Override - protected void onCreateControlsRowAndPresenter() { - // backward compatible, we dont create row / presenter by default. - // User is expected to call createControlsRowAndPresenter() or setControlsRow() - // explicitly. - } - - /** - * Helper method for instantiating a - * {@link android.support.v17.leanback.widget.PlaybackControlsRow} and corresponding - * {@link android.support.v17.leanback.widget.PlaybackControlsRowPresenter}. - */ - public PlaybackControlsRowPresenter createControlsRowAndPresenter() { - super.onCreateControlsRowAndPresenter(); - return getControlsRowPresenter(); - } - - @Override - protected SparseArrayObjectAdapter createPrimaryActionsAdapter( - PresenterSelector presenterSelector) { - return super.createPrimaryActionsAdapter(presenterSelector); - } - - /** - * Interface allowing the application to handle input events. - * @deprecated Use - * {@link PlaybackGlueHost#setOnKeyInterceptListener(View.OnKeyListener)}. - */ - @Deprecated - public interface InputEventHandler { - /** - * Called when an {@link InputEvent} is received. - * - * @return If the event should be consumed, return true. To allow the event to - * continue on to the next handler, return false. - */ - boolean handleInputEvent(InputEvent event); - } - - static final class PlaybackGlueHostOld extends PlaybackGlueHost { - final PlaybackOverlayFragment mFragment; - PlaybackControlGlue mGlue; - OnActionClickedListener mActionClickedListener; - - public PlaybackGlueHostOld(PlaybackOverlayFragment fragment) { - mFragment = fragment; - mFragment.setOnItemViewClickedListener(new OnItemViewClickedListener() { - @Override - public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, - RowPresenter.ViewHolder rowViewHolder, Row row) { - if (item instanceof Action - && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder - && mActionClickedListener != null) { - mActionClickedListener.onActionClicked((Action) item); - } else if (mGlue != null && mGlue.getOnItemViewClickedListener() != null) { - mGlue.getOnItemViewClickedListener().onItemClicked(itemViewHolder, - item, rowViewHolder, row); - } - } - }); - } - - @Override - public void setFadingEnabled(boolean enable) { - mFragment.setFadingEnabled(enable); - } - - @Override - public void setOnKeyInterceptListener(final View.OnKeyListener onKeyListener) { - mFragment.setEventHandler( new InputEventHandler() { - @Override - public boolean handleInputEvent(InputEvent event) { - if (event instanceof KeyEvent) { - KeyEvent keyEvent = (KeyEvent) event; - return onKeyListener.onKey(null, keyEvent.getKeyCode(), keyEvent); - } - return false; - } - }); - } - - @Override - public void setOnActionClickedListener(final OnActionClickedListener listener) { - mActionClickedListener = listener; - } - - @Override - public void setHostCallback(HostCallback callback) { - mFragment.setHostCallback(callback); - } - - @Override - public void fadeOut() { - mFragment.fadeOut(); - } - - @Override - public void notifyPlaybackRowChanged() { - mGlue.onRowChanged(mGlue.getControlsRow()); - } - } -} diff --git a/android/support/v17/leanback/app/PlaybackControlSupportGlue.java b/android/support/v17/leanback/app/PlaybackControlSupportGlue.java deleted file mode 100644 index b3d19aee..00000000 --- a/android/support/v17/leanback/app/PlaybackControlSupportGlue.java +++ /dev/null @@ -1,202 +0,0 @@ -/* This file is auto-generated from PlaybackControlGlue.java. DO NOT MODIFY. */ - -package android.support.v17.leanback.app; - -import android.content.Context; -import android.support.v17.leanback.media.PlaybackGlueHost; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.OnItemViewClickedListener; -import android.support.v17.leanback.widget.PlaybackRowPresenter; -import android.support.v17.leanback.widget.Presenter; -import android.support.v17.leanback.widget.Row; -import android.support.v17.leanback.widget.RowPresenter; -import android.view.InputEvent; -import android.view.KeyEvent; -import android.view.View; - -/** - * @deprecated Use {@link android.support.v17.leanback.media.PlaybackControlGlue} and - * {@link PlaybackSupportFragmentGlueHost} for {@link PlaybackSupportFragment}. - */ -@Deprecated -public abstract class PlaybackControlSupportGlue extends PlaybackControlGlue { - /** - * The adapter key for the first custom control on the left side - * of the predefined primary controls. - */ - public static final int ACTION_CUSTOM_LEFT_FIRST = PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST; - - /** - * The adapter key for the skip to previous control. - */ - public static final int ACTION_SKIP_TO_PREVIOUS = PlaybackControlGlue.ACTION_SKIP_TO_PREVIOUS; - - /** - * The adapter key for the rewind control. - */ - public static final int ACTION_REWIND = PlaybackControlGlue.ACTION_REWIND; - - /** - * The adapter key for the play/pause control. - */ - public static final int ACTION_PLAY_PAUSE = PlaybackControlGlue.ACTION_PLAY_PAUSE; - - /** - * The adapter key for the fast forward control. - */ - public static final int ACTION_FAST_FORWARD = PlaybackControlGlue.ACTION_FAST_FORWARD; - - /** - * The adapter key for the skip to next control. - */ - public static final int ACTION_SKIP_TO_NEXT = PlaybackControlGlue.ACTION_SKIP_TO_NEXT; - - /** - * The adapter key for the first custom control on the right side - * of the predefined primary controls. - */ - public static final int ACTION_CUSTOM_RIGHT_FIRST = - PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST; - - /** - * Invalid playback speed. - */ - public static final int PLAYBACK_SPEED_INVALID = PlaybackControlGlue.PLAYBACK_SPEED_INVALID; - - /** - * Speed representing playback state that is paused. - */ - public static final int PLAYBACK_SPEED_PAUSED = PlaybackControlGlue.PLAYBACK_SPEED_PAUSED; - - /** - * Speed representing playback state that is playing normally. - */ - public static final int PLAYBACK_SPEED_NORMAL = PlaybackControlGlue.PLAYBACK_SPEED_NORMAL; - - /** - * The initial (level 0) fast forward playback speed. - * The negative of this value is for rewind at the same speed. - */ - public static final int PLAYBACK_SPEED_FAST_L0 = PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0; - - /** - * The level 1 fast forward playback speed. - * The negative of this value is for rewind at the same speed. - */ - public static final int PLAYBACK_SPEED_FAST_L1 = PlaybackControlGlue.PLAYBACK_SPEED_FAST_L1; - - /** - * The level 2 fast forward playback speed. - * The negative of this value is for rewind at the same speed. - */ - public static final int PLAYBACK_SPEED_FAST_L2 = PlaybackControlGlue.PLAYBACK_SPEED_FAST_L2; - - /** - * The level 3 fast forward playback speed. - * The negative of this value is for rewind at the same speed. - */ - public static final int PLAYBACK_SPEED_FAST_L3 = PlaybackControlGlue.PLAYBACK_SPEED_FAST_L3; - - /** - * The level 4 fast forward playback speed. - * The negative of this value is for rewind at the same speed. - */ - public static final int PLAYBACK_SPEED_FAST_L4 = PlaybackControlGlue.PLAYBACK_SPEED_FAST_L4; - - public PlaybackControlSupportGlue(Context context, int[] seekSpeeds) { - this(context, null, seekSpeeds, seekSpeeds); - } - - public PlaybackControlSupportGlue( - Context context, int[] fastForwardSpeeds, int[] rewindSpeeds) { - this(context, null, fastForwardSpeeds, rewindSpeeds); - } - - public PlaybackControlSupportGlue( - Context context, - PlaybackOverlaySupportFragment fragment, - int[] seekSpeeds) { - this(context, fragment, seekSpeeds, seekSpeeds); - } - - public PlaybackControlSupportGlue( - Context context, - PlaybackOverlaySupportFragment fragment, - int[] fastForwardSpeeds, - int[] rewindSpeeds) { - super(context, fastForwardSpeeds, rewindSpeeds); - setHost(fragment == null ? null : new PlaybackSupportGlueHostOld(fragment)); - } - - @Override - protected void onAttachedToHost(PlaybackGlueHost host) { - super.onAttachedToHost(host); - if (host instanceof PlaybackSupportGlueHostOld) { - ((PlaybackSupportGlueHostOld) host).mGlue = this; - } - } - - static final class PlaybackSupportGlueHostOld extends PlaybackGlueHost { - final PlaybackOverlaySupportFragment mFragment; - PlaybackControlSupportGlue mGlue; - OnActionClickedListener mActionClickedListener; - - public PlaybackSupportGlueHostOld(PlaybackOverlaySupportFragment fragment) { - mFragment = fragment; - mFragment.setOnItemViewClickedListener(new OnItemViewClickedListener() { - @Override - public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, - RowPresenter.ViewHolder rowViewHolder, Row row) { - if (item instanceof Action - && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder - && mActionClickedListener != null) { - mActionClickedListener.onActionClicked((Action) item); - } else if (mGlue != null && mGlue.getOnItemViewClickedListener() != null) { - mGlue.getOnItemViewClickedListener().onItemClicked(itemViewHolder, - item, rowViewHolder, row); - } - } - }); - } - - @Override - public void setFadingEnabled(boolean enable) { - mFragment.setFadingEnabled(enable); - } - - @Override - public void setOnKeyInterceptListener(final View.OnKeyListener onKeyListenerr) { - mFragment.setEventHandler( new InputEventHandler() { - @Override - public boolean handleInputEvent(InputEvent event) { - if (event instanceof KeyEvent) { - KeyEvent keyEvent = (KeyEvent) event; - return onKeyListenerr.onKey(null, keyEvent.getKeyCode(), keyEvent); - } - return false; - } - }); - } - - @Override - public void setOnActionClickedListener(final OnActionClickedListener listener) { - mActionClickedListener = listener; - } - - @Override - public void setHostCallback(HostCallback callback) { - mFragment.setHostCallback(callback); - } - - @Override - public void fadeOut() { - mFragment.fadeOut(); - } - - @Override - public void notifyPlaybackRowChanged() { - mGlue.onRowChanged(mGlue.getControlsRow()); - } - } -} diff --git a/android/support/v17/leanback/app/PlaybackFragment.java b/android/support/v17/leanback/app/PlaybackFragment.java index 68a12151..33e787c3 100644 --- a/android/support/v17/leanback/app/PlaybackFragment.java +++ b/android/support/v17/leanback/app/PlaybackFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from PlaybackSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2016 The Android Open Source Project * @@ -18,13 +21,14 @@ import android.animation.AnimatorInflater; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.app.Fragment; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.animation.LogAccelerateInterpolator; import android.support.v17.leanback.animation.LogDecelerateInterpolator; @@ -45,6 +49,7 @@ import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import android.support.v17.leanback.widget.VerticalGridView; +import android.app.Fragment; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.InputEvent; @@ -447,7 +452,7 @@ public class PlaybackFragment extends Fragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // controls view are initially visible, make it invisible // if app has called hideControlsOverlay() before view created. @@ -505,7 +510,7 @@ public class PlaybackFragment extends Fragment { } }; - Context context = FragmentUtil.getContext(this); + Context context = FragmentUtil.getContext(PlaybackFragment.this); mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in); mBgFadeInAnimator.addUpdateListener(listener); mBgFadeInAnimator.addListener(mFadeListener); @@ -540,7 +545,7 @@ public class PlaybackFragment extends Fragment { } }; - Context context = FragmentUtil.getContext(this); + Context context = FragmentUtil.getContext(PlaybackFragment.this); mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); mControlRowFadeInAnimator.addUpdateListener(updateListener); mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); @@ -570,7 +575,7 @@ public class PlaybackFragment extends Fragment { } }; - Context context = FragmentUtil.getContext(this); + Context context = FragmentUtil.getContext(PlaybackFragment.this); mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); mOtherRowFadeInAnimator.addUpdateListener(updateListener); mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); diff --git a/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java b/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java index d537c3a0..4a9d10f8 100644 --- a/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java +++ b/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from {}PlaybackSupportFragmentGlueHost.java. DO NOT MODIFY. */ + /* * Copyright (C) 2016 The Android Open Source Project * diff --git a/android/support/v17/leanback/app/PlaybackOverlayFragment.java b/android/support/v17/leanback/app/PlaybackOverlayFragment.java deleted file mode 100644 index d4b532b0..00000000 --- a/android/support/v17/leanback/app/PlaybackOverlayFragment.java +++ /dev/null @@ -1,863 +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.v17.leanback.app; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.support.v17.leanback.R; -import android.support.v17.leanback.animation.LogAccelerateInterpolator; -import android.support.v17.leanback.animation.LogDecelerateInterpolator; -import android.support.v17.leanback.media.PlaybackGlueHost; -import android.support.v17.leanback.widget.ItemAlignmentFacet; -import android.support.v17.leanback.widget.ItemBridgeAdapter; -import android.support.v17.leanback.widget.ObjectAdapter; -import android.support.v17.leanback.widget.ObjectAdapter.DataObserver; -import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; -import android.support.v17.leanback.widget.PlaybackRowPresenter; -import android.support.v17.leanback.widget.Presenter; -import android.support.v17.leanback.widget.PresenterSelector; -import android.support.v17.leanback.widget.RowPresenter; -import android.support.v17.leanback.widget.VerticalGridView; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.InputEvent; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; - -/** - * A fragment for displaying playback controls and related content. - * <p> - * A PlaybackOverlayFragment renders the elements of its {@link ObjectAdapter} as a set - * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses - * of {@link RowPresenter}. - * </p> - * <p> - * An instance of {@link android.support.v17.leanback.widget.PlaybackControlsRow} is expected to be - * at position 0 in the adapter. - * </p> - * <p> - * This class is now deprecated, please us - * </p> - * @deprecated Use {@link PlaybackFragment}. - */ -@Deprecated -public class PlaybackOverlayFragment extends DetailsFragment { - - /** - * No background. - */ - public static final int BG_NONE = 0; - - /** - * A dark translucent background. - */ - public static final int BG_DARK = 1; - - /** - * A light translucent background. - */ - public static final int BG_LIGHT = 2; - - /** - * Listener allowing the application to receive notification of fade in and/or fade out - * completion events. - */ - public static class OnFadeCompleteListener { - public void onFadeInComplete() { - } - public void onFadeOutComplete() { - } - } - - static final String TAG = "PlaybackOF"; - static final boolean DEBUG = false; - private static final int ANIMATION_MULTIPLIER = 1; - - static int START_FADE_OUT = 1; - - // Fading status - static final int IDLE = 0; - private static final int IN = 1; - static final int OUT = 2; - - private int mOtherRowsCenterToBottom; - private int mPaddingBottom; - private View mRootView; - private int mBackgroundType = BG_DARK; - private int mBgDarkColor; - private int mBgLightColor; - private int mShowTimeMs; - private int mMajorFadeTranslateY, mMinorFadeTranslateY; - int mAnimationTranslateY; - OnFadeCompleteListener mFadeCompleteListener; - private PlaybackControlGlue.InputEventHandler mInputEventHandler; - boolean mFadingEnabled = true; - int mFadingStatus = IDLE; - int mBgAlpha; - private ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator; - private ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator; - private ValueAnimator mDescriptionFadeInAnimator, mDescriptionFadeOutAnimator; - private ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator; - boolean mResetControlsToPrimaryActionsPending; - PlaybackGlueHost.HostCallback mHostCallback; - - private final Animator.AnimatorListener mFadeListener = - new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - enableVerticalGridAnimations(false); - } - @Override - public void onAnimationRepeat(Animator animation) { - } - @Override - public void onAnimationCancel(Animator animation) { - } - @Override - public void onAnimationEnd(Animator animation) { - if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha); - if (mBgAlpha > 0) { - enableVerticalGridAnimations(true); - startFadeTimer(); - if (mFadeCompleteListener != null) { - mFadeCompleteListener.onFadeInComplete(); - } - } else { - VerticalGridView verticalView = getVerticalGridView(); - // reset focus to the primary actions only if the selected row was the controls row - if (verticalView != null && verticalView.getSelectedPosition() == 0) { - resetControlsToPrimaryActions(null); - } - if (mFadeCompleteListener != null) { - mFadeCompleteListener.onFadeOutComplete(); - } - } - mFadingStatus = IDLE; - } - }; - - static class FadeHandler extends Handler { - @Override - public void handleMessage(Message message) { - PlaybackOverlayFragment fragment; - if (message.what == START_FADE_OUT) { - fragment = ((WeakReference<PlaybackOverlayFragment>) message.obj).get(); - if (fragment != null && fragment.mFadingEnabled) { - fragment.fade(false); - } - } - } - } - - static final Handler sHandler = new FadeHandler(); - - final WeakReference<PlaybackOverlayFragment> mFragmentReference = new WeakReference(this); - - private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener = - new VerticalGridView.OnTouchInterceptListener() { - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - return onInterceptInputEvent(event); - } - }; - - private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener = - new VerticalGridView.OnKeyInterceptListener() { - @Override - public boolean onInterceptKeyEvent(KeyEvent event) { - return onInterceptInputEvent(event); - } - }; - - void setBgAlpha(int alpha) { - mBgAlpha = alpha; - if (mRootView != null) { - mRootView.getBackground().setAlpha(alpha); - } - } - - void enableVerticalGridAnimations(boolean enable) { - if (getVerticalGridView() != null) { - getVerticalGridView().setAnimateChildLayout(enable); - } - } - - void resetControlsToPrimaryActions(ItemBridgeAdapter.ViewHolder vh) { - if (vh == null && getVerticalGridView() != null) { - vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView().findViewHolderForPosition(0); - } - if (vh == null) { - mResetControlsToPrimaryActionsPending = true; - } else if (vh.getPresenter() instanceof PlaybackControlsRowPresenter) { - mResetControlsToPrimaryActionsPending = false; - ((PlaybackControlsRowPresenter) vh.getPresenter()).showPrimaryActions( - (PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder()); - } - } - - /** - * Enables or disables view fading. If enabled, - * the view will be faded in when the fragment starts, - * and will fade out after a time period. The timeout - * period is reset each time {@link #tickle} is called. - * - */ - public void setFadingEnabled(boolean enabled) { - if (DEBUG) Log.v(TAG, "setFadingEnabled " + enabled); - if (enabled != mFadingEnabled) { - mFadingEnabled = enabled; - if (mFadingEnabled) { - if (isResumed() && mFadingStatus == IDLE - && !sHandler.hasMessages(START_FADE_OUT, mFragmentReference)) { - startFadeTimer(); - } - } else { - // Ensure fully opaque - sHandler.removeMessages(START_FADE_OUT, mFragmentReference); - fade(true); - } - } - } - - /** - * Returns true if view fading is enabled. - */ - public boolean isFadingEnabled() { - return mFadingEnabled; - } - - /** - * Sets the listener to be called when fade in or out has completed. - */ - public void setFadeCompleteListener(OnFadeCompleteListener listener) { - mFadeCompleteListener = listener; - } - - /** - * Returns the listener to be called when fade in or out has completed. - */ - public OnFadeCompleteListener getFadeCompleteListener() { - return mFadeCompleteListener; - } - - @Deprecated - public interface InputEventHandler extends PlaybackControlGlue.InputEventHandler { - } - - /** - * Sets the input event handler. - */ - @Deprecated - public final void setInputEventHandler(InputEventHandler handler) { - mInputEventHandler = handler; - } - - /** - * Returns the input event handler. - */ - @Deprecated - public final InputEventHandler getInputEventHandler() { - return (InputEventHandler)mInputEventHandler; - } - - /** - * Sets the input event handler. - */ - public final void setEventHandler(PlaybackControlGlue.InputEventHandler handler) { - mInputEventHandler = handler; - } - - /** - * Returns the input event handler. - */ - public final PlaybackControlGlue.InputEventHandler getEventHandler() { - return mInputEventHandler; - } - - /** - * Tickles the playback controls. Fades in the view if it was faded out, - * otherwise resets the fade out timer. Tickling on input events is handled - * by the fragment. - */ - public void tickle() { - if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed()); - if (!mFadingEnabled || !isResumed()) { - return; - } - if (sHandler.hasMessages(START_FADE_OUT, mFragmentReference)) { - // Restart the timer - startFadeTimer(); - } else { - fade(true); - } - } - - /** - * Fades out the playback overlay immediately. - */ - public void fadeOut() { - sHandler.removeMessages(START_FADE_OUT, mFragmentReference); - fade(false); - } - - /** - * Sets the {@link PlaybackGlueHost.HostCallback}. Implementor of this interface will - * take appropriate actions to take action when the hosting fragment starts/stops processing. - */ - void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) { - this.mHostCallback = hostCallback; - } - - @Override - public void onStop() { - if (mHostCallback != null) { - mHostCallback.onHostStop(); - } - super.onStop(); - } - - @Override - public void onPause() { - if (mHostCallback != null) { - mHostCallback.onHostPause(); - } - super.onPause(); - } - - private boolean areControlsHidden() { - return mFadingStatus == IDLE && mBgAlpha == 0; - } - - boolean onInterceptInputEvent(InputEvent event) { - final boolean controlsHidden = areControlsHidden(); - if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event); - boolean consumeEvent = false; - int keyCode = KeyEvent.KEYCODE_UNKNOWN; - - if (mInputEventHandler != null) { - consumeEvent = mInputEventHandler.handleInputEvent(event); - } - if (event instanceof KeyEvent) { - keyCode = ((KeyEvent) event).getKeyCode(); - } - - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - // Event may be consumed; regardless, if controls are hidden then these keys will - // bring up the controls. - if (controlsHidden) { - consumeEvent = true; - } - tickle(); - break; - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_ESCAPE: - // If fading enabled and controls are not hidden, back will be consumed to fade - // them out (even if the key was consumed by the handler). - if (mFadingEnabled && !controlsHidden) { - consumeEvent = true; - sHandler.removeMessages(START_FADE_OUT, mFragmentReference); - fade(false); - } else if (consumeEvent) { - tickle(); - } - break; - default: - if (consumeEvent) { - tickle(); - } - } - return consumeEvent; - } - - @Override - public void onResume() { - super.onResume(); - if (mFadingEnabled) { - setBgAlpha(0); - fade(true); - } - getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener); - getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener); - if (mHostCallback != null) { - mHostCallback.onHostResume(); - } - } - - void startFadeTimer() { - sHandler.removeMessages(START_FADE_OUT, mFragmentReference); - sHandler.sendMessageDelayed(sHandler.obtainMessage(START_FADE_OUT, mFragmentReference), - mShowTimeMs); - } - - private static ValueAnimator loadAnimator(Context context, int resId) { - ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId); - animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER); - return animator; - } - - private void loadBgAnimator() { - AnimatorUpdateListener listener = new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - setBgAlpha((Integer) arg0.getAnimatedValue()); - } - }; - - Context context = FragmentUtil.getContext(this); - mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in); - mBgFadeInAnimator.addUpdateListener(listener); - mBgFadeInAnimator.addListener(mFadeListener); - - mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out); - mBgFadeOutAnimator.addUpdateListener(listener); - mBgFadeOutAnimator.addListener(mFadeListener); - } - - private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100,0); - private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100,0); - - View getControlRowView() { - if (getVerticalGridView() == null) { - return null; - } - RecyclerView.ViewHolder vh = getVerticalGridView().findViewHolderForPosition(0); - if (vh == null) { - return null; - } - return vh.itemView; - } - - private void loadControlRowAnimator() { - final AnimatorListener listener = new AnimatorListener() { - @Override - void getViews(ArrayList<View> views) { - View view = getControlRowView(); - if (view != null) { - views.add(view); - } - } - }; - final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - View view = getControlRowView(); - if (view != null) { - final float fraction = (Float) arg0.getAnimatedValue(); - if (DEBUG) Log.v(TAG, "fraction " + fraction); - view.setAlpha(fraction); - view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); - } - } - }; - - Context context = FragmentUtil.getContext(this); - mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); - mControlRowFadeInAnimator.addUpdateListener(updateListener); - mControlRowFadeInAnimator.addListener(listener); - mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); - - mControlRowFadeOutAnimator = loadAnimator(context, - R.animator.lb_playback_controls_fade_out); - mControlRowFadeOutAnimator.addUpdateListener(updateListener); - mControlRowFadeOutAnimator.addListener(listener); - mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator); - } - - private void loadOtherRowAnimator() { - final AnimatorListener listener = new AnimatorListener() { - @Override - void getViews(ArrayList<View> views) { - if (getVerticalGridView() == null) { - return; - } - final int count = getVerticalGridView().getChildCount(); - for (int i = 0; i < count; i++) { - View view = getVerticalGridView().getChildAt(i); - if (view != null) { - views.add(view); - } - } - } - }; - final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - if (getVerticalGridView() == null) { - return; - } - final float fraction = (Float) arg0.getAnimatedValue(); - for (View view : listener.mViews) { - if (getVerticalGridView().getChildPosition(view) > 0) { - view.setAlpha(fraction); - view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); - } - } - } - }; - - Context context = FragmentUtil.getContext(this); - mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); - mOtherRowFadeInAnimator.addListener(listener); - mOtherRowFadeInAnimator.addUpdateListener(updateListener); - mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); - - mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out); - mOtherRowFadeOutAnimator.addListener(listener); - mOtherRowFadeOutAnimator.addUpdateListener(updateListener); - mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator()); - } - - private void loadDescriptionAnimator() { - AnimatorUpdateListener listener = new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - if (getVerticalGridView() == null) { - return; - } - ItemBridgeAdapter.ViewHolder adapterVh = (ItemBridgeAdapter.ViewHolder) - getVerticalGridView().findViewHolderForPosition(0); - if (adapterVh != null && adapterVh.getViewHolder() - instanceof PlaybackControlsRowPresenter.ViewHolder) { - final Presenter.ViewHolder vh = ((PlaybackControlsRowPresenter.ViewHolder) - adapterVh.getViewHolder()).mDescriptionViewHolder; - if (vh != null) { - vh.view.setAlpha((Float) arg0.getAnimatedValue()); - } - } - } - }; - - Context context = FragmentUtil.getContext(this); - mDescriptionFadeInAnimator = loadAnimator(context, - R.animator.lb_playback_description_fade_in); - mDescriptionFadeInAnimator.addUpdateListener(listener); - mDescriptionFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); - - mDescriptionFadeOutAnimator = loadAnimator(context, - R.animator.lb_playback_description_fade_out); - mDescriptionFadeOutAnimator.addUpdateListener(listener); - } - - void fade(boolean fadeIn) { - if (DEBUG) Log.v(TAG, "fade " + fadeIn); - if (getView() == null) { - return; - } - if ((fadeIn && mFadingStatus == IN) || (!fadeIn && mFadingStatus == OUT)) { - if (DEBUG) Log.v(TAG, "requested fade in progress"); - return; - } - if ((fadeIn && mBgAlpha == 255) || (!fadeIn && mBgAlpha == 0)) { - if (DEBUG) Log.v(TAG, "fade is no-op"); - return; - } - - mAnimationTranslateY = getVerticalGridView().getSelectedPosition() == 0 - ? mMajorFadeTranslateY : mMinorFadeTranslateY; - - if (mFadingStatus == IDLE) { - if (fadeIn) { - mBgFadeInAnimator.start(); - mControlRowFadeInAnimator.start(); - mOtherRowFadeInAnimator.start(); - mDescriptionFadeInAnimator.start(); - } else { - mBgFadeOutAnimator.start(); - mControlRowFadeOutAnimator.start(); - mOtherRowFadeOutAnimator.start(); - mDescriptionFadeOutAnimator.start(); - } - } else { - if (fadeIn) { - mBgFadeOutAnimator.reverse(); - mControlRowFadeOutAnimator.reverse(); - mOtherRowFadeOutAnimator.reverse(); - mDescriptionFadeOutAnimator.reverse(); - } else { - mBgFadeInAnimator.reverse(); - mControlRowFadeInAnimator.reverse(); - mOtherRowFadeInAnimator.reverse(); - mDescriptionFadeInAnimator.reverse(); - } - } - getView().announceForAccessibility(getString(fadeIn ? R.string.lb_playback_controls_shown - : R.string.lb_playback_controls_hidden)); - - // If fading in while control row is focused, set initial translationY so - // views slide in from below. - if (fadeIn && mFadingStatus == IDLE) { - final int count = getVerticalGridView().getChildCount(); - for (int i = 0; i < count; i++) { - getVerticalGridView().getChildAt(i).setTranslationY(mAnimationTranslateY); - } - } - - mFadingStatus = fadeIn ? IN : OUT; - } - - /** - * Sets the list of rows for the fragment. - */ - @Override - public void setAdapter(ObjectAdapter adapter) { - if (getAdapter() != null) { - getAdapter().unregisterObserver(mObserver); - } - super.setAdapter(adapter); - if (adapter != null) { - adapter.registerObserver(mObserver); - } - } - - @Override - protected void setupPresenter(Presenter rowPresenter) { - if (rowPresenter instanceof PlaybackRowPresenter) { - if (rowPresenter.getFacet(ItemAlignmentFacet.class) == null) { - ItemAlignmentFacet itemAlignment = new ItemAlignmentFacet(); - ItemAlignmentFacet.ItemAlignmentDef def = - new ItemAlignmentFacet.ItemAlignmentDef(); - def.setItemAlignmentOffset(0); - def.setItemAlignmentOffsetPercent(100); - itemAlignment.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[] - {def}); - rowPresenter.setFacet(ItemAlignmentFacet.class, itemAlignment); - } - } else { - super.setupPresenter(rowPresenter); - } - } - - @Override - void setVerticalGridViewLayout(VerticalGridView listview) { - if (listview == null) { - return; - } - - // we set the base line of alignment to -paddingBottom - listview.setWindowAlignmentOffset(-mPaddingBottom); - listview.setWindowAlignmentOffsetPercent( - VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); - - // align other rows that arent the last to center of screen, since our baseline is - // -mPaddingBottom, we need subtract that from mOtherRowsCenterToBottom. - listview.setItemAlignmentOffset(mOtherRowsCenterToBottom - mPaddingBottom); - listview.setItemAlignmentOffsetPercent(50); - - // Push last row to the bottom padding - // Padding affects alignment when last row is focused - listview.setPadding(listview.getPaddingLeft(), listview.getPaddingTop(), - listview.getPaddingRight(), mPaddingBottom); - listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mOtherRowsCenterToBottom = getResources() - .getDimensionPixelSize(R.dimen.lb_playback_other_rows_center_to_bottom); - mPaddingBottom = - getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom); - mBgDarkColor = - getResources().getColor(R.color.lb_playback_controls_background_dark); - mBgLightColor = - getResources().getColor(R.color.lb_playback_controls_background_light); - mShowTimeMs = - getResources().getInteger(R.integer.lb_playback_controls_show_time_ms); - mMajorFadeTranslateY = - getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y); - mMinorFadeTranslateY = - getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y); - - loadBgAnimator(); - loadControlRowAnimator(); - loadOtherRowAnimator(); - loadDescriptionAnimator(); - } - - /** - * Sets the background type. - * - * @param type One of BG_LIGHT, BG_DARK, or BG_NONE. - */ - public void setBackgroundType(int type) { - switch (type) { - case BG_LIGHT: - case BG_DARK: - case BG_NONE: - if (type != mBackgroundType) { - mBackgroundType = type; - updateBackground(); - } - break; - default: - throw new IllegalArgumentException("Invalid background type"); - } - } - - /** - * Returns the background type. - */ - public int getBackgroundType() { - return mBackgroundType; - } - - private void updateBackground() { - if (mRootView != null) { - int color = mBgDarkColor; - switch (mBackgroundType) { - case BG_DARK: break; - case BG_LIGHT: color = mBgLightColor; break; - case BG_NONE: color = Color.TRANSPARENT; break; - } - mRootView.setBackground(new ColorDrawable(color)); - } - } - - void updateControlsBottomSpace(ItemBridgeAdapter.ViewHolder vh) { - // Add extra space between rows 0 and 1 - if (vh == null && getVerticalGridView() != null) { - vh = (ItemBridgeAdapter.ViewHolder) - getVerticalGridView().findViewHolderForPosition(0); - } - if (vh != null && vh.getPresenter() instanceof PlaybackControlsRowPresenter) { - final int adapterSize = getAdapter() == null ? 0 : getAdapter().size(); - ((PlaybackControlsRowPresenter) vh.getPresenter()).showBottomSpace( - (PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder(), - adapterSize > 1); - } - } - - private final ItemBridgeAdapter.AdapterListener mAdapterListener = - new ItemBridgeAdapter.AdapterListener() { - @Override - public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) { - if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view); - if ((mFadingStatus == IDLE && mBgAlpha == 0) || mFadingStatus == OUT) { - if (DEBUG) Log.v(TAG, "setting alpha to 0"); - vh.getViewHolder().view.setAlpha(0); - } - if (vh.getPosition() == 0 && mResetControlsToPrimaryActionsPending) { - resetControlsToPrimaryActions(vh); - } - } - @Override - public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) { - if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view); - // Reset animation state - vh.getViewHolder().view.setAlpha(1f); - vh.getViewHolder().view.setTranslationY(0); - if (vh.getViewHolder() instanceof PlaybackControlsRowPresenter.ViewHolder) { - Presenter.ViewHolder descriptionVh = ((PlaybackControlsRowPresenter.ViewHolder) - vh.getViewHolder()).mDescriptionViewHolder; - if (descriptionVh != null) { - descriptionVh.view.setAlpha(1f); - } - } - } - @Override - public void onBind(ItemBridgeAdapter.ViewHolder vh) { - if (vh.getPosition() == 0) { - updateControlsBottomSpace(vh); - } - } - }; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - mRootView = super.onCreateView(inflater, container, savedInstanceState); - mBgAlpha = 255; - updateBackground(); - getRowsFragment().setExternalAdapterListener(mAdapterListener); - return mRootView; - } - - @Override - public void onDestroyView() { - mRootView = null; - if (mHostCallback != null) { - mHostCallback.onHostDestroy(); - } - super.onDestroyView(); - } - - @Override - public void onStart() { - super.onStart(); - // Workaround problem VideoView forcing itself to focused, let controls take focus. - getRowsFragment().getView().requestFocus(); - if (mHostCallback != null) { - mHostCallback.onHostStart(); - } - } - - private final DataObserver mObserver = new DataObserver() { - @Override - public void onChanged() { - updateControlsBottomSpace(null); - } - }; - - static abstract class AnimatorListener implements Animator.AnimatorListener { - ArrayList<View> mViews = new ArrayList<View>(); - ArrayList<Integer> mLayerType = new ArrayList<Integer>(); - - @Override - public void onAnimationCancel(Animator animation) { - } - @Override - public void onAnimationRepeat(Animator animation) { - } - @Override - public void onAnimationStart(Animator animation) { - getViews(mViews); - for (View view : mViews) { - mLayerType.add(view.getLayerType()); - view.setLayerType(View.LAYER_TYPE_HARDWARE, null); - } - } - @Override - public void onAnimationEnd(Animator animation) { - for (int i = 0; i < mViews.size(); i++) { - mViews.get(i).setLayerType(mLayerType.get(i), null); - } - mLayerType.clear(); - mViews.clear(); - } - abstract void getViews(ArrayList<View> views); - - }; -} diff --git a/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java b/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java deleted file mode 100644 index d7513204..00000000 --- a/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java +++ /dev/null @@ -1,866 +0,0 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from PlaybackOverlayFragment.java. DO NOT MODIFY. */ - -/* - * 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.v17.leanback.app; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.support.v17.leanback.R; -import android.support.v17.leanback.animation.LogAccelerateInterpolator; -import android.support.v17.leanback.animation.LogDecelerateInterpolator; -import android.support.v17.leanback.media.PlaybackGlueHost; -import android.support.v17.leanback.widget.ItemAlignmentFacet; -import android.support.v17.leanback.widget.ItemBridgeAdapter; -import android.support.v17.leanback.widget.ObjectAdapter; -import android.support.v17.leanback.widget.ObjectAdapter.DataObserver; -import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; -import android.support.v17.leanback.widget.PlaybackRowPresenter; -import android.support.v17.leanback.widget.Presenter; -import android.support.v17.leanback.widget.PresenterSelector; -import android.support.v17.leanback.widget.RowPresenter; -import android.support.v17.leanback.widget.VerticalGridView; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.InputEvent; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; - -/** - * A fragment for displaying playback controls and related content. - * <p> - * A PlaybackOverlaySupportFragment renders the elements of its {@link ObjectAdapter} as a set - * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses - * of {@link RowPresenter}. - * </p> - * <p> - * An instance of {@link android.support.v17.leanback.widget.PlaybackControlsRow} is expected to be - * at position 0 in the adapter. - * </p> - * <p> - * This class is now deprecated, please us - * </p> - * @deprecated Use {@link PlaybackSupportFragment}. - */ -@Deprecated -public class PlaybackOverlaySupportFragment extends DetailsSupportFragment { - - /** - * No background. - */ - public static final int BG_NONE = 0; - - /** - * A dark translucent background. - */ - public static final int BG_DARK = 1; - - /** - * A light translucent background. - */ - public static final int BG_LIGHT = 2; - - /** - * Listener allowing the application to receive notification of fade in and/or fade out - * completion events. - */ - public static class OnFadeCompleteListener { - public void onFadeInComplete() { - } - public void onFadeOutComplete() { - } - } - - static final String TAG = "PlaybackOF"; - static final boolean DEBUG = false; - private static final int ANIMATION_MULTIPLIER = 1; - - static int START_FADE_OUT = 1; - - // Fading status - static final int IDLE = 0; - private static final int IN = 1; - static final int OUT = 2; - - private int mOtherRowsCenterToBottom; - private int mPaddingBottom; - private View mRootView; - private int mBackgroundType = BG_DARK; - private int mBgDarkColor; - private int mBgLightColor; - private int mShowTimeMs; - private int mMajorFadeTranslateY, mMinorFadeTranslateY; - int mAnimationTranslateY; - OnFadeCompleteListener mFadeCompleteListener; - private PlaybackControlGlue.InputEventHandler mInputEventHandler; - boolean mFadingEnabled = true; - int mFadingStatus = IDLE; - int mBgAlpha; - private ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator; - private ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator; - private ValueAnimator mDescriptionFadeInAnimator, mDescriptionFadeOutAnimator; - private ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator; - boolean mResetControlsToPrimaryActionsPending; - PlaybackGlueHost.HostCallback mHostCallback; - - private final Animator.AnimatorListener mFadeListener = - new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - enableVerticalGridAnimations(false); - } - @Override - public void onAnimationRepeat(Animator animation) { - } - @Override - public void onAnimationCancel(Animator animation) { - } - @Override - public void onAnimationEnd(Animator animation) { - if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha); - if (mBgAlpha > 0) { - enableVerticalGridAnimations(true); - startFadeTimer(); - if (mFadeCompleteListener != null) { - mFadeCompleteListener.onFadeInComplete(); - } - } else { - VerticalGridView verticalView = getVerticalGridView(); - // reset focus to the primary actions only if the selected row was the controls row - if (verticalView != null && verticalView.getSelectedPosition() == 0) { - resetControlsToPrimaryActions(null); - } - if (mFadeCompleteListener != null) { - mFadeCompleteListener.onFadeOutComplete(); - } - } - mFadingStatus = IDLE; - } - }; - - static class FadeHandler extends Handler { - @Override - public void handleMessage(Message message) { - PlaybackOverlaySupportFragment fragment; - if (message.what == START_FADE_OUT) { - fragment = ((WeakReference<PlaybackOverlaySupportFragment>) message.obj).get(); - if (fragment != null && fragment.mFadingEnabled) { - fragment.fade(false); - } - } - } - } - - static final Handler sHandler = new FadeHandler(); - - final WeakReference<PlaybackOverlaySupportFragment> mFragmentReference = new WeakReference(this); - - private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener = - new VerticalGridView.OnTouchInterceptListener() { - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - return onInterceptInputEvent(event); - } - }; - - private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener = - new VerticalGridView.OnKeyInterceptListener() { - @Override - public boolean onInterceptKeyEvent(KeyEvent event) { - return onInterceptInputEvent(event); - } - }; - - void setBgAlpha(int alpha) { - mBgAlpha = alpha; - if (mRootView != null) { - mRootView.getBackground().setAlpha(alpha); - } - } - - void enableVerticalGridAnimations(boolean enable) { - if (getVerticalGridView() != null) { - getVerticalGridView().setAnimateChildLayout(enable); - } - } - - void resetControlsToPrimaryActions(ItemBridgeAdapter.ViewHolder vh) { - if (vh == null && getVerticalGridView() != null) { - vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView().findViewHolderForPosition(0); - } - if (vh == null) { - mResetControlsToPrimaryActionsPending = true; - } else if (vh.getPresenter() instanceof PlaybackControlsRowPresenter) { - mResetControlsToPrimaryActionsPending = false; - ((PlaybackControlsRowPresenter) vh.getPresenter()).showPrimaryActions( - (PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder()); - } - } - - /** - * Enables or disables view fading. If enabled, - * the view will be faded in when the fragment starts, - * and will fade out after a time period. The timeout - * period is reset each time {@link #tickle} is called. - * - */ - public void setFadingEnabled(boolean enabled) { - if (DEBUG) Log.v(TAG, "setFadingEnabled " + enabled); - if (enabled != mFadingEnabled) { - mFadingEnabled = enabled; - if (mFadingEnabled) { - if (isResumed() && mFadingStatus == IDLE - && !sHandler.hasMessages(START_FADE_OUT, mFragmentReference)) { - startFadeTimer(); - } - } else { - // Ensure fully opaque - sHandler.removeMessages(START_FADE_OUT, mFragmentReference); - fade(true); - } - } - } - - /** - * Returns true if view fading is enabled. - */ - public boolean isFadingEnabled() { - return mFadingEnabled; - } - - /** - * Sets the listener to be called when fade in or out has completed. - */ - public void setFadeCompleteListener(OnFadeCompleteListener listener) { - mFadeCompleteListener = listener; - } - - /** - * Returns the listener to be called when fade in or out has completed. - */ - public OnFadeCompleteListener getFadeCompleteListener() { - return mFadeCompleteListener; - } - - @Deprecated - public interface InputEventHandler extends PlaybackControlGlue.InputEventHandler { - } - - /** - * Sets the input event handler. - */ - @Deprecated - public final void setInputEventHandler(InputEventHandler handler) { - mInputEventHandler = handler; - } - - /** - * Returns the input event handler. - */ - @Deprecated - public final InputEventHandler getInputEventHandler() { - return (InputEventHandler)mInputEventHandler; - } - - /** - * Sets the input event handler. - */ - public final void setEventHandler(PlaybackControlGlue.InputEventHandler handler) { - mInputEventHandler = handler; - } - - /** - * Returns the input event handler. - */ - public final PlaybackControlGlue.InputEventHandler getEventHandler() { - return mInputEventHandler; - } - - /** - * Tickles the playback controls. Fades in the view if it was faded out, - * otherwise resets the fade out timer. Tickling on input events is handled - * by the fragment. - */ - public void tickle() { - if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed()); - if (!mFadingEnabled || !isResumed()) { - return; - } - if (sHandler.hasMessages(START_FADE_OUT, mFragmentReference)) { - // Restart the timer - startFadeTimer(); - } else { - fade(true); - } - } - - /** - * Fades out the playback overlay immediately. - */ - public void fadeOut() { - sHandler.removeMessages(START_FADE_OUT, mFragmentReference); - fade(false); - } - - /** - * Sets the {@link PlaybackGlueHost.HostCallback}. Implementor of this interface will - * take appropriate actions to take action when the hosting fragment starts/stops processing. - */ - void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) { - this.mHostCallback = hostCallback; - } - - @Override - public void onStop() { - if (mHostCallback != null) { - mHostCallback.onHostStop(); - } - super.onStop(); - } - - @Override - public void onPause() { - if (mHostCallback != null) { - mHostCallback.onHostPause(); - } - super.onPause(); - } - - private boolean areControlsHidden() { - return mFadingStatus == IDLE && mBgAlpha == 0; - } - - boolean onInterceptInputEvent(InputEvent event) { - final boolean controlsHidden = areControlsHidden(); - if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event); - boolean consumeEvent = false; - int keyCode = KeyEvent.KEYCODE_UNKNOWN; - - if (mInputEventHandler != null) { - consumeEvent = mInputEventHandler.handleInputEvent(event); - } - if (event instanceof KeyEvent) { - keyCode = ((KeyEvent) event).getKeyCode(); - } - - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - // Event may be consumed; regardless, if controls are hidden then these keys will - // bring up the controls. - if (controlsHidden) { - consumeEvent = true; - } - tickle(); - break; - case KeyEvent.KEYCODE_BACK: - case KeyEvent.KEYCODE_ESCAPE: - // If fading enabled and controls are not hidden, back will be consumed to fade - // them out (even if the key was consumed by the handler). - if (mFadingEnabled && !controlsHidden) { - consumeEvent = true; - sHandler.removeMessages(START_FADE_OUT, mFragmentReference); - fade(false); - } else if (consumeEvent) { - tickle(); - } - break; - default: - if (consumeEvent) { - tickle(); - } - } - return consumeEvent; - } - - @Override - public void onResume() { - super.onResume(); - if (mFadingEnabled) { - setBgAlpha(0); - fade(true); - } - getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener); - getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener); - if (mHostCallback != null) { - mHostCallback.onHostResume(); - } - } - - void startFadeTimer() { - sHandler.removeMessages(START_FADE_OUT, mFragmentReference); - sHandler.sendMessageDelayed(sHandler.obtainMessage(START_FADE_OUT, mFragmentReference), - mShowTimeMs); - } - - private static ValueAnimator loadAnimator(Context context, int resId) { - ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId); - animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER); - return animator; - } - - private void loadBgAnimator() { - AnimatorUpdateListener listener = new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - setBgAlpha((Integer) arg0.getAnimatedValue()); - } - }; - - Context context = getContext(); - mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in); - mBgFadeInAnimator.addUpdateListener(listener); - mBgFadeInAnimator.addListener(mFadeListener); - - mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out); - mBgFadeOutAnimator.addUpdateListener(listener); - mBgFadeOutAnimator.addListener(mFadeListener); - } - - private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100,0); - private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100,0); - - View getControlRowView() { - if (getVerticalGridView() == null) { - return null; - } - RecyclerView.ViewHolder vh = getVerticalGridView().findViewHolderForPosition(0); - if (vh == null) { - return null; - } - return vh.itemView; - } - - private void loadControlRowAnimator() { - final AnimatorListener listener = new AnimatorListener() { - @Override - void getViews(ArrayList<View> views) { - View view = getControlRowView(); - if (view != null) { - views.add(view); - } - } - }; - final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - View view = getControlRowView(); - if (view != null) { - final float fraction = (Float) arg0.getAnimatedValue(); - if (DEBUG) Log.v(TAG, "fraction " + fraction); - view.setAlpha(fraction); - view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); - } - } - }; - - Context context = getContext(); - mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); - mControlRowFadeInAnimator.addUpdateListener(updateListener); - mControlRowFadeInAnimator.addListener(listener); - mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); - - mControlRowFadeOutAnimator = loadAnimator(context, - R.animator.lb_playback_controls_fade_out); - mControlRowFadeOutAnimator.addUpdateListener(updateListener); - mControlRowFadeOutAnimator.addListener(listener); - mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator); - } - - private void loadOtherRowAnimator() { - final AnimatorListener listener = new AnimatorListener() { - @Override - void getViews(ArrayList<View> views) { - if (getVerticalGridView() == null) { - return; - } - final int count = getVerticalGridView().getChildCount(); - for (int i = 0; i < count; i++) { - View view = getVerticalGridView().getChildAt(i); - if (view != null) { - views.add(view); - } - } - } - }; - final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - if (getVerticalGridView() == null) { - return; - } - final float fraction = (Float) arg0.getAnimatedValue(); - for (View view : listener.mViews) { - if (getVerticalGridView().getChildPosition(view) > 0) { - view.setAlpha(fraction); - view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); - } - } - } - }; - - Context context = getContext(); - mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); - mOtherRowFadeInAnimator.addListener(listener); - mOtherRowFadeInAnimator.addUpdateListener(updateListener); - mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); - - mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out); - mOtherRowFadeOutAnimator.addListener(listener); - mOtherRowFadeOutAnimator.addUpdateListener(updateListener); - mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator()); - } - - private void loadDescriptionAnimator() { - AnimatorUpdateListener listener = new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - if (getVerticalGridView() == null) { - return; - } - ItemBridgeAdapter.ViewHolder adapterVh = (ItemBridgeAdapter.ViewHolder) - getVerticalGridView().findViewHolderForPosition(0); - if (adapterVh != null && adapterVh.getViewHolder() - instanceof PlaybackControlsRowPresenter.ViewHolder) { - final Presenter.ViewHolder vh = ((PlaybackControlsRowPresenter.ViewHolder) - adapterVh.getViewHolder()).mDescriptionViewHolder; - if (vh != null) { - vh.view.setAlpha((Float) arg0.getAnimatedValue()); - } - } - } - }; - - Context context = getContext(); - mDescriptionFadeInAnimator = loadAnimator(context, - R.animator.lb_playback_description_fade_in); - mDescriptionFadeInAnimator.addUpdateListener(listener); - mDescriptionFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); - - mDescriptionFadeOutAnimator = loadAnimator(context, - R.animator.lb_playback_description_fade_out); - mDescriptionFadeOutAnimator.addUpdateListener(listener); - } - - void fade(boolean fadeIn) { - if (DEBUG) Log.v(TAG, "fade " + fadeIn); - if (getView() == null) { - return; - } - if ((fadeIn && mFadingStatus == IN) || (!fadeIn && mFadingStatus == OUT)) { - if (DEBUG) Log.v(TAG, "requested fade in progress"); - return; - } - if ((fadeIn && mBgAlpha == 255) || (!fadeIn && mBgAlpha == 0)) { - if (DEBUG) Log.v(TAG, "fade is no-op"); - return; - } - - mAnimationTranslateY = getVerticalGridView().getSelectedPosition() == 0 - ? mMajorFadeTranslateY : mMinorFadeTranslateY; - - if (mFadingStatus == IDLE) { - if (fadeIn) { - mBgFadeInAnimator.start(); - mControlRowFadeInAnimator.start(); - mOtherRowFadeInAnimator.start(); - mDescriptionFadeInAnimator.start(); - } else { - mBgFadeOutAnimator.start(); - mControlRowFadeOutAnimator.start(); - mOtherRowFadeOutAnimator.start(); - mDescriptionFadeOutAnimator.start(); - } - } else { - if (fadeIn) { - mBgFadeOutAnimator.reverse(); - mControlRowFadeOutAnimator.reverse(); - mOtherRowFadeOutAnimator.reverse(); - mDescriptionFadeOutAnimator.reverse(); - } else { - mBgFadeInAnimator.reverse(); - mControlRowFadeInAnimator.reverse(); - mOtherRowFadeInAnimator.reverse(); - mDescriptionFadeInAnimator.reverse(); - } - } - getView().announceForAccessibility(getString(fadeIn ? R.string.lb_playback_controls_shown - : R.string.lb_playback_controls_hidden)); - - // If fading in while control row is focused, set initial translationY so - // views slide in from below. - if (fadeIn && mFadingStatus == IDLE) { - final int count = getVerticalGridView().getChildCount(); - for (int i = 0; i < count; i++) { - getVerticalGridView().getChildAt(i).setTranslationY(mAnimationTranslateY); - } - } - - mFadingStatus = fadeIn ? IN : OUT; - } - - /** - * Sets the list of rows for the fragment. - */ - @Override - public void setAdapter(ObjectAdapter adapter) { - if (getAdapter() != null) { - getAdapter().unregisterObserver(mObserver); - } - super.setAdapter(adapter); - if (adapter != null) { - adapter.registerObserver(mObserver); - } - } - - @Override - protected void setupPresenter(Presenter rowPresenter) { - if (rowPresenter instanceof PlaybackRowPresenter) { - if (rowPresenter.getFacet(ItemAlignmentFacet.class) == null) { - ItemAlignmentFacet itemAlignment = new ItemAlignmentFacet(); - ItemAlignmentFacet.ItemAlignmentDef def = - new ItemAlignmentFacet.ItemAlignmentDef(); - def.setItemAlignmentOffset(0); - def.setItemAlignmentOffsetPercent(100); - itemAlignment.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[] - {def}); - rowPresenter.setFacet(ItemAlignmentFacet.class, itemAlignment); - } - } else { - super.setupPresenter(rowPresenter); - } - } - - @Override - void setVerticalGridViewLayout(VerticalGridView listview) { - if (listview == null) { - return; - } - - // we set the base line of alignment to -paddingBottom - listview.setWindowAlignmentOffset(-mPaddingBottom); - listview.setWindowAlignmentOffsetPercent( - VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); - - // align other rows that arent the last to center of screen, since our baseline is - // -mPaddingBottom, we need subtract that from mOtherRowsCenterToBottom. - listview.setItemAlignmentOffset(mOtherRowsCenterToBottom - mPaddingBottom); - listview.setItemAlignmentOffsetPercent(50); - - // Push last row to the bottom padding - // Padding affects alignment when last row is focused - listview.setPadding(listview.getPaddingLeft(), listview.getPaddingTop(), - listview.getPaddingRight(), mPaddingBottom); - listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mOtherRowsCenterToBottom = getResources() - .getDimensionPixelSize(R.dimen.lb_playback_other_rows_center_to_bottom); - mPaddingBottom = - getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom); - mBgDarkColor = - getResources().getColor(R.color.lb_playback_controls_background_dark); - mBgLightColor = - getResources().getColor(R.color.lb_playback_controls_background_light); - mShowTimeMs = - getResources().getInteger(R.integer.lb_playback_controls_show_time_ms); - mMajorFadeTranslateY = - getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y); - mMinorFadeTranslateY = - getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y); - - loadBgAnimator(); - loadControlRowAnimator(); - loadOtherRowAnimator(); - loadDescriptionAnimator(); - } - - /** - * Sets the background type. - * - * @param type One of BG_LIGHT, BG_DARK, or BG_NONE. - */ - public void setBackgroundType(int type) { - switch (type) { - case BG_LIGHT: - case BG_DARK: - case BG_NONE: - if (type != mBackgroundType) { - mBackgroundType = type; - updateBackground(); - } - break; - default: - throw new IllegalArgumentException("Invalid background type"); - } - } - - /** - * Returns the background type. - */ - public int getBackgroundType() { - return mBackgroundType; - } - - private void updateBackground() { - if (mRootView != null) { - int color = mBgDarkColor; - switch (mBackgroundType) { - case BG_DARK: break; - case BG_LIGHT: color = mBgLightColor; break; - case BG_NONE: color = Color.TRANSPARENT; break; - } - mRootView.setBackground(new ColorDrawable(color)); - } - } - - void updateControlsBottomSpace(ItemBridgeAdapter.ViewHolder vh) { - // Add extra space between rows 0 and 1 - if (vh == null && getVerticalGridView() != null) { - vh = (ItemBridgeAdapter.ViewHolder) - getVerticalGridView().findViewHolderForPosition(0); - } - if (vh != null && vh.getPresenter() instanceof PlaybackControlsRowPresenter) { - final int adapterSize = getAdapter() == null ? 0 : getAdapter().size(); - ((PlaybackControlsRowPresenter) vh.getPresenter()).showBottomSpace( - (PlaybackControlsRowPresenter.ViewHolder) vh.getViewHolder(), - adapterSize > 1); - } - } - - private final ItemBridgeAdapter.AdapterListener mAdapterListener = - new ItemBridgeAdapter.AdapterListener() { - @Override - public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) { - if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view); - if ((mFadingStatus == IDLE && mBgAlpha == 0) || mFadingStatus == OUT) { - if (DEBUG) Log.v(TAG, "setting alpha to 0"); - vh.getViewHolder().view.setAlpha(0); - } - if (vh.getPosition() == 0 && mResetControlsToPrimaryActionsPending) { - resetControlsToPrimaryActions(vh); - } - } - @Override - public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) { - if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view); - // Reset animation state - vh.getViewHolder().view.setAlpha(1f); - vh.getViewHolder().view.setTranslationY(0); - if (vh.getViewHolder() instanceof PlaybackControlsRowPresenter.ViewHolder) { - Presenter.ViewHolder descriptionVh = ((PlaybackControlsRowPresenter.ViewHolder) - vh.getViewHolder()).mDescriptionViewHolder; - if (descriptionVh != null) { - descriptionVh.view.setAlpha(1f); - } - } - } - @Override - public void onBind(ItemBridgeAdapter.ViewHolder vh) { - if (vh.getPosition() == 0) { - updateControlsBottomSpace(vh); - } - } - }; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - mRootView = super.onCreateView(inflater, container, savedInstanceState); - mBgAlpha = 255; - updateBackground(); - getRowsSupportFragment().setExternalAdapterListener(mAdapterListener); - return mRootView; - } - - @Override - public void onDestroyView() { - mRootView = null; - if (mHostCallback != null) { - mHostCallback.onHostDestroy(); - } - super.onDestroyView(); - } - - @Override - public void onStart() { - super.onStart(); - // Workaround problem VideoView forcing itself to focused, let controls take focus. - getRowsSupportFragment().getView().requestFocus(); - if (mHostCallback != null) { - mHostCallback.onHostStart(); - } - } - - private final DataObserver mObserver = new DataObserver() { - @Override - public void onChanged() { - updateControlsBottomSpace(null); - } - }; - - static abstract class AnimatorListener implements Animator.AnimatorListener { - ArrayList<View> mViews = new ArrayList<View>(); - ArrayList<Integer> mLayerType = new ArrayList<Integer>(); - - @Override - public void onAnimationCancel(Animator animation) { - } - @Override - public void onAnimationRepeat(Animator animation) { - } - @Override - public void onAnimationStart(Animator animation) { - getViews(mViews); - for (View view : mViews) { - mLayerType.add(view.getLayerType()); - view.setLayerType(View.LAYER_TYPE_HARDWARE, null); - } - } - @Override - public void onAnimationEnd(Animator animation) { - for (int i = 0; i < mViews.size(); i++) { - mViews.get(i).setLayerType(mLayerType.get(i), null); - } - mLayerType.clear(); - mViews.clear(); - } - abstract void getViews(ArrayList<View> views); - - }; -} diff --git a/android/support/v17/leanback/app/PlaybackSupportFragment.java b/android/support/v17/leanback/app/PlaybackSupportFragment.java index d63e72c6..a8741aba 100644 --- a/android/support/v17/leanback/app/PlaybackSupportFragment.java +++ b/android/support/v17/leanback/app/PlaybackSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from PlaybackFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2016 The Android Open Source Project * @@ -21,13 +18,14 @@ import android.animation.AnimatorInflater; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.support.v4.app.Fragment; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.animation.LogAccelerateInterpolator; import android.support.v17.leanback.animation.LogDecelerateInterpolator; @@ -48,6 +46,7 @@ import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import android.support.v17.leanback.widget.VerticalGridView; +import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.InputEvent; @@ -450,7 +449,7 @@ public class PlaybackSupportFragment extends Fragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // controls view are initially visible, make it invisible // if app has called hideControlsOverlay() before view created. diff --git a/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java b/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java index cdf3f97a..e7450947 100644 --- a/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java +++ b/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from VideoPlaybackFragmentGlueHost.java. DO NOT MODIFY. */ - /* * Copyright (C) 2016 The Android Open Source Project * diff --git a/android/support/v17/leanback/app/RowsFragment.java b/android/support/v17/leanback/app/RowsFragment.java index dd0dbede..a008ad60 100644 --- a/android/support/v17/leanback/app/RowsFragment.java +++ b/android/support/v17/leanback/app/RowsFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from RowsSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -16,6 +19,8 @@ package android.support.v17.leanback.app; import android.animation.TimeAnimator; import android.animation.TimeAnimator.TimeListener; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.BaseOnItemViewClickedListener; import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener; @@ -285,7 +290,7 @@ public class RowsFragment extends BaseRowFragment implements } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { if (DEBUG) Log.v(TAG, "onViewCreated"); super.onViewCreated(view, savedInstanceState); // Align the top edge of child with id row_content. @@ -625,6 +630,11 @@ public class RowsFragment extends BaseRowFragment implements } + /** + * The adapter that RowsFragment implements + * BrowseFragment.MainFragmentRowsAdapter. + * @see #getMainFragmentRowsAdapter(). + */ public static class MainFragmentRowsAdapter extends BrowseFragment.MainFragmentRowsAdapter<RowsFragment> { diff --git a/android/support/v17/leanback/app/RowsSupportFragment.java b/android/support/v17/leanback/app/RowsSupportFragment.java index c00f78b9..05e38130 100644 --- a/android/support/v17/leanback/app/RowsSupportFragment.java +++ b/android/support/v17/leanback/app/RowsSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from RowsFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * @@ -19,6 +16,8 @@ package android.support.v17.leanback.app; import android.animation.TimeAnimator; import android.animation.TimeAnimator.TimeListener; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.BaseOnItemViewClickedListener; import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener; @@ -288,7 +287,7 @@ public class RowsSupportFragment extends BaseRowSupportFragment implements } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { if (DEBUG) Log.v(TAG, "onViewCreated"); super.onViewCreated(view, savedInstanceState); // Align the top edge of child with id row_content. @@ -628,6 +627,11 @@ public class RowsSupportFragment extends BaseRowSupportFragment implements } + /** + * The adapter that RowsSupportFragment implements + * BrowseSupportFragment.MainFragmentRowsAdapter. + * @see #getMainFragmentRowsAdapter(). + */ public static class MainFragmentRowsAdapter extends BrowseSupportFragment.MainFragmentRowsAdapter<RowsSupportFragment> { diff --git a/android/support/v17/leanback/app/SearchFragment.java b/android/support/v17/leanback/app/SearchFragment.java index acf47454..2154ff28 100644 --- a/android/support/v17/leanback/app/SearchFragment.java +++ b/android/support/v17/leanback/app/SearchFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from SearchSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -16,7 +19,6 @@ package android.support.v17.leanback.app; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.Manifest; -import android.app.Fragment; import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.Bundle; @@ -35,6 +37,7 @@ import android.support.v17.leanback.widget.SearchBar; import android.support.v17.leanback.widget.SearchOrbView; import android.support.v17.leanback.widget.SpeechRecognitionCallback; import android.support.v17.leanback.widget.VerticalGridView; +import android.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -52,12 +55,11 @@ import java.util.List; * into a {@link RowsFragment}, in the same way that they are in a {@link * BrowseFragment}. * - * <p>If you do not supply a callback via - * {@link #setSpeechRecognitionCallback(SpeechRecognitionCallback)}, an internal speech - * recognizer will be used for which your application will need to declare + * <p>A SpeechRecognizer object will be created for which your application will need to declare * android.permission.RECORD_AUDIO in AndroidManifest file. If app's target version is >= 23 and * the device version is >= 23, a permission dialog will show first time using speech recognition. * 0 will be used as requestCode in requestPermissions() call. + * {@link #setSpeechRecognitionCallback(SpeechRecognitionCallback)} is deprecated. * </p> * <p> * Speech recognition is automatically started when fragment is created, but @@ -393,7 +395,7 @@ public class SearchFragment extends Fragment { mIsPaused = false; if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) { mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer( - FragmentUtil.getContext(this)); + FragmentUtil.getContext(SearchFragment.this)); mSearchBar.setSpeechRecognizer(mSpeechRecognizer); } if (mPendingStartRecognitionWhenPaused) { @@ -576,8 +578,11 @@ public class SearchFragment extends Fragment { /** * Sets this callback to have the fragment pass speech recognition requests - * to the activity rather than using an internal recognizer. + * to the activity rather than using a SpeechRecognizer object. + * @deprecated Launching voice recognition activity is no longer supported. App should declare + * android.permission.RECORD_AUDIO in AndroidManifest file. */ + @Deprecated public void setSpeechRecognitionCallback(SpeechRecognitionCallback callback) { mSpeechRecognitionCallback = callback; if (mSearchBar != null) { diff --git a/android/support/v17/leanback/app/SearchSupportFragment.java b/android/support/v17/leanback/app/SearchSupportFragment.java index 36b560de..ed2a6792 100644 --- a/android/support/v17/leanback/app/SearchSupportFragment.java +++ b/android/support/v17/leanback/app/SearchSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from SearchFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * @@ -19,7 +16,6 @@ package android.support.v17.leanback.app; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.Manifest; -import android.support.v4.app.Fragment; import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.Bundle; @@ -38,6 +34,7 @@ import android.support.v17.leanback.widget.SearchBar; import android.support.v17.leanback.widget.SearchOrbView; import android.support.v17.leanback.widget.SpeechRecognitionCallback; import android.support.v17.leanback.widget.VerticalGridView; +import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -55,12 +52,11 @@ import java.util.List; * into a {@link RowsSupportFragment}, in the same way that they are in a {@link * BrowseSupportFragment}. * - * <p>If you do not supply a callback via - * {@link #setSpeechRecognitionCallback(SpeechRecognitionCallback)}, an internal speech - * recognizer will be used for which your application will need to declare + * <p>A SpeechRecognizer object will be created for which your application will need to declare * android.permission.RECORD_AUDIO in AndroidManifest file. If app's target version is >= 23 and * the device version is >= 23, a permission dialog will show first time using speech recognition. * 0 will be used as requestCode in requestPermissions() call. + * {@link #setSpeechRecognitionCallback(SpeechRecognitionCallback)} is deprecated. * </p> * <p> * Speech recognition is automatically started when fragment is created, but @@ -579,8 +575,11 @@ public class SearchSupportFragment extends Fragment { /** * Sets this callback to have the fragment pass speech recognition requests - * to the activity rather than using an internal recognizer. + * to the activity rather than using a SpeechRecognizer object. + * @deprecated Launching voice recognition activity is no longer supported. App should declare + * android.permission.RECORD_AUDIO in AndroidManifest file. */ + @Deprecated public void setSpeechRecognitionCallback(SpeechRecognitionCallback callback) { mSpeechRecognitionCallback = callback; if (mSearchBar != null) { diff --git a/android/support/v17/leanback/app/VerticalGridFragment.java b/android/support/v17/leanback/app/VerticalGridFragment.java index 5cf5799e..5bc52ff5 100644 --- a/android/support/v17/leanback/app/VerticalGridFragment.java +++ b/android/support/v17/leanback/app/VerticalGridFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from VerticalGridSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2014 The Android Open Source Project * @@ -240,7 +243,7 @@ public class VerticalGridFragment extends BaseFragment { @Override protected Object createEntranceTransition() { - return TransitionHelper.loadTransition(FragmentUtil.getContext(this), + return TransitionHelper.loadTransition(FragmentUtil.getContext(VerticalGridFragment.this), R.transition.lb_vertical_grid_entrance_transition); } diff --git a/android/support/v17/leanback/app/VerticalGridSupportFragment.java b/android/support/v17/leanback/app/VerticalGridSupportFragment.java index a38bac52..4cfe981a 100644 --- a/android/support/v17/leanback/app/VerticalGridSupportFragment.java +++ b/android/support/v17/leanback/app/VerticalGridSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from VerticalGridFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2014 The Android Open Source Project * diff --git a/android/support/v17/leanback/app/VideoFragment.java b/android/support/v17/leanback/app/VideoFragment.java index 41241d0b..1b2b8d07 100644 --- a/android/support/v17/leanback/app/VideoFragment.java +++ b/android/support/v17/leanback/app/VideoFragment.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from VideoSupportFragment.java. DO NOT MODIFY. */ + /* * Copyright (C) 2016 The Android Open Source Project * @@ -38,7 +41,7 @@ public class VideoFragment extends PlaybackFragment { public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ViewGroup root = (ViewGroup) super.onCreateView(inflater, container, savedInstanceState); - mVideoSurface = (SurfaceView) LayoutInflater.from(FragmentUtil.getContext(this)).inflate( + mVideoSurface = (SurfaceView) LayoutInflater.from(FragmentUtil.getContext(VideoFragment.this)).inflate( R.layout.lb_video_surface, root, false); root.addView(mVideoSurface, 0); mVideoSurface.getHolder().addCallback(new SurfaceHolder.Callback() { diff --git a/android/support/v17/leanback/app/VideoFragmentGlueHost.java b/android/support/v17/leanback/app/VideoFragmentGlueHost.java index a64b521b..d123676f 100644 --- a/android/support/v17/leanback/app/VideoFragmentGlueHost.java +++ b/android/support/v17/leanback/app/VideoFragmentGlueHost.java @@ -1,3 +1,6 @@ +// CHECKSTYLE:OFF Generated code +/* This file is auto-generated from VideoSupportFragmentGlueHost.java. DO NOT MODIFY. */ + /* * Copyright (C) 2016 The Android Open Source Project * diff --git a/android/support/v17/leanback/app/VideoSupportFragment.java b/android/support/v17/leanback/app/VideoSupportFragment.java index 321bdbed..51003d35 100644 --- a/android/support/v17/leanback/app/VideoSupportFragment.java +++ b/android/support/v17/leanback/app/VideoSupportFragment.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from VideoFragment.java. DO NOT MODIFY. */ - /* * Copyright (C) 2016 The Android Open Source Project * diff --git a/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java b/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java index 28f919b6..66aabc41 100644 --- a/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java +++ b/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java @@ -1,6 +1,3 @@ -// CHECKSTYLE:OFF Generated code -/* This file is auto-generated from VideoVideoFragmentGlueHost.java. DO NOT MODIFY. */ - /* * Copyright (C) 2016 The Android Open Source Project * diff --git a/android/support/v17/leanback/app/package-info.java b/android/support/v17/leanback/app/package-info.java index 852a0075..b7369090 100644 --- a/android/support/v17/leanback/app/package-info.java +++ b/android/support/v17/leanback/app/package-info.java @@ -13,47 +13,54 @@ */ /** - * <p>Support classes providing high level Leanback user interface building blocks: - * fragments and helpers.</p> + * <p>Support classes providing high level Leanback user interface building blocks.</p> * <p> - * Leanback fragments are available both as platform fragments (subclassed from - * {@link android.app.Fragment android.app.Fragment}) and as support fragments (subclassed from - * {@link android.support.v4.app.Fragment android.support.v4.app.Fragment}). A few of the most + * Leanback fragments are available both as support fragments (subclassed from + * {@link android.support.v4.app.Fragment android.support.v4.app.Fragment}) and as platform + * fragments (subclassed from {@link android.app.Fragment android.app.Fragment}). A few of the most * commonly used leanback fragments are described here. * </p> * <p> - * A {@link android.support.v17.leanback.app.BrowseFragment} includes an optional “fastlane” + * A {@link android.support.v17.leanback.app.BrowseSupportFragment} by default operates in the "row" + * mode. It includes an optional “fastlane” * navigation side panel and a list of rows, with one-to-one correspondance between each header * in the fastlane and a row. The application supplies the * {@link android.support.v17.leanback.widget.ObjectAdapter} containing the list of * rows and a {@link android.support.v17.leanback.widget.PresenterSelector} of row presenters. * </p> * <p> - * A {@link android.support.v17.leanback.app.DetailsFragment} will typically consist of a large - * overview of an item at the top, + * A {@link android.support.v17.leanback.app.BrowseSupportFragment} also works in a "page" mode when + * each row of fastlane is mapped to a fragment that the app registers in + * {@link android.support.v17.leanback.app.BrowseSupportFragment#getMainFragmentRegistry()}. + * </p> + * <p> + * A {@link android.support.v17.leanback.app.DetailsSupportFragment} will typically consist of a + * large overview of an item at the top, * some actions that a user can perform, and possibly rows of additional or related items. - * The content for this fragment is specified in the same way as for the BrowseFragment, with the - * convention that the first element in the ObjectAdapter corresponds to the overview row. + * The content for this fragment is specified in the same way as for the BrowseSupportFragment, with + * the convention that the first element in the ObjectAdapter corresponds to the overview row. * The {@link android.support.v17.leanback.widget.DetailsOverviewRow} and - * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter} provide a default template - * for this row. + * {@link android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter} provide a + * default template for this row. * </p> * <p> - * A {@link android.support.v17.leanback.app.PlaybackOverlayFragment} implements standard playback - * transport controls with a Leanback - * look and feel. It is recommended to use an instance of the - * {@link android.support.v17.leanback.app.PlaybackControlGlue} with the - * PlaybackOverlayFragment. This helper implements a standard behavior for user interaction with - * the most commonly used controls such as fast forward and rewind. + * A {@link android.support.v17.leanback.app.PlaybackSupportFragment} or its subclass + * {@link android.support.v17.leanback.app.VideoSupportFragment} hosts + * {@link android.support.v17.leanback.media.PlaybackTransportControlGlue} + * or {@link android.support.v17.leanback.media.PlaybackBannerControlGlue} with a Leanback + * look and feel. It is recommended to use an instance of + * {@link android.support.v17.leanback.media.PlaybackTransportControlGlue}. + * This helper implements a standard behavior for user interaction with + * the most commonly used controls as well as video scrubbing. * </p> * <p> - * A {@link android.support.v17.leanback.app.SearchFragment} allows the developer to accept a query - * from a user and display the results + * A {@link android.support.v17.leanback.app.SearchSupportFragment} allows the developer to accept a + * query from a user and display the results * using the familiar list rows. * </p> * <p> - * A {@link android.support.v17.leanback.app.GuidedStepFragment} is used to guide the user through a - * decision or series of decisions. + * A {@link android.support.v17.leanback.app.GuidedStepSupportFragment} is used to guide the user + * through a decision or series of decisions. * </p> **/ diff --git a/android/support/v17/leanback/media/MediaControllerGlue.java b/android/support/v17/leanback/media/MediaControllerGlue.java index 730bf3a4..b8e9b745 100644 --- a/android/support/v17/leanback/media/MediaControllerGlue.java +++ b/android/support/v17/leanback/media/MediaControllerGlue.java @@ -28,7 +28,10 @@ import android.util.Log; /** * A helper class for implementing a glue layer for {@link MediaControllerCompat}. + * @deprecated Use {@link MediaControllerAdapter} with {@link PlaybackTransportControlGlue} or + * {@link PlaybackBannerControlGlue}. */ +@Deprecated public abstract class MediaControllerGlue extends PlaybackControlGlue { static final String TAG = "MediaControllerGlue"; static final boolean DEBUG = false; diff --git a/android/support/v17/leanback/media/MediaPlayerGlue.java b/android/support/v17/leanback/media/MediaPlayerGlue.java index 3a274b1c..73bca979 100644 --- a/android/support/v17/leanback/media/MediaPlayerGlue.java +++ b/android/support/v17/leanback/media/MediaPlayerGlue.java @@ -22,6 +22,7 @@ import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.os.Handler; +import android.support.annotation.RestrictTo; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.OnItemViewSelectedListener; @@ -50,7 +51,11 @@ import java.util.List; * </ul> * * @hide + * @deprecated Use {@link MediaPlayerAdapter} with {@link PlaybackTransportControlGlue} or + * {@link PlaybackBannerControlGlue}. */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@Deprecated public class MediaPlayerGlue extends PlaybackControlGlue implements OnItemViewSelectedListener { @@ -485,11 +490,6 @@ public class MediaPlayerGlue extends PlaybackControlGlue implements } @Override - public boolean isReadyForPlayback() { - return mInitialized; - } - - @Override public boolean isPrepared() { return mInitialized; } diff --git a/android/support/v17/leanback/media/PlaybackBannerControlGlue.java b/android/support/v17/leanback/media/PlaybackBannerControlGlue.java index ca424a8e..e6446323 100644 --- a/android/support/v17/leanback/media/PlaybackBannerControlGlue.java +++ b/android/support/v17/leanback/media/PlaybackBannerControlGlue.java @@ -59,6 +59,24 @@ import java.lang.annotation.RetentionPolicy; * {@link #onPlayCompleted()}. * </p> * + * Sample Code: + * <pre><code> + * public class MyVideoFragment extends VideoFragment { + * @Override + * public void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * PlaybackBannerControlGlue<MediaPlayerAdapter> playerGlue = + * new PlaybackBannerControlGlue(getActivity(), + * new MediaPlayerAdapter(getActivity())); + * playerGlue.setHost(new VideoFragmentGlueHost(this)); + * playerGlue.setSubtitle("Leanback artist"); + * playerGlue.setTitle("Leanback team at work"); + * String uriPath = "android.resource://com.example.android.leanback/raw/video"; + * playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath)); + * playerGlue.playWhenPrepared(); + * } + * } + * </code></pre> * @param <T> Type of {@link PlayerAdapter} passed in constructor. */ public class PlaybackBannerControlGlue<T extends PlayerAdapter> diff --git a/android/support/v17/leanback/media/PlaybackGlue.java b/android/support/v17/leanback/media/PlaybackGlue.java index 32d5545c..7c595739 100644 --- a/android/support/v17/leanback/media/PlaybackGlue.java +++ b/android/support/v17/leanback/media/PlaybackGlue.java @@ -49,21 +49,10 @@ public abstract class PlaybackGlue { */ public abstract static class PlayerCallback { /** - * This method is fired when media is ready for playback {@link #isPrepared()}. - * @deprecated use {@link #onPreparedStateChanged(PlaybackGlue)}. - */ - @Deprecated - public void onReadyForPlayback() { - } - - /** * Event for {@link #isPrepared()} changed. * @param glue The PlaybackGlue that has changed {@link #isPrepared()}. */ public void onPreparedStateChanged(PlaybackGlue glue) { - if (glue.isPrepared()) { - onReadyForPlayback(); - } } /** @@ -98,41 +87,12 @@ public abstract class PlaybackGlue { } /** - * Returns true when the media player is ready to start media playback. Subclasses must - * implement this method correctly. When returning false, app may listen to - * {@link PlayerCallback#onReadyForPlayback()} event. - * - * @see PlayerCallback#onReadyForPlayback() - * @deprecated Use isPrepared() instead. - */ - @Deprecated - public boolean isReadyForPlayback() { - return true; - } - - /** * Returns true when the media player is prepared to start media playback. When returning false, * app may listen to {@link PlayerCallback#onPreparedStateChanged(PlaybackGlue)} event. * @return True if prepared, false otherwise. */ public boolean isPrepared() { - return isReadyForPlayback(); - } - - /** - * Sets the {@link PlayerCallback} callback. It will reset the existing callbacks. - * In most cases you would call {@link #addPlayerCallback(PlayerCallback)}. - * @deprecated Use {@link #addPlayerCallback(PlayerCallback)}. - */ - @Deprecated - public void setPlayerCallback(PlayerCallback playerCallback) { - if (playerCallback == null) { - if (mPlayerCallbacks != null) { - mPlayerCallbacks.clear(); - } - } else { - addPlayerCallback(playerCallback); - } + return true; } /** @@ -174,12 +134,32 @@ public abstract class PlaybackGlue { } /** - * Starts the media player. + * Starts the media player. Does nothing if {@link #isPrepared()} is false. To wait + * {@link #isPrepared()} to be true before playing, use {@link #playWhenPrepared()}. */ public void play() { } /** + * Starts play when {@link #isPrepared()} becomes true. + */ + public void playWhenPrepared() { + if (isPrepared()) { + play(); + } else { + addPlayerCallback(new PlayerCallback() { + @Override + public void onPreparedStateChanged(PlaybackGlue glue) { + if (glue.isPrepared()) { + removePlayerCallback(this); + play(); + } + } + }); + } + } + + /** * Pauses the media player. */ public void pause() { diff --git a/android/support/v17/leanback/media/PlaybackTransportControlGlue.java b/android/support/v17/leanback/media/PlaybackTransportControlGlue.java index 61ea52ba..4aa9bf66 100644 --- a/android/support/v17/leanback/media/PlaybackTransportControlGlue.java +++ b/android/support/v17/leanback/media/PlaybackTransportControlGlue.java @@ -68,23 +68,15 @@ import java.lang.ref.WeakReference; * @Override * public void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); - * final PlaybackTransportControlGlue<MediaPlayerAdapter> playerGlue = + * PlaybackTransportControlGlue<MediaPlayerAdapter> playerGlue = * new PlaybackTransportControlGlue(getActivity(), * new MediaPlayerAdapter(getActivity())); * playerGlue.setHost(new VideoFragmentGlueHost(this)); - * playerGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() { - * @Override - * public void onPreparedStateChanged(PlaybackGlue glue) { - * if (glue.isPrepared()) { - * playerGlue.setSeekProvider(new MySeekProvider()); - * playerGlue.play(); - * } - * } - * }); * playerGlue.setSubtitle("Leanback artist"); * playerGlue.setTitle("Leanback team at work"); * String uriPath = "android.resource://com.example.android.leanback/raw/video"; * playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath)); + * playerGlue.playWhenPrepared(); * } * } * </code></pre> diff --git a/android/support/v17/leanback/package-info.java b/android/support/v17/leanback/package-info.java index aa648277..80b26e92 100644 --- a/android/support/v17/leanback/package-info.java +++ b/android/support/v17/leanback/package-info.java @@ -41,19 +41,20 @@ * <p> * Leanback contains a mixture of higher level building blocks such as Fragments in the * {@link android.support.v17.leanback.app} package. Notable examples are the - * {@link android.support.v17.leanback.app.BrowseFragment} and the - * {@link android.support.v17.leanback.app.GuidedStepFragment}. Helper classes are also provided - * that work with the leanback fragments, for example the - * {@link android.support.v17.leanback.app.PlaybackControlGlue}. + * {@link android.support.v17.leanback.app.BrowseSupportFragment}, + * {@link android.support.v17.leanback.app.DetailsSupportFragment}, + * {@link android.support.v17.leanback.app.PlaybackSupportFragment} and the + * {@link android.support.v17.leanback.app.GuidedStepSupportFragment}. Helper classes are also + * provided that work with the leanback fragments, for example the + * {@link android.support.v17.leanback.media.PlaybackTransportControlGlue} and + * {@link android.support.v17.leanback.app.PlaybackSupportFragmentGlueHost}. * </p> * <p> * Many lower level building blocks are also provided in the {@link android.support.v17.leanback.widget} package. * These allow applications to easily incorporate Leanback look and feel while allowing for a * high degree of customization. Primary examples include the UI widget * {@link android.support.v17.leanback.widget.HorizontalGridView} and - * {@link android.support.v17.leanback.widget.VerticalGridView}. Helper classes also exist at this level - * which do not depend on the leanback fragments, for example the - * {@link android.support.v17.leanback.widget.TitleHelper}. + * {@link android.support.v17.leanback.widget.VerticalGridView}. */ package android.support.v17.leanback;
\ No newline at end of file diff --git a/android/support/v17/leanback/widget/ActionPresenterSelector.java b/android/support/v17/leanback/widget/ActionPresenterSelector.java index 1ced4d4b..a018c2e2 100644 --- a/android/support/v17/leanback/widget/ActionPresenterSelector.java +++ b/android/support/v17/leanback/widget/ActionPresenterSelector.java @@ -55,7 +55,41 @@ class ActionPresenterSelector extends PresenterSelector { } } - static class OneLineActionPresenter extends Presenter { + abstract static class ActionPresenter extends Presenter { + @Override + public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + Action action = (Action) item; + ActionViewHolder vh = (ActionViewHolder) viewHolder; + vh.mAction = action; + Drawable icon = action.getIcon(); + if (icon != null) { + final int startPadding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start); + final int endPadding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end); + vh.view.setPaddingRelative(startPadding, 0, endPadding, 0); + } else { + final int padding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); + vh.view.setPaddingRelative(padding, 0, padding, 0); + } + if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) { + vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null); + } else { + vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); + } + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { + ActionViewHolder vh = (ActionViewHolder) viewHolder; + vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + vh.view.setPadding(0, 0, 0, 0); + vh.mAction = null; + } + } + + static class OneLineActionPresenter extends ActionPresenter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { View v = LayoutInflater.from(parent.getContext()) @@ -65,19 +99,14 @@ class ActionPresenterSelector extends PresenterSelector { @Override public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + super.onBindViewHolder(viewHolder, item); + ActionViewHolder vh = ((ActionViewHolder) viewHolder); Action action = (Action) item; - ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mAction = action; vh.mButton.setText(action.getLabel1()); } - - @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { - ((ActionViewHolder) viewHolder).mAction = null; - } } - static class TwoLineActionPresenter extends Presenter { + static class TwoLineActionPresenter extends ActionPresenter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { View v = LayoutInflater.from(parent.getContext()) @@ -87,27 +116,9 @@ class ActionPresenterSelector extends PresenterSelector { @Override public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + super.onBindViewHolder(viewHolder, item); Action action = (Action) item; ActionViewHolder vh = (ActionViewHolder) viewHolder; - Drawable icon = action.getIcon(); - vh.mAction = action; - - if (icon != null) { - final int startPadding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start); - final int endPadding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end); - vh.view.setPaddingRelative(startPadding, 0, endPadding, 0); - } else { - final int padding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); - vh.view.setPaddingRelative(padding, 0, padding, 0); - } - if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null); - } else { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); - } CharSequence line1 = action.getLabel1(); CharSequence line2 = action.getLabel2(); @@ -119,13 +130,5 @@ class ActionPresenterSelector extends PresenterSelector { vh.mButton.setText(line1 + "\n" + line2); } } - - @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { - ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - vh.view.setPadding(0, 0, 0, 0); - vh.mAction = null; - } } } diff --git a/android/support/v17/leanback/widget/ArrayObjectAdapter.java b/android/support/v17/leanback/widget/ArrayObjectAdapter.java index 88de24cb..00bc073d 100644 --- a/android/support/v17/leanback/widget/ArrayObjectAdapter.java +++ b/android/support/v17/leanback/widget/ArrayObjectAdapter.java @@ -230,10 +230,17 @@ public class ArrayObjectAdapter extends ObjectAdapter { * specified position. * * @param itemList List of new Items - * @param callback DiffCallback Object to compute the difference between the old data set and - * new data set. + * @param callback Optional DiffCallback Object to compute the difference between the old data + * set and new data set. When null, {@link #notifyChanged()} will be fired. */ public void setItems(final List itemList, final DiffCallback callback) { + if (callback == null) { + // shortcut when DiffCallback is not provided + mItems.clear(); + mItems.addAll(itemList); + notifyChanged(); + return; + } mOldItems.clear(); mOldItems.addAll(mItems); diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java index 8b0c4a73..81431972 100644 --- a/android/support/v17/leanback/widget/GridLayoutManager.java +++ b/android/support/v17/leanback/widget/GridLayoutManager.java @@ -2348,21 +2348,22 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { // scroll in main direction may add/prune views private int scrollDirectionPrimary(int da) { if (TRACE) TraceCompat.beginSection("scrollPrimary"); - boolean isMaxUnknown = false, isMinUnknown = false; - int minScroll = 0, maxScroll = 0; - if (!mIsSlidingChildViews) { + // We apply the cap of maxScroll/minScroll to the delta, except for two cases: + // 1. when children are in sliding out mode + // 2. During onLayoutChildren(), it may compensate the remaining scroll delta, + // we should honor the request regardless if it goes over minScroll / maxScroll. + // (see b/64931938 testScrollAndRemove and testScrollAndRemoveSample1) + if (!mIsSlidingChildViews && !mInLayout) { if (da > 0) { - isMaxUnknown = mWindowAlignment.mainAxis().isMaxUnknown(); - if (!isMaxUnknown) { - maxScroll = mWindowAlignment.mainAxis().getMaxScroll(); + if (!mWindowAlignment.mainAxis().isMaxUnknown()) { + int maxScroll = mWindowAlignment.mainAxis().getMaxScroll(); if (da > maxScroll) { da = maxScroll; } } } else if (da < 0) { - isMinUnknown = mWindowAlignment.mainAxis().isMinUnknown(); - if (!isMinUnknown) { - minScroll = mWindowAlignment.mainAxis().getMinScroll(); + if (!mWindowAlignment.mainAxis().isMinUnknown()) { + int minScroll = mWindowAlignment.mainAxis().getMinScroll(); if (da < minScroll) { da = minScroll; } @@ -2856,7 +2857,8 @@ final class GridLayoutManager extends RecyclerView.LayoutManager { if (!mScrollEnabled && smooth) { return; } - if (getScrollPosition(view, childView, sTwoInts)) { + if (getScrollPosition(view, childView, sTwoInts) + || extraDelta != 0 || extraDeltaSecondary != 0) { scrollGrid(sTwoInts[0] + extraDelta, sTwoInts[1] + extraDeltaSecondary, smooth); } } diff --git a/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java b/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java index 82cfa799..000db3c4 100644 --- a/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java +++ b/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java @@ -32,7 +32,7 @@ import android.widget.LinearLayout; /** * A PlaybackControlsRowPresenter renders a {@link PlaybackControlsRow} to display a * series of playback control buttons. Typically this row will be the first row in a fragment - * such as the {@link android.support.v17.leanback.app.PlaybackOverlayFragment}. + * such as the {@link android.support.v17.leanback.app.PlaybackFragment}. * * <p>The detailed description is rendered using a {@link Presenter} passed in * {@link #PlaybackControlsRowPresenter(Presenter)}. Typically this will be an instance of diff --git a/android/support/v17/leanback/widget/SearchBar.java b/android/support/v17/leanback/widget/SearchBar.java index 18f608e2..1094343f 100644 --- a/android/support/v17/leanback/widget/SearchBar.java +++ b/android/support/v17/leanback/widget/SearchBar.java @@ -116,14 +116,6 @@ public class SearchBar extends RelativeLayout { } - private AudioManager.OnAudioFocusChangeListener mAudioFocusChangeListener = - new AudioManager.OnAudioFocusChangeListener() { - @Override - public void onAudioFocusChange(int focusChange) { - stopRecognition(); - } - }; - SearchBarListener mSearchBarListener; SearchEditText mSearchTextEditor; SpeechOrbView mSpeechOrbView; @@ -495,7 +487,12 @@ public class SearchBar extends RelativeLayout { /** * Sets the speech recognition callback. + * + * @deprecated Launching voice recognition activity is no longer supported. App should declare + * android.permission.RECORD_AUDIO in AndroidManifest file. See details in + * {@link android.support.v17.leanback.app.SearchSupportFragment}. */ + @Deprecated public void setSpeechRecognitionCallback(SpeechRecognitionCallback request) { mSpeechRecognitionCallback = request; if (mSpeechRecognitionCallback != null && mSpeechRecognizer != null) { @@ -582,7 +579,6 @@ public class SearchBar extends RelativeLayout { if (mListening) { mSpeechRecognizer.cancel(); mListening = false; - mAudioManager.abandonAudioFocus(mAudioFocusChangeListener); } mSpeechRecognizer.setRecognitionListener(null); @@ -624,17 +620,6 @@ public class SearchBar extends RelativeLayout { } mRecognizing = true; - // Request audio focus - int result = mAudioManager.requestAudioFocus(mAudioFocusChangeListener, - // Use the music stream. - AudioManager.STREAM_MUSIC, - // Request exclusive transient focus. - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); - - - if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Log.w(TAG, "Could not get audio focus"); - } mSearchTextEditor.setText(""); diff --git a/android/support/v17/leanback/widget/SpeechRecognitionCallback.java b/android/support/v17/leanback/widget/SpeechRecognitionCallback.java index 02b0990e..173444d8 100644 --- a/android/support/v17/leanback/widget/SpeechRecognitionCallback.java +++ b/android/support/v17/leanback/widget/SpeechRecognitionCallback.java @@ -15,7 +15,12 @@ package android.support.v17.leanback.widget; /** * Interface for receiving notification that speech recognition should be initiated. + * + * @deprecated Launching voice recognition activity is no longer supported. App should declare + * android.permission.RECORD_AUDIO in AndroidManifest file. See details in + * {@link android.support.v17.leanback.app.SearchSupportFragment}. */ +@Deprecated public interface SpeechRecognitionCallback { /** * Method invoked when speech recognition should be initiated. diff --git a/android/support/v17/leanback/widget/WindowAlignment.java b/android/support/v17/leanback/widget/WindowAlignment.java index 3ddb6f0d..55fa7589 100644 --- a/android/support/v17/leanback/widget/WindowAlignment.java +++ b/android/support/v17/leanback/widget/WindowAlignment.java @@ -261,20 +261,18 @@ class WindowAlignment { // minScroll mMinScroll = Math.min(mMinScroll, calculateScrollToKeyLine(maxChildViewCenter, keyLine)); - } else { - // don't over scroll max - mMaxScroll = Math.max(mMinScroll, mMaxScroll); } + // don't over scroll max + mMaxScroll = Math.max(mMinScroll, mMaxScroll); } else if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) { if (isPreferKeylineOverHighEdge()) { // if we prefer key line, might align min child to key line for // maxScroll mMaxScroll = Math.max(mMaxScroll, calculateScrollToKeyLine(minChildViewCenter, keyLine)); - } else { - // don't over scroll min - mMinScroll = Math.min(mMinScroll, mMaxScroll); } + // don't over scroll min + mMinScroll = Math.min(mMinScroll, mMaxScroll); } } else { if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) { @@ -283,20 +281,18 @@ class WindowAlignment { // maxScroll mMaxScroll = Math.max(mMaxScroll, calculateScrollToKeyLine(minChildViewCenter, keyLine)); - } else { - // don't over scroll min - mMinScroll = Math.min(mMinScroll, mMaxScroll); } + // don't over scroll min + mMinScroll = Math.min(mMinScroll, mMaxScroll); } else if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) { if (isPreferKeylineOverHighEdge()) { // if we prefer key line, might align max child to key line for // minScroll mMinScroll = Math.min(mMinScroll, calculateScrollToKeyLine(maxChildViewCenter, keyLine)); - } else { - // don't over scroll max - mMaxScroll = Math.max(mMinScroll, mMaxScroll); } + // don't over scroll max + mMaxScroll = Math.max(mMinScroll, mMaxScroll); } } } diff --git a/android/support/v17/preference/LeanbackPreferenceFragment.java b/android/support/v17/preference/LeanbackPreferenceFragment.java index dbff1c85..48d14b83 100644 --- a/android/support/v17/preference/LeanbackPreferenceFragment.java +++ b/android/support/v17/preference/LeanbackPreferenceFragment.java @@ -30,12 +30,12 @@ import android.widget.TextView; * <p>The following sample code shows a simple leanback preference fragment that is * populated from a resource. The resource it loads is:</p> * - * {@sample frameworks/support/samples/SupportPreferenceDemos/res/xml/preferences.xml preferences} + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml preferences} * * <p>The fragment needs only to implement {@link #onCreatePreferences(Bundle, String)} to populate * the list of preference objects:</p> * - * {@sample frameworks/support/samples/SupportPreferenceDemos/src/com/example/android/supportpreference/FragmentSupportPreferencesLeanback.java + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesLeanback.java * support_fragment_leanback} */ public abstract class LeanbackPreferenceFragment extends BaseLeanbackPreferenceFragment { diff --git a/android/support/v17/preference/LeanbackSettingsFragment.java b/android/support/v17/preference/LeanbackSettingsFragment.java index 08f19c47..d56a2a63 100644 --- a/android/support/v17/preference/LeanbackSettingsFragment.java +++ b/android/support/v17/preference/LeanbackSettingsFragment.java @@ -42,14 +42,14 @@ import android.widget.Space; * <p>The following sample code shows a simple leanback preference fragment that is * populated from a resource. The resource it loads is:</p> * - * {@sample frameworks/support/samples/SupportPreferenceDemos/res/xml/preferences.xml preferences} + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml preferences} * * <p>The sample implements * {@link PreferenceFragment.OnPreferenceStartFragmentCallback#onPreferenceStartFragment(PreferenceFragment, Preference)}, * {@link PreferenceFragment.OnPreferenceStartScreenCallback#onPreferenceStartScreen(PreferenceFragment, PreferenceScreen)}, * and {@link #onPreferenceStartInitialScreen()}:</p> * - * {@sample frameworks/support/samples/SupportPreferenceDemos/src/com/example/android/supportpreference/FragmentSupportPreferencesLeanback.java + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesLeanback.java * support_fragment_leanback} */ public abstract class LeanbackSettingsFragment extends Fragment diff --git a/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java b/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java index 3905ca55..0dcd9029 100644 --- a/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java +++ b/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java @@ -20,6 +20,8 @@ import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityServiceInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.View; /** @@ -188,8 +190,9 @@ public final class AccessibilityServiceInfoCompat { * @param packageManager The current package manager * @return The localized description. */ + @Nullable public static String loadDescription( - AccessibilityServiceInfo info, PackageManager packageManager) { + @NonNull AccessibilityServiceInfo info, @NonNull PackageManager packageManager) { if (Build.VERSION.SDK_INT >= 16) { return info.loadDescription(packageManager); } else { @@ -206,6 +209,7 @@ public final class AccessibilityServiceInfoCompat { * @param feedbackType The feedback type. * @return The string representation. */ + @NonNull public static String feedbackTypeToString(int feedbackType) { StringBuilder builder = new StringBuilder(); builder.append("["); @@ -245,6 +249,7 @@ public final class AccessibilityServiceInfoCompat { * @param flag The flag. * @return The string representation. */ + @Nullable public static String flagToString(int flag) { switch (flag) { case AccessibilityServiceInfo.DEFAULT: @@ -276,7 +281,7 @@ public final class AccessibilityServiceInfoCompat { * @see #CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY * @see #CAPABILITY_CAN_FILTER_KEY_EVENTS */ - public static int getCapabilities(AccessibilityServiceInfo info) { + public static int getCapabilities(@NonNull AccessibilityServiceInfo info) { if (Build.VERSION.SDK_INT >= 18) { return info.getCapabilities(); } else { @@ -296,6 +301,7 @@ public final class AccessibilityServiceInfoCompat { * @param capability The capability. * @return The string representation. */ + @NonNull public static String capabilityToString(int capability) { switch (capability) { case CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT: diff --git a/android/support/v4/app/ActivityCompat.java b/android/support/v4/app/ActivityCompat.java index f260508c..5833481a 100644 --- a/android/support/v4/app/ActivityCompat.java +++ b/android/support/v4/app/ActivityCompat.java @@ -74,6 +74,61 @@ public class ActivityCompat extends ContextCompat { } /** + * Customizable delegate that allows delegating permission compatibility methods to a custom + * implementation. + * + * <p> + * To delegate permission compatibility methods to a custom class, implement this interface, + * and call {@code ActivityCompat.setPermissionCompatDelegate(delegate);}. All future calls + * to the permission compatibility methods in this class will first check whether the + * delegate can handle the method call, and invoke the corresponding method if it can. + * </p> + */ + public interface PermissionCompatDelegate { + + /** + * Determines whether the delegate should handle + * {@link ActivityCompat#requestPermissions(Activity, String[], int)}, and request + * permissions if applicable. If this method returns true, it means that permission + * request is successfully handled by the delegate, and platform should not perform any + * further requests for permission. + * + * @param activity The target activity. + * @param permissions The requested permissions. Must me non-null and not empty. + * @param requestCode Application specific request code to match with a result reported to + * {@link + * OnRequestPermissionsResultCallback#onRequestPermissionsResult(int, String[], int[])}. + * Should be >= 0. + * + * @return Whether the delegate has handled the permission request. + * @see ActivityCompat#requestPermissions(Activity, String[], int) + */ + boolean requestPermissions(@NonNull Activity activity, + @NonNull String[] permissions, @IntRange(from = 0) int requestCode); + + /** + * Determines whether the delegate should handle the permission request as part of + * {@code FragmentActivity#onActivityResult(int, int, Intent)}. If this method returns true, + * it means that activity result is successfully handled by the delegate, and no further + * action is needed on this activity result. + * + * @param activity The target Activity. + * @param requestCode The integer request code originally supplied to + * {@code startActivityForResult()}, allowing you to identify who this + * result came from. + * @param resultCode The integer result code returned by the child activity + * through its {@code }setResult()}. + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + * + * @return Whether the delegate has handled the activity result. + * @see ActivityCompat#requestPermissions(Activity, String[], int) + */ + boolean onActivityResult(@NonNull Activity activity, + @IntRange(from = 0) int requestCode, int resultCode, @Nullable Intent data); + } + + /** * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -81,6 +136,8 @@ public class ActivityCompat extends ContextCompat { void validateRequestPermissionsRequestCode(int requestCode); } + private static PermissionCompatDelegate sDelegate; + /** * This class should not be instantiated, but the constructor must be * visible for the class to be extended (as in support-v13). @@ -90,6 +147,25 @@ public class ActivityCompat extends ContextCompat { } /** + * Sets the permission delegate for {@code ActivityCompat}. Replaces the previously set + * delegate. + * + * @param delegate The delegate to be set. {@code null} to clear the set delegate. + */ + public static void setPermissionCompatDelegate( + @Nullable PermissionCompatDelegate delegate) { + sDelegate = delegate; + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static PermissionCompatDelegate getPermissionCompatDelegate() { + return sDelegate; + } + + /** * Invalidate the activity's options menu, if able. * * <p>Before API level 11 (Android 3.0/Honeycomb) the lifecycle of the @@ -120,7 +196,9 @@ public class ActivityCompat extends ContextCompat { * * @param activity Invalidate the options menu of this activity * @return true if this operation was supported and it completed; false if it was not available. + * @deprecated Use {@link Activity#invalidateOptionsMenu()} directly. */ + @Deprecated public static boolean invalidateOptionsMenu(Activity activity) { activity.invalidateOptionsMenu(); return true; @@ -146,8 +224,8 @@ public class ActivityCompat extends ContextCompat { * supplied here; there are no supported definitions for * building it manually. */ - public static void startActivityForResult(Activity activity, Intent intent, int requestCode, - @Nullable Bundle options) { + public static void startActivityForResult(@NonNull Activity activity, @NonNull Intent intent, + int requestCode, @Nullable Bundle options) { if (Build.VERSION.SDK_INT >= 16) { activity.startActivityForResult(intent, requestCode, options); } else { @@ -181,9 +259,10 @@ public class ActivityCompat extends ContextCompat { * supplied here; there are no supported definitions for * building it manually. */ - public static void startIntentSenderForResult(Activity activity, IntentSender intent, - int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, - int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException { + public static void startIntentSenderForResult(@NonNull Activity activity, + @NonNull IntentSender intent, int requestCode, @Nullable Intent fillInIntent, + int flagsMask, int flagsValues, int extraFlags, @Nullable Bundle options) + throws IntentSender.SendIntentException { if (Build.VERSION.SDK_INT >= 16) { activity.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options); @@ -200,7 +279,7 @@ public class ActivityCompat extends ContextCompat { * <p>On Android 4.1+ calling this method will call through to the native version of this * method. For other platforms {@link Activity#finish()} will be called instead.</p> */ - public static void finishAffinity(Activity activity) { + public static void finishAffinity(@NonNull Activity activity) { if (Build.VERSION.SDK_INT >= 16) { activity.finishAffinity(); } else { @@ -217,7 +296,7 @@ public class ActivityCompat extends ContextCompat { * <p>On Android 4.4 or lower, this method only finishes the Activity with no * special exit transition.</p> */ - public static void finishAfterTransition(Activity activity) { + public static void finishAfterTransition(@NonNull Activity activity) { if (Build.VERSION.SDK_INT >= 21) { activity.finishAfterTransition(); } else { @@ -242,7 +321,7 @@ public class ActivityCompat extends ContextCompat { * referrer information, applications can spoof it.</p> */ @Nullable - public static Uri getReferrer(Activity activity) { + public static Uri getReferrer(@NonNull Activity activity) { if (Build.VERSION.SDK_INT >= 22) { return activity.getReferrer(); } @@ -266,8 +345,8 @@ public class ActivityCompat extends ContextCompat { * * @param callback Used to manipulate shared element transitions on the launched Activity. */ - public static void setEnterSharedElementCallback(Activity activity, - SharedElementCallback callback) { + public static void setEnterSharedElementCallback(@NonNull Activity activity, + @Nullable SharedElementCallback callback) { if (Build.VERSION.SDK_INT >= 23) { android.app.SharedElementCallback frameworkCallback = callback != null ? new SharedElementCallback23Impl(callback) @@ -290,8 +369,8 @@ public class ActivityCompat extends ContextCompat { * * @param callback Used to manipulate shared element transitions on the launching Activity. */ - public static void setExitSharedElementCallback(Activity activity, - SharedElementCallback callback) { + public static void setExitSharedElementCallback(@NonNull Activity activity, + @Nullable SharedElementCallback callback) { if (Build.VERSION.SDK_INT >= 23) { android.app.SharedElementCallback frameworkCallback = callback != null ? new SharedElementCallback23Impl(callback) @@ -305,13 +384,13 @@ public class ActivityCompat extends ContextCompat { } } - public static void postponeEnterTransition(Activity activity) { + public static void postponeEnterTransition(@NonNull Activity activity) { if (Build.VERSION.SDK_INT >= 21) { activity.postponeEnterTransition(); } } - public static void startPostponedEnterTransition(Activity activity) { + public static void startPostponedEnterTransition(@NonNull Activity activity) { if (Build.VERSION.SDK_INT >= 21) { activity.startPostponedEnterTransition(); } @@ -386,6 +465,12 @@ public class ActivityCompat extends ContextCompat { */ public static void requestPermissions(final @NonNull Activity activity, final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) { + if (sDelegate != null + && sDelegate.requestPermissions(activity, permissions, requestCode)) { + // Delegate has handled the permission request. + return; + } + if (Build.VERSION.SDK_INT >= 23) { if (activity instanceof RequestPermissionsRequestCodeValidator) { ((RequestPermissionsRequestCodeValidator) activity) diff --git a/android/support/v4/app/ActivityOptionsCompat.java b/android/support/v4/app/ActivityOptionsCompat.java index 7b5916f5..66768058 100644 --- a/android/support/v4/app/ActivityOptionsCompat.java +++ b/android/support/v4/app/ActivityOptionsCompat.java @@ -24,6 +24,7 @@ import android.graphics.Bitmap; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v4.util.Pair; @@ -60,7 +61,8 @@ public class ActivityOptionsCompat { * @return Returns a new ActivityOptions object that you can use to supply * these options as the options Bundle when starting an activity. */ - public static ActivityOptionsCompat makeCustomAnimation(Context context, + @NonNull + public static ActivityOptionsCompat makeCustomAnimation(@NonNull Context context, int enterResId, int exitResId) { if (Build.VERSION.SDK_INT >= 16) { return createImpl(ActivityOptions.makeCustomAnimation(context, enterResId, exitResId)); @@ -88,7 +90,8 @@ public class ActivityOptionsCompat { * @return Returns a new ActivityOptions object that you can use to supply * these options as the options Bundle when starting an activity. */ - public static ActivityOptionsCompat makeScaleUpAnimation(View source, + @NonNull + public static ActivityOptionsCompat makeScaleUpAnimation(@NonNull View source, int startX, int startY, int startWidth, int startHeight) { if (Build.VERSION.SDK_INT >= 16) { return createImpl(ActivityOptions.makeScaleUpAnimation( @@ -111,7 +114,8 @@ public class ActivityOptionsCompat { * @return Returns a new ActivityOptions object that you can use to * supply these options as the options Bundle when starting an activity. */ - public static ActivityOptionsCompat makeClipRevealAnimation(View source, + @NonNull + public static ActivityOptionsCompat makeClipRevealAnimation(@NonNull View source, int startX, int startY, int width, int height) { if (Build.VERSION.SDK_INT >= 23) { return createImpl(ActivityOptions.makeClipRevealAnimation( @@ -139,8 +143,9 @@ public class ActivityOptionsCompat { * @return Returns a new ActivityOptions object that you can use to supply * these options as the options Bundle when starting an activity. */ - public static ActivityOptionsCompat makeThumbnailScaleUpAnimation(View source, - Bitmap thumbnail, int startX, int startY) { + @NonNull + public static ActivityOptionsCompat makeThumbnailScaleUpAnimation(@NonNull View source, + @NonNull Bitmap thumbnail, int startX, int startY) { if (Build.VERSION.SDK_INT >= 16) { return createImpl(ActivityOptions.makeThumbnailScaleUpAnimation( source, thumbnail, startX, startY)); @@ -166,8 +171,9 @@ public class ActivityOptionsCompat { * @return Returns a new ActivityOptions object that you can use to * supply these options as the options Bundle when starting an activity. */ - public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity, - View sharedElement, String sharedElementName) { + @NonNull + public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity, + @NonNull View sharedElement, @NonNull String sharedElementName) { if (Build.VERSION.SDK_INT >= 21) { return createImpl(ActivityOptions.makeSceneTransitionAnimation( activity, sharedElement, sharedElementName)); @@ -192,8 +198,9 @@ public class ActivityOptionsCompat { * @return Returns a new ActivityOptions object that you can use to * supply these options as the options Bundle when starting an activity. */ + @NonNull @SuppressWarnings("unchecked") - public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity, + public static ActivityOptionsCompat makeSceneTransitionAnimation(@NonNull Activity activity, Pair<View, String>... sharedElements) { if (Build.VERSION.SDK_INT >= 21) { android.util.Pair<View, String>[] pairs = null; @@ -219,6 +226,7 @@ public class ActivityOptionsCompat { * {@link android.R.attr#launchMode launchMode} values of * <code>singleInstance</code> or <code>singleTask</code>. */ + @NonNull public static ActivityOptionsCompat makeTaskLaunchBehind() { if (Build.VERSION.SDK_INT >= 21) { return createImpl(ActivityOptions.makeTaskLaunchBehind()); @@ -230,6 +238,7 @@ public class ActivityOptionsCompat { * Create a basic ActivityOptions that has no special animation associated with it. * Other options can still be set. */ + @NonNull public static ActivityOptionsCompat makeBasic() { if (Build.VERSION.SDK_INT >= 23) { return createImpl(ActivityOptions.makeBasic()); @@ -314,8 +323,9 @@ public class ActivityOptionsCompat { * {@link android.content.pm.PackageManager#FEATURE_PICTURE_IN_PICTURE} enabled. * @param screenSpacePixelRect Launch bounds to use for the activity or null for fullscreen. */ + @NonNull public ActivityOptionsCompat setLaunchBounds(@Nullable Rect screenSpacePixelRect) { - return null; + return this; } /** @@ -335,6 +345,7 @@ public class ActivityOptionsCompat { * object; you must not modify it, but can supply it to the startActivity * methods that take an options Bundle. */ + @Nullable public Bundle toBundle() { return null; } @@ -344,7 +355,7 @@ public class ActivityOptionsCompat { * otherOptions. Any values defined in otherOptions replace those in the * base options. */ - public void update(ActivityOptionsCompat otherOptions) { + public void update(@NonNull ActivityOptionsCompat otherOptions) { // Do nothing. } @@ -372,7 +383,7 @@ public class ActivityOptionsCompat { * * @param receiver A broadcast receiver that will receive the report. */ - public void requestUsageTimeReport(PendingIntent receiver) { + public void requestUsageTimeReport(@NonNull PendingIntent receiver) { // Do nothing. } } diff --git a/android/support/v4/app/AlarmManagerCompat.java b/android/support/v4/app/AlarmManagerCompat.java index 5a4582ba..a297cb5a 100644 --- a/android/support/v4/app/AlarmManagerCompat.java +++ b/android/support/v4/app/AlarmManagerCompat.java @@ -19,6 +19,7 @@ package android.support.v4.app; import android.app.AlarmManager; import android.app.PendingIntent; import android.os.Build; +import android.support.annotation.NonNull; /** * Compatibility library for {@link AlarmManager} with fallbacks for older platforms. @@ -52,8 +53,8 @@ public final class AlarmManagerCompat { * @see android.content.Context#registerReceiver * @see android.content.Intent#filterEquals */ - public static void setAlarmClock(AlarmManager alarmManager, long triggerTime, - PendingIntent showIntent, PendingIntent operation) { + public static void setAlarmClock(@NonNull AlarmManager alarmManager, long triggerTime, + @NonNull PendingIntent showIntent, @NonNull PendingIntent operation) { if (Build.VERSION.SDK_INT >= 21) { alarmManager.setAlarmClock(new AlarmManager.AlarmClockInfo(triggerTime, showIntent), operation); @@ -110,8 +111,8 @@ public final class AlarmManagerCompat { * @see AlarmManager#RTC * @see AlarmManager#RTC_WAKEUP */ - public static void setAndAllowWhileIdle(AlarmManager alarmManager, int type, - long triggerAtMillis, PendingIntent operation) { + public static void setAndAllowWhileIdle(@NonNull AlarmManager alarmManager, int type, + long triggerAtMillis, @NonNull PendingIntent operation) { if (Build.VERSION.SDK_INT >= 23) { alarmManager.setAndAllowWhileIdle(type, triggerAtMillis, operation); } else { @@ -155,8 +156,8 @@ public final class AlarmManagerCompat { * @see AlarmManager#RTC * @see AlarmManager#RTC_WAKEUP */ - public static void setExact(AlarmManager alarmManager, int type, long triggerAtMillis, - PendingIntent operation) { + public static void setExact(@NonNull AlarmManager alarmManager, int type, long triggerAtMillis, + @NonNull PendingIntent operation) { if (Build.VERSION.SDK_INT >= 19) { alarmManager.setExact(type, triggerAtMillis, operation); } else { @@ -215,8 +216,8 @@ public final class AlarmManagerCompat { * @see AlarmManager#RTC * @see AlarmManager#RTC_WAKEUP */ - public static void setExactAndAllowWhileIdle(AlarmManager alarmManager, int type, - long triggerAtMillis, PendingIntent operation) { + public static void setExactAndAllowWhileIdle(@NonNull AlarmManager alarmManager, int type, + long triggerAtMillis, @NonNull PendingIntent operation) { if (Build.VERSION.SDK_INT >= 23) { alarmManager.setExactAndAllowWhileIdle(type, triggerAtMillis, operation); } else { diff --git a/android/support/v4/app/AppLaunchChecker.java b/android/support/v4/app/AppLaunchChecker.java index f8beb91c..af9512ad 100644 --- a/android/support/v4/app/AppLaunchChecker.java +++ b/android/support/v4/app/AppLaunchChecker.java @@ -22,8 +22,8 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.content.IntentCompat; -import android.support.v4.content.SharedPreferencesCompat; /** * This class provides APIs for determining how an app has been launched. @@ -46,7 +46,7 @@ public class AppLaunchChecker { * @param context Context to check * @return true if this app has been started by the user from the launcher at least once */ - public static boolean hasStartedFromLauncher(Context context) { + public static boolean hasStartedFromLauncher(@NonNull Context context) { return context.getSharedPreferences(SHARED_PREFS_NAME, 0) .getBoolean(KEY_STARTED_FROM_LAUNCHER, false); } @@ -62,7 +62,7 @@ public class AppLaunchChecker { * * @param activity the Activity currently running onCreate */ - public static void onActivityCreate(Activity activity) { + public static void onActivityCreate(@NonNull Activity activity) { final SharedPreferences sp = activity.getSharedPreferences(SHARED_PREFS_NAME, 0); if (sp.getBoolean(KEY_STARTED_FROM_LAUNCHER, false)) { return; @@ -76,8 +76,7 @@ public class AppLaunchChecker { if (Intent.ACTION_MAIN.equals(launchIntent.getAction()) && (launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER) || launchIntent.hasCategory(IntentCompat.CATEGORY_LEANBACK_LAUNCHER))) { - SharedPreferencesCompat.EditorCompat.getInstance().apply( - sp.edit().putBoolean(KEY_STARTED_FROM_LAUNCHER, true)); + sp.edit().putBoolean(KEY_STARTED_FROM_LAUNCHER, true).apply(); } } } diff --git a/android/support/v4/app/AppOpsManagerCompat.java b/android/support/v4/app/AppOpsManagerCompat.java index ce2d2c6b..7e97199a 100644 --- a/android/support/v4/app/AppOpsManagerCompat.java +++ b/android/support/v4/app/AppOpsManagerCompat.java @@ -21,6 +21,7 @@ import static android.os.Build.VERSION.SDK_INT; import android.app.AppOpsManager; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; /** * Helper for accessing features in {@link android.app.AppOpsManager}. @@ -56,6 +57,7 @@ public final class AppOpsManagerCompat { * @param permission The permission. * @return The app op associated with the permission or null. */ + @Nullable public static String permissionToOp(@NonNull String permission) { if (SDK_INT >= 23) { return AppOpsManager.permissionToOp(permission); diff --git a/android/support/v4/app/BundleCompat.java b/android/support/v4/app/BundleCompat.java index e5fc3027..21d730d8 100644 --- a/android/support/v4/app/BundleCompat.java +++ b/android/support/v4/app/BundleCompat.java @@ -19,6 +19,8 @@ package android.support.v4.app; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import java.lang.reflect.InvocationTargetException; @@ -94,7 +96,8 @@ public final class BundleCompat { * @param key The key to use while getting the {@link IBinder}. * @return The {@link IBinder} that was obtained. */ - public static IBinder getBinder(Bundle bundle, String key) { + @Nullable + public static IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) { if (Build.VERSION.SDK_INT >= 18) { return bundle.getBinder(key); } else { @@ -109,7 +112,8 @@ public final class BundleCompat { * @param key The key to use while putting the {@link IBinder}. * @param binder The {@link IBinder} to put. */ - public static void putBinder(Bundle bundle, String key, IBinder binder) { + public static void putBinder(@NonNull Bundle bundle, @Nullable String key, + @Nullable IBinder binder) { if (Build.VERSION.SDK_INT >= 18) { bundle.putBinder(key, binder); } else { diff --git a/android/support/v4/app/Fragment.java b/android/support/v4/app/Fragment.java index ba74521d..e734a274 100644 --- a/android/support/v4/app/Fragment.java +++ b/android/support/v4/app/Fragment.java @@ -463,6 +463,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener /** * Get the tag name of the fragment, if specified. */ + @Nullable final public String getTag() { return mTag; } @@ -474,7 +475,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * <p>This method cannot be called if the fragment is added to a FragmentManager and * if {@link #isStateSaved()} would return true.</p> */ - public void setArguments(Bundle args) { + public void setArguments(@Nullable Bundle args) { if (mIndex >= 0 && isStateSaved()) { throw new IllegalStateException("Fragment already active and state has been saved"); } @@ -485,6 +486,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * Return the arguments supplied when the fragment was instantiated, * if any. */ + @Nullable final public Bundle getArguments() { return mArguments; } @@ -512,7 +514,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * * @param state The state the fragment should be restored from. */ - public void setInitialSavedState(SavedState state) { + public void setInitialSavedState(@Nullable SavedState state) { if (mIndex >= 0) { throw new IllegalStateException("Fragment already active"); } @@ -532,7 +534,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * are going to call back with {@link #onActivityResult(int, int, Intent)}. */ @SuppressWarnings("ReferenceEquality") - public void setTargetFragment(Fragment fragment, int requestCode) { + public void setTargetFragment(@Nullable Fragment fragment, int requestCode) { // Don't allow a caller to set a target fragment in another FragmentManager, // but there's a snag: people do set target fragments before fragments get added. // We'll have the FragmentManager check that for validity when we move @@ -558,6 +560,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener /** * Return the target fragment set by {@link #setTargetFragment}. */ + @Nullable final public Fragment getTargetFragment() { return mTarget; } @@ -572,6 +575,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener /** * Return the {@link Context} this fragment is currently associated with. */ + @Nullable public Context getContext() { return mHost == null ? null : mHost.getContext(); } @@ -581,6 +585,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * May return {@code null} if the fragment is associated with a {@link Context} * instead. */ + @Nullable final public FragmentActivity getActivity() { return mHost == null ? null : (FragmentActivity) mHost.getActivity(); } @@ -589,6 +594,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * Return the host object of this fragment. May return {@code null} if the fragment * isn't currently being hosted. */ + @Nullable final public Object getHost() { return mHost == null ? null : mHost.onGetHost(); } @@ -596,6 +602,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener /** * Return <code>getActivity().getResources()</code>. */ + @NonNull final public Resources getResources() { if (mHost == null) { throw new IllegalStateException("Fragment " + this + " not attached to Activity"); @@ -609,6 +616,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * * @param resId Resource id for the CharSequence text */ + @NonNull public final CharSequence getText(@StringRes int resId) { return getResources().getText(resId); } @@ -619,6 +627,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * * @param resId Resource id for the string */ + @NonNull public final String getString(@StringRes int resId) { return getResources().getString(resId); } @@ -631,7 +640,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @param resId Resource id for the format string * @param formatArgs The format arguments that will be used for substitution. */ - + @NonNull public final String getString(@StringRes int resId, Object... formatArgs) { return getResources().getString(resId, formatArgs); } @@ -646,6 +655,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * <p>If this Fragment is a child of another Fragment, the FragmentManager * returned here will be the parent's {@link #getChildFragmentManager()}. */ + @Nullable final public FragmentManager getFragmentManager() { return mFragmentManager; } @@ -654,6 +664,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * Return a private FragmentManager for placing and managing Fragments * inside of this Fragment. */ + @NonNull final public FragmentManager getChildFragmentManager() { if (mChildFragmentManager == null) { instantiateChildFragmentManager(); @@ -674,6 +685,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * Return this fragment's child FragmentManager one has been previously created, * otherwise null. */ + @Nullable FragmentManager peekChildFragmentManager() { return mChildFragmentManager; } @@ -682,6 +694,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * Returns the parent Fragment containing this Fragment. If this Fragment * is attached directly to an Activity, returns null. */ + @Nullable final public Fragment getParentFragment() { return mParentFragment; } @@ -1082,7 +1095,8 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * a previous saved state, this is the state. * @return The LayoutInflater used to inflate Views of this Fragment. */ - public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) { + @NonNull + public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) { // TODO: move the implementation in getLayoutInflater to here return getLayoutInflater(savedInstanceState); } @@ -1113,7 +1127,8 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * a previous saved state, this is the state. * @return The LayoutInflater used to inflate Views of this Fragment. */ - LayoutInflater performGetLayoutInflater(Bundle savedInstanceState) { + @NonNull + LayoutInflater performGetLayoutInflater(@Nullable Bundle savedInstanceState) { LayoutInflater layoutInflater = onGetLayoutInflater(savedInstanceState); mLayoutInflater = layoutInflater; return mLayoutInflater; @@ -1129,8 +1144,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * {@link #getLayoutInflater()} instead of this method. */ @Deprecated + @NonNull @RestrictTo(LIBRARY_GROUP) - public LayoutInflater getLayoutInflater(Bundle savedFragmentState) { + public LayoutInflater getLayoutInflater(@Nullable Bundle savedFragmentState) { if (mHost == null) { throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the " + "Fragment is attached to the FragmentManager."); @@ -1157,24 +1173,24 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * <p>Here is a typical implementation of a fragment that can take parameters * both through attributes supplied here as well from {@link #getArguments()}:</p> * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentArgumentsSupport.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentArgumentsSupport.java * fragment} * * <p>Note that parsing the XML attributes uses a "styleable" resource. The * declaration for the styleable used here is:</p> * - * {@sample frameworks/support/samples/Support4Demos/res/values/attrs.xml fragment_arguments} + * {@sample frameworks/support/samples/Support4Demos/src/main/res/values/attrs.xml fragment_arguments} * * <p>The fragment can then be declared within its activity's content layout * through a tag like this:</p> * - * {@sample frameworks/support/samples/Support4Demos/res/layout/fragment_arguments_support.xml from_attributes} + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_arguments_support.xml from_attributes} * * <p>This fragment can also be created dynamically from arguments given * at runtime in the arguments Bundle; here is an example of doing so at * creation of the containing activity:</p> * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentArgumentsSupport.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentArgumentsSupport.java * create} * * @param context The Activity that is inflating this fragment. @@ -1356,7 +1372,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @return Return the View for the fragment's UI, or null. */ @Nullable - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return null; } @@ -1371,7 +1387,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @param savedInstanceState If non-null, this fragment is being re-constructed * from a previous saved state as given here. */ - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { } /** @@ -1469,7 +1485,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * * @param outState Bundle in which to place your saved state. */ - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { } /** @@ -1768,7 +1784,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * * @param transition The Transition to use to move Views into the initial Scene. */ - public void setEnterTransition(Object transition) { + public void setEnterTransition(@Nullable Object transition) { ensureAnimationInfo().mEnterTransition = transition; } @@ -1781,6 +1797,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * * @return the Transition to use to move Views into the initial Scene. */ + @Nullable public Object getEnterTransition() { if (mAnimationInfo == null) { return null; @@ -1802,7 +1819,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * is preparing to close. <code>transition</code> must be an * android.transition.Transition. */ - public void setReturnTransition(Object transition) { + public void setReturnTransition(@Nullable Object transition) { ensureAnimationInfo().mReturnTransition = transition; } @@ -1818,6 +1835,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @return the Transition to use to move Views out of the Scene when the Fragment * is preparing to close. */ + @Nullable public Object getReturnTransition() { if (mAnimationInfo == null) { return null; @@ -1839,7 +1857,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * is being closed not due to popping the back stack. <code>transition</code> * must be an android.transition.Transition. */ - public void setExitTransition(Object transition) { + public void setExitTransition(@Nullable Object transition) { ensureAnimationInfo().mExitTransition = transition; } @@ -1855,6 +1873,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @return the Transition to use to move Views out of the Scene when the Fragment * is being closed not due to popping the back stack. */ + @Nullable public Object getExitTransition() { if (mAnimationInfo == null) { return null; @@ -1875,7 +1894,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * previously-started Activity. <code>transition</code> * must be an android.transition.Transition. */ - public void setReenterTransition(Object transition) { + public void setReenterTransition(@Nullable Object transition) { ensureAnimationInfo().mReenterTransition = transition; } @@ -1908,7 +1927,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @param transition The Transition to use for shared elements transferred into the content * Scene. <code>transition</code> must be an android.transition.Transition. */ - public void setSharedElementEnterTransition(Object transition) { + public void setSharedElementEnterTransition(@Nullable Object transition) { ensureAnimationInfo().mSharedElementEnterTransition = transition; } @@ -1921,6 +1940,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @return The Transition to use for shared elements transferred into the content * Scene. */ + @Nullable public Object getSharedElementEnterTransition() { if (mAnimationInfo == null) { return null; @@ -1940,7 +1960,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @param transition The Transition to use for shared elements transferred out of the content * Scene. <code>transition</code> must be an android.transition.Transition. */ - public void setSharedElementReturnTransition(Object transition) { + public void setSharedElementReturnTransition(@Nullable Object transition) { ensureAnimationInfo().mSharedElementReturnTransition = transition; } @@ -1956,6 +1976,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener * @return The Transition to use for shared elements transferred out of the content * Scene. */ + @Nullable public Object getSharedElementReturnTransition() { if (mAnimationInfo == null) { return null; @@ -2231,8 +2252,8 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); } - View performCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { if (mChildFragmentManager != null) { mChildFragmentManager.noteStateNotSaved(); } diff --git a/android/support/v4/app/FragmentActivity.java b/android/support/v4/app/FragmentActivity.java index 481f50d3..cb3c59a6 100644 --- a/android/support/v4/app/FragmentActivity.java +++ b/android/support/v4/app/FragmentActivity.java @@ -153,6 +153,13 @@ public class FragmentActivity extends BaseFragmentActivityApi16 implements return; } + ActivityCompat.PermissionCompatDelegate delegate = + ActivityCompat.getPermissionCompatDelegate(); + if (delegate != null && delegate.onActivityResult(this, requestCode, resultCode, data)) { + // Delegate has handled the activity result + return; + } + super.onActivityResult(requestCode, resultCode, data); } @@ -270,6 +277,16 @@ public class FragmentActivity extends BaseFragmentActivityApi16 implements } /** + * Returns the Lifecycle of the provider. + * + * @return The lifecycle of the provider. + */ + @Override + public Lifecycle getLifecycle() { + return super.getLifecycle(); + } + + /** * Perform initialization of all fragments and loaders. */ @SuppressWarnings("deprecation") @@ -750,6 +767,7 @@ public class FragmentActivity extends BaseFragmentActivityApi16 implements @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + mFragments.noteStateNotSaved(); int index = (requestCode >> 16) & 0xffff; if (index != 0) { index--; diff --git a/android/support/v4/app/FragmentHostCallback.java b/android/support/v4/app/FragmentHostCallback.java index 7dc9f595..eeae62a5 100644 --- a/android/support/v4/app/FragmentHostCallback.java +++ b/android/support/v4/app/FragmentHostCallback.java @@ -94,8 +94,9 @@ public abstract class FragmentHostCallback<E> extends FragmentContainer { * Return a {@link LayoutInflater}. * See {@link Activity#getLayoutInflater()}. */ + @NonNull public LayoutInflater onGetLayoutInflater() { - return (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + return LayoutInflater.from(mContext); } /** diff --git a/android/support/v4/app/FragmentManager.java b/android/support/v4/app/FragmentManager.java index 6e6caa04..16103f8c 100644 --- a/android/support/v4/app/FragmentManager.java +++ b/android/support/v4/app/FragmentManager.java @@ -1605,12 +1605,21 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate @Override public void onAnimationEnd(Animation animation) { super.onAnimationEnd(animation); - container.endViewTransition(viewToAnimate); - - if (fragment.getAnimatingAway() != null) { - fragment.setAnimatingAway(null); - moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, false); - } + // onAnimationEnd() comes during draw(), so there can still be some + // draw events happening after this call. We don't want to detach + // the view until after the onAnimationEnd() + container.post(new Runnable() { + @Override + public void run() { + container.endViewTransition(viewToAnimate); + + if (fragment.getAnimatingAway() != null) { + fragment.setAnimatingAway(null); + moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, + false); + } + } + }); } }); setHWLayerAnimListenerIfAlpha(viewToAnimate, anim); diff --git a/android/support/v4/app/FragmentPagerAdapter.java b/android/support/v4/app/FragmentPagerAdapter.java index 61b181df..6b25d2f7 100644 --- a/android/support/v4/app/FragmentPagerAdapter.java +++ b/android/support/v4/app/FragmentPagerAdapter.java @@ -44,18 +44,18 @@ import android.view.ViewGroup; * <p>Here is an example implementation of a pager containing fragments of * lists: * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentPagerSupport.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentPagerSupport.java * complete} * * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is: * - * {@sample frameworks/support/samples/Support4Demos/res/layout/fragment_pager.xml + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager.xml * complete} * * <p>The <code>R.layout.fragment_pager_list</code> resource containing each * individual fragment's layout is: * - * {@sample frameworks/support/samples/Support4Demos/res/layout/fragment_pager_list.xml + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager_list.xml * complete} */ public abstract class FragmentPagerAdapter extends PagerAdapter { diff --git a/android/support/v4/app/FragmentStatePagerAdapter.java b/android/support/v4/app/FragmentStatePagerAdapter.java index fc27c4fc..040f2db2 100644 --- a/android/support/v4/app/FragmentStatePagerAdapter.java +++ b/android/support/v4/app/FragmentStatePagerAdapter.java @@ -47,18 +47,18 @@ import java.util.ArrayList; * <p>Here is an example implementation of a pager containing fragments of * lists: * - * {@sample frameworks/support/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java + * {@sample frameworks/support/samples/Support13Demos/src/main/java/com/example/android/supportv13/app/FragmentStatePagerSupport.java * complete} * * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is: * - * {@sample frameworks/support/samples/Support13Demos/res/layout/fragment_pager.xml + * {@sample frameworks/support/samples/Support13Demos/src/main/res/layout/fragment_pager.xml * complete} * * <p>The <code>R.layout.fragment_pager_list</code> resource containing each * individual fragment's layout is: * - * {@sample frameworks/support/samples/Support13Demos/res/layout/fragment_pager_list.xml + * {@sample frameworks/support/samples/Support13Demos/src/main/res/layout/fragment_pager_list.xml * complete} */ public abstract class FragmentStatePagerAdapter extends PagerAdapter { diff --git a/android/support/v4/app/FragmentTabHost.java b/android/support/v4/app/FragmentTabHost.java index 09b89b7f..6b914fef 100644 --- a/android/support/v4/app/FragmentTabHost.java +++ b/android/support/v4/app/FragmentTabHost.java @@ -41,12 +41,12 @@ import java.util.ArrayList; * * <p>Here is a simple example of using a FragmentTabHost in an Activity: * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentTabs.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java * complete} * * <p>This can also be used inside of a fragment through fragment nesting: * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java * complete} */ public class FragmentTabHost extends TabHost diff --git a/android/support/v4/app/JobIntentService.java b/android/support/v4/app/JobIntentService.java index c0d7f13e..87b7441e 100644 --- a/android/support/v4/app/JobIntentService.java +++ b/android/support/v4/app/JobIntentService.java @@ -84,7 +84,7 @@ import java.util.HashMap; * * <p>Here is an example implementation of this class:</p> * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/SimpleJobIntentService.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/SimpleJobIntentService.java * complete} */ public abstract class JobIntentService extends Service { diff --git a/android/support/v4/app/ListFragment.java b/android/support/v4/app/ListFragment.java index 21617ad0..496bd8e1 100644 --- a/android/support/v4/app/ListFragment.java +++ b/android/support/v4/app/ListFragment.java @@ -19,6 +19,8 @@ package android.support.v4.app; import android.content.Context; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -142,7 +144,7 @@ public class ListFragment extends Fragment { * Attach to list view once the view hierarchy has been created. */ @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ensureList(); } diff --git a/android/support/v4/app/NavUtils.java b/android/support/v4/app/NavUtils.java index 99d44939..d2594178 100644 --- a/android/support/v4/app/NavUtils.java +++ b/android/support/v4/app/NavUtils.java @@ -24,6 +24,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -53,7 +54,8 @@ public final class NavUtils { * @return true if navigating up should recreate a new task stack, false if the same task * should be used for the destination */ - public static boolean shouldUpRecreateTask(Activity sourceActivity, Intent targetIntent) { + public static boolean shouldUpRecreateTask(@NonNull Activity sourceActivity, + @NonNull Intent targetIntent) { if (Build.VERSION.SDK_INT >= 16) { return sourceActivity.shouldUpRecreateTask(targetIntent); } else { @@ -74,7 +76,7 @@ public final class NavUtils { * * @param sourceActivity The current activity from which the user is attempting to navigate up */ - public static void navigateUpFromSameTask(Activity sourceActivity) { + public static void navigateUpFromSameTask(@NonNull Activity sourceActivity) { Intent upIntent = getParentActivityIntent(sourceActivity); if (upIntent == null) { @@ -101,7 +103,7 @@ public final class NavUtils { * @param sourceActivity The current activity from which the user is attempting to navigate up * @param upIntent An intent representing the target destination for up navigation */ - public static void navigateUpTo(Activity sourceActivity, Intent upIntent) { + public static void navigateUpTo(@NonNull Activity sourceActivity, @NonNull Intent upIntent) { if (Build.VERSION.SDK_INT >= 16) { sourceActivity.navigateUpTo(upIntent); } else { @@ -121,7 +123,8 @@ public final class NavUtils { * @param sourceActivity Activity to fetch a parent intent for * @return a new Intent targeting the defined parent activity of sourceActivity */ - public static Intent getParentActivityIntent(Activity sourceActivity) { + @Nullable + public static Intent getParentActivityIntent(@NonNull Activity sourceActivity) { if (Build.VERSION.SDK_INT >= 16) { // Prefer the "real" JB definition if available, // else fall back to the meta-data element. @@ -157,7 +160,9 @@ public final class NavUtils { * @return a new Intent targeting the defined parent activity of sourceActivity * @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid */ - public static Intent getParentActivityIntent(Context context, Class<?> sourceActivityClass) + @Nullable + public static Intent getParentActivityIntent(@NonNull Context context, + @NonNull Class<?> sourceActivityClass) throws NameNotFoundException { String parentActivity = getParentActivityName(context, new ComponentName(context, sourceActivityClass)); @@ -182,7 +187,9 @@ public final class NavUtils { * @return a new Intent targeting the defined parent activity of sourceActivity * @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid */ - public static Intent getParentActivityIntent(Context context, ComponentName componentName) + @Nullable + public static Intent getParentActivityIntent(@NonNull Context context, + @NonNull ComponentName componentName) throws NameNotFoundException { String parentActivity = getParentActivityName(context, componentName); if (parentActivity == null) return null; @@ -207,7 +214,7 @@ public final class NavUtils { * it was not specified */ @Nullable - public static String getParentActivityName(Activity sourceActivity) { + public static String getParentActivityName(@NonNull Activity sourceActivity) { try { return getParentActivityName(sourceActivity, sourceActivity.getComponentName()); } catch (NameNotFoundException e) { @@ -226,7 +233,8 @@ public final class NavUtils { * it was not specified */ @Nullable - public static String getParentActivityName(Context context, ComponentName componentName) + public static String getParentActivityName(@NonNull Context context, + @NonNull ComponentName componentName) throws NameNotFoundException { PackageManager pm = context.getPackageManager(); ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA); diff --git a/android/support/v4/app/NotificationManagerCompat.java b/android/support/v4/app/NotificationManagerCompat.java index 93775eca..1a0f1bca 100644 --- a/android/support/v4/app/NotificationManagerCompat.java +++ b/android/support/v4/app/NotificationManagerCompat.java @@ -36,6 +36,8 @@ import android.os.Message; import android.os.RemoteException; import android.provider.Settings; import android.support.annotation.GuardedBy; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import java.lang.reflect.Field; @@ -144,7 +146,8 @@ public final class NotificationManagerCompat { public static final int IMPORTANCE_MAX = 5; /** Get a {@link NotificationManagerCompat} instance for a provided context. */ - public static NotificationManagerCompat from(Context context) { + @NonNull + public static NotificationManagerCompat from(@NonNull Context context) { return new NotificationManagerCompat(context); } @@ -167,7 +170,7 @@ public final class NotificationManagerCompat { * @param tag the string identifier of the notification. * @param id the ID of the notification */ - public void cancel(String tag, int id) { + public void cancel(@Nullable String tag, int id) { mNotificationManager.cancel(tag, id); if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { pushSideChannelQueue(new CancelTask(mContext.getPackageName(), id, tag)); @@ -197,7 +200,7 @@ public final class NotificationManagerCompat { * @param id the ID of the notification. The pair (tag, id) must be unique within your app. * @param notification the notification to post to the system */ - public void notify(String tag, int id, Notification notification) { + public void notify(@Nullable String tag, int id, @NonNull Notification notification) { if (useSideChannelForNotification(notification)) { pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification)); // Cancel this notification in notification manager if it just transitioned to being @@ -253,7 +256,8 @@ public final class NotificationManagerCompat { /** * Get the set of packages that have an enabled notification listener component within them. */ - public static Set<String> getEnabledListenerPackages(Context context) { + @NonNull + public static Set<String> getEnabledListenerPackages(@NonNull Context context) { final String enabledNotificationListeners = Settings.Secure.getString( context.getContentResolver(), SETTING_ENABLED_NOTIFICATION_LISTENERS); diff --git a/android/support/v4/app/ServiceCompat.java b/android/support/v4/app/ServiceCompat.java index 1676ee8a..2e63b237 100644 --- a/android/support/v4/app/ServiceCompat.java +++ b/android/support/v4/app/ServiceCompat.java @@ -22,6 +22,7 @@ import android.app.Notification; import android.app.Service; import android.os.Build; import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import java.lang.annotation.Retention; @@ -92,7 +93,7 @@ public final class ServiceCompat { * {@link #STOP_FOREGROUND_DETACH}. * @see Service#startForeground(int, Notification) */ - public static void stopForeground(Service service, @StopForegroundFlags int flags) { + public static void stopForeground(@NonNull Service service, @StopForegroundFlags int flags) { if (Build.VERSION.SDK_INT >= 24) { service.stopForeground(flags); } else { diff --git a/android/support/v4/app/TaskStackBuilder.java b/android/support/v4/app/TaskStackBuilder.java index dc9a2dc0..14aadce2 100644 --- a/android/support/v4/app/TaskStackBuilder.java +++ b/android/support/v4/app/TaskStackBuilder.java @@ -16,7 +16,6 @@ package android.support.v4.app; -import android.support.annotation.RequiresApi; import android.app.Activity; import android.app.PendingIntent; import android.content.ComponentName; @@ -25,6 +24,9 @@ import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; import android.support.v4.content.ContextCompat; import android.util.Log; @@ -70,6 +72,7 @@ public final class TaskStackBuilder implements Iterable<Intent> { private static final String TAG = "TaskStackBuilder"; public interface SupportParentable { + @Nullable Intent getSupportParentActivityIntent(); } @@ -117,7 +120,8 @@ public final class TaskStackBuilder implements Iterable<Intent> { * @param context The context that will launch the new task stack or generate a PendingIntent * @return A new TaskStackBuilder */ - public static TaskStackBuilder create(Context context) { + @NonNull + public static TaskStackBuilder create(@NonNull Context context) { return new TaskStackBuilder(context); } @@ -142,7 +146,8 @@ public final class TaskStackBuilder implements Iterable<Intent> { * @param nextIntent Intent for the next Activity in the synthesized task stack * @return This TaskStackBuilder for method chaining */ - public TaskStackBuilder addNextIntent(Intent nextIntent) { + @NonNull + public TaskStackBuilder addNextIntent(@NonNull Intent nextIntent) { mIntents.add(nextIntent); return this; } @@ -159,7 +164,8 @@ public final class TaskStackBuilder implements Iterable<Intent> { * Its chain of parents as specified in the manifest will be added. * @return This TaskStackBuilder for method chaining. */ - public TaskStackBuilder addNextIntentWithParentStack(Intent nextIntent) { + @NonNull + public TaskStackBuilder addNextIntentWithParentStack(@NonNull Intent nextIntent) { ComponentName target = nextIntent.getComponent(); if (target == null) { target = nextIntent.resolveActivity(mSourceContext.getPackageManager()); @@ -178,7 +184,8 @@ public final class TaskStackBuilder implements Iterable<Intent> { * @param sourceActivity All parents of this activity will be added * @return This TaskStackBuilder for method chaining */ - public TaskStackBuilder addParentStack(Activity sourceActivity) { + @NonNull + public TaskStackBuilder addParentStack(@NonNull Activity sourceActivity) { Intent parent = null; if (sourceActivity instanceof SupportParentable) { parent = ((SupportParentable) sourceActivity).getSupportParentActivityIntent(); @@ -207,7 +214,8 @@ public final class TaskStackBuilder implements Iterable<Intent> { * @param sourceActivityClass All parents of this activity will be added * @return This TaskStackBuilder for method chaining */ - public TaskStackBuilder addParentStack(Class<?> sourceActivityClass) { + @NonNull + public TaskStackBuilder addParentStack(@NonNull Class<?> sourceActivityClass) { return addParentStack(new ComponentName(mSourceContext, sourceActivityClass)); } @@ -264,6 +272,7 @@ public final class TaskStackBuilder implements Iterable<Intent> { * @param index Index from 0-getIntentCount() * @return the intent at position index */ + @Nullable public Intent editIntentAt(int index) { return mIntents.get(index); } @@ -300,7 +309,7 @@ public final class TaskStackBuilder implements Iterable<Intent> { * @param options Additional options for how the Activity should be started. * See {@link android.content.Context#startActivity(Intent, Bundle)} */ - public void startActivities(Bundle options) { + public void startActivities(@Nullable Bundle options) { if (mIntents.isEmpty()) { throw new IllegalStateException( "No intents added to TaskStackBuilder; cannot startActivities"); @@ -325,8 +334,10 @@ public final class TaskStackBuilder implements Iterable<Intent> { * {@link PendingIntent#FLAG_UPDATE_CURRENT}, or any of the flags supported by * {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the * intent that can be supplied when the actual send happens. - * @return The obtained PendingIntent + * @return The obtained PendingIntent. May return null only if + * {@link PendingIntent#FLAG_NO_CREATE} has been supplied. */ + @Nullable public PendingIntent getPendingIntent(int requestCode, int flags) { return getPendingIntent(requestCode, flags, null); } @@ -342,9 +353,11 @@ public final class TaskStackBuilder implements Iterable<Intent> { * intent that can be supplied when the actual send happens. * @param options Additional options for how the Activity should be started. * See {@link android.content.Context#startActivity(Intent, Bundle)} - * @return The obtained PendingIntent + * @return The obtained PendingIntent. May return null only if + * {@link PendingIntent#FLAG_NO_CREATE} has been supplied. */ - public PendingIntent getPendingIntent(int requestCode, int flags, Bundle options) { + @Nullable + public PendingIntent getPendingIntent(int requestCode, int flags, @Nullable Bundle options) { if (mIntents.isEmpty()) { throw new IllegalStateException( "No intents added to TaskStackBuilder; cannot getPendingIntent"); @@ -364,6 +377,7 @@ public final class TaskStackBuilder implements Iterable<Intent> { * * @return An array containing the intents added to this builder. */ + @NonNull public Intent[] getIntents() { Intent[] intents = new Intent[mIntents.size()]; if (intents.length == 0) return intents; diff --git a/android/support/v4/content/AsyncTaskLoader.java b/android/support/v4/content/AsyncTaskLoader.java index faa13ad7..5882f691 100644 --- a/android/support/v4/content/AsyncTaskLoader.java +++ b/android/support/v4/content/AsyncTaskLoader.java @@ -21,6 +21,8 @@ import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.content.Context; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.os.OperationCanceledException; import android.support.v4.util.TimeUtils; @@ -121,11 +123,11 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { long mLastLoadCompleteTime = -10000; Handler mHandler; - public AsyncTaskLoader(Context context) { + public AsyncTaskLoader(@NonNull Context context) { this(context, ModernAsyncTask.THREAD_POOL_EXECUTOR); } - private AsyncTaskLoader(Context context, Executor executor) { + private AsyncTaskLoader(@NonNull Context context, @NonNull Executor executor) { super(context); mExecutor = executor; } @@ -200,7 +202,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { * @param data The value that was returned by {@link #loadInBackground}, or null * if the task threw {@link OperationCanceledException}. */ - public void onCanceled(D data) { + public void onCanceled(@Nullable D data) { } void executePendingTask() { @@ -284,6 +286,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { * @see #cancelLoadInBackground * @see #onCanceled */ + @Nullable public abstract D loadInBackground(); /** @@ -298,6 +301,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { * * @see #loadInBackground */ + @Nullable protected D onLoadInBackground() { return loadInBackground(); } diff --git a/android/support/v4/content/ContextCompat.java b/android/support/v4/content/ContextCompat.java index fdbe32f1..8a4fd7aa 100644 --- a/android/support/v4/content/ContextCompat.java +++ b/android/support/v4/content/ContextCompat.java @@ -79,7 +79,7 @@ public class ContextCompat { * length-1 will correspond to the top activity on the resulting task stack. * @return true if the underlying API was available and the call was successful, false otherwise */ - public static boolean startActivities(Context context, Intent[] intents) { + public static boolean startActivities(@NonNull Context context, @NonNull Intent[] intents) { return startActivities(context, intents, null); } @@ -110,7 +110,8 @@ public class ContextCompat { * See {@link android.content.Context#startActivity(Intent, android.os.Bundle)} * @return true if the underlying API was available and the call was successful, false otherwise */ - public static boolean startActivities(Context context, Intent[] intents, Bundle options) { + public static boolean startActivities(@NonNull Context context, @NonNull Intent[] intents, + @Nullable Bundle options) { if (Build.VERSION.SDK_INT >= 16) { context.startActivities(intents, options); } else { @@ -136,7 +137,8 @@ public class ContextCompat { * supplied here; there are no supported definitions for * building it manually. */ - public static void startActivity(Context context, Intent intent, @Nullable Bundle options) { + public static void startActivity(@NonNull Context context, @NonNull Intent intent, + @Nullable Bundle options) { if (Build.VERSION.SDK_INT >= 16) { context.startActivity(intent, options); } else { @@ -159,7 +161,8 @@ public class ContextCompat { * * @see ApplicationInfo#dataDir */ - public static File getDataDir(Context context) { + @Nullable + public static File getDataDir(@NonNull Context context) { if (Build.VERSION.SDK_INT >= 24) { return context.getDataDir(); } else { @@ -211,7 +214,8 @@ public class ContextCompat { * @see Context#getObbDir() * @see EnvironmentCompat#getStorageState(File) */ - public static File[] getObbDirs(Context context) { + @NonNull + public static File[] getObbDirs(@NonNull Context context) { if (Build.VERSION.SDK_INT >= 19) { return context.getObbDirs(); } else { @@ -263,7 +267,8 @@ public class ContextCompat { * @see Context#getExternalFilesDir(String) * @see EnvironmentCompat#getStorageState(File) */ - public static File[] getExternalFilesDirs(Context context, String type) { + @NonNull + public static File[] getExternalFilesDirs(@NonNull Context context, @Nullable String type) { if (Build.VERSION.SDK_INT >= 19) { return context.getExternalFilesDirs(type); } else { @@ -315,7 +320,8 @@ public class ContextCompat { * @see Context#getExternalCacheDir() * @see EnvironmentCompat#getStorageState(File) */ - public static File[] getExternalCacheDirs(Context context) { + @NonNull + public static File[] getExternalCacheDirs(@NonNull Context context) { if (Build.VERSION.SDK_INT >= 19) { return context.getExternalCacheDirs(); } else { @@ -346,7 +352,8 @@ public class ContextCompat { * The value 0 is an invalid identifier. * @return Drawable An object that can be used to draw this resource. */ - public static final Drawable getDrawable(Context context, @DrawableRes int id) { + @Nullable + public static final Drawable getDrawable(@NonNull Context context, @DrawableRes int id) { if (Build.VERSION.SDK_INT >= 21) { return context.getDrawable(id); } else if (Build.VERSION.SDK_INT >= 16) { @@ -382,7 +389,9 @@ public class ContextCompat { * @throws android.content.res.Resources.NotFoundException if the given ID * does not exist. */ - public static final ColorStateList getColorStateList(Context context, @ColorRes int id) { + @Nullable + public static final ColorStateList getColorStateList(@NonNull Context context, + @ColorRes int id) { if (Build.VERSION.SDK_INT >= 23) { return context.getColorStateList(id); } else { @@ -404,7 +413,7 @@ public class ContextCompat { * does not exist. */ @ColorInt - public static final int getColor(Context context, @ColorRes int id) { + public static final int getColor(@NonNull Context context, @ColorRes int id) { if (Build.VERSION.SDK_INT >= 23) { return context.getColor(id); } else { @@ -444,7 +453,8 @@ public class ContextCompat { * * @see android.content.Context#getFilesDir() */ - public static final File getNoBackupFilesDir(Context context) { + @Nullable + public static final File getNoBackupFilesDir(@NonNull Context context) { if (Build.VERSION.SDK_INT >= 21) { return context.getNoBackupFilesDir(); } else { @@ -468,7 +478,7 @@ public class ContextCompat { * * @return The path of the directory holding application code cache files. */ - public static File getCodeCacheDir(Context context) { + public static File getCodeCacheDir(@NonNull Context context) { if (Build.VERSION.SDK_INT >= 21) { return context.getCodeCacheDir(); } else { @@ -522,7 +532,8 @@ public class ContextCompat { * * @see ContextCompat#isDeviceProtectedStorage(Context) */ - public static Context createDeviceProtectedStorageContext(Context context) { + @Nullable + public static Context createDeviceProtectedStorageContext(@NonNull Context context) { if (Build.VERSION.SDK_INT >= 24) { return context.createDeviceProtectedStorageContext(); } else { @@ -536,7 +547,7 @@ public class ContextCompat { * * @see ContextCompat#createDeviceProtectedStorageContext(Context) */ - public static boolean isDeviceProtectedStorage(Context context) { + public static boolean isDeviceProtectedStorage(@NonNull Context context) { if (Build.VERSION.SDK_INT >= 24) { return context.isDeviceProtectedStorage(); } else { @@ -554,7 +565,7 @@ public class ContextCompat { * @see Context#startForegeroundService() * @see Context#startService() */ - public static void startForegroundService(Context context, Intent intent) { + public static void startForegroundService(@NonNull Context context, @NonNull Intent intent) { if (Build.VERSION.SDK_INT >= 26) { context.startForegroundService(intent); } else { diff --git a/android/support/v4/content/CursorLoader.java b/android/support/v4/content/CursorLoader.java index 503bb9c7..5c6925d9 100644 --- a/android/support/v4/content/CursorLoader.java +++ b/android/support/v4/content/CursorLoader.java @@ -20,6 +20,8 @@ import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.os.CancellationSignal; import android.support.v4.os.OperationCanceledException; @@ -115,7 +117,7 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> { * calls to {@link #setUri(Uri)}, {@link #setSelection(String)}, etc * to specify the query to perform. */ - public CursorLoader(Context context) { + public CursorLoader(@NonNull Context context) { super(context); mObserver = new ForceLoadContentObserver(); } @@ -126,8 +128,9 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> { * ContentResolver.query()} for documentation on the meaning of the * parameters. These will be passed as-is to that call. */ - public CursorLoader(Context context, Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { + public CursorLoader(@NonNull Context context, @NonNull Uri uri, @Nullable String[] projection, + @Nullable String selection, @Nullable String[] selectionArgs, + @Nullable String sortOrder) { super(context); mObserver = new ForceLoadContentObserver(); mUri = uri; @@ -183,43 +186,48 @@ public class CursorLoader extends AsyncTaskLoader<Cursor> { mCursor = null; } + @NonNull public Uri getUri() { return mUri; } - public void setUri(Uri uri) { + public void setUri(@NonNull Uri uri) { mUri = uri; } + @Nullable public String[] getProjection() { return mProjection; } - public void setProjection(String[] projection) { + public void setProjection(@Nullable String[] projection) { mProjection = projection; } + @Nullable public String getSelection() { return mSelection; } - public void setSelection(String selection) { + public void setSelection(@Nullable String selection) { mSelection = selection; } + @Nullable public String[] getSelectionArgs() { return mSelectionArgs; } - public void setSelectionArgs(String[] selectionArgs) { + public void setSelectionArgs(@Nullable String[] selectionArgs) { mSelectionArgs = selectionArgs; } + @Nullable public String getSortOrder() { return mSortOrder; } - public void setSortOrder(String sortOrder) { + public void setSortOrder(@Nullable String sortOrder) { mSortOrder = sortOrder; } diff --git a/android/support/v4/content/FileProvider.java b/android/support/v4/content/FileProvider.java index c49fc123..8599911a 100644 --- a/android/support/v4/content/FileProvider.java +++ b/android/support/v4/content/FileProvider.java @@ -33,6 +33,8 @@ import android.os.Environment; import android.os.ParcelFileDescriptor; import android.provider.OpenableColumns; import android.support.annotation.GuardedBy; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.webkit.MimeTypeMap; @@ -362,7 +364,7 @@ public class FileProvider extends ContentProvider { * @param info A {@link ProviderInfo} for the new provider. */ @Override - public void attachInfo(Context context, ProviderInfo info) { + public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) { super.attachInfo(context, info); // Sanity check our security @@ -396,7 +398,8 @@ public class FileProvider extends ContentProvider { * @throws IllegalArgumentException When the given {@link File} is outside * the paths supported by the provider. */ - public static Uri getUriForFile(Context context, String authority, File file) { + public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, + @NonNull File file) { final PathStrategy strategy = getPathStrategy(context, authority); return strategy.getUriForFile(file); } @@ -430,8 +433,9 @@ public class FileProvider extends ContentProvider { * */ @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { + public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder) { // ContentProvider has already checked granted permissions final File file = mStrategy.getFileForUri(uri); @@ -470,7 +474,7 @@ public class FileProvider extends ContentProvider { * extension; otherwise <code>application/octet-stream</code>. */ @Override - public String getType(Uri uri) { + public String getType(@NonNull Uri uri) { // ContentProvider has already checked granted permissions final File file = mStrategy.getFileForUri(uri); @@ -491,7 +495,7 @@ public class FileProvider extends ContentProvider { * subclass FileProvider if you want to provide different functionality. */ @Override - public Uri insert(Uri uri, ContentValues values) { + public Uri insert(@NonNull Uri uri, ContentValues values) { throw new UnsupportedOperationException("No external inserts"); } @@ -500,7 +504,8 @@ public class FileProvider extends ContentProvider { * subclass FileProvider if you want to provide different functionality. */ @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection, + @Nullable String[] selectionArgs) { throw new UnsupportedOperationException("No external updates"); } @@ -516,7 +521,8 @@ public class FileProvider extends ContentProvider { * @return 1 if the delete succeeds; otherwise, 0. */ @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { + public int delete(@NonNull Uri uri, @Nullable String selection, + @Nullable String[] selectionArgs) { // ContentProvider has already checked granted permissions final File file = mStrategy.getFileForUri(uri); return file.delete() ? 1 : 0; @@ -538,7 +544,8 @@ public class FileProvider extends ContentProvider { * @return A new {@link ParcelFileDescriptor} with which you can access the file. */ @Override - public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) + throws FileNotFoundException { // ContentProvider has already checked granted permissions final File file = mStrategy.getFileForUri(uri); final int fileMode = modeToMode(mode); diff --git a/android/support/v4/content/IntentCompat.java b/android/support/v4/content/IntentCompat.java index 1a7295b3..f5b3a0b1 100644 --- a/android/support/v4/content/IntentCompat.java +++ b/android/support/v4/content/IntentCompat.java @@ -18,6 +18,7 @@ package android.support.v4.content; import android.content.Intent; import android.os.Build; +import android.support.annotation.NonNull; /** * Helper for accessing features in {@link android.content.Intent}. @@ -69,8 +70,9 @@ public final class IntentCompat { * @return Returns a newly created Intent that can be used to launch the * activity as a main application entry. */ - public static Intent makeMainSelectorActivity(String selectorAction, - String selectorCategory) { + @NonNull + public static Intent makeMainSelectorActivity(@NonNull String selectorAction, + @NonNull String selectorCategory) { if (Build.VERSION.SDK_INT >= 15) { return Intent.makeMainSelectorActivity(selectorAction, selectorCategory); } else { diff --git a/android/support/v4/content/Loader.java b/android/support/v4/content/Loader.java index 40b459fa..2ac10d73 100644 --- a/android/support/v4/content/Loader.java +++ b/android/support/v4/content/Loader.java @@ -19,6 +19,8 @@ package android.support.v4.content; import android.content.Context; import android.database.ContentObserver; import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.util.DebugUtils; import java.io.FileDescriptor; @@ -80,7 +82,7 @@ public class Loader<D> { * @param loader the loader that completed the load * @param data the result of the load */ - public void onLoadComplete(Loader<D> loader, D data); + void onLoadComplete(@NonNull Loader<D> loader, @Nullable D data); } /** @@ -97,7 +99,7 @@ public class Loader<D> { * * @param loader the loader that canceled the load */ - public void onLoadCanceled(Loader<D> loader); + void onLoadCanceled(@NonNull Loader<D> loader); } /** @@ -110,7 +112,7 @@ public class Loader<D> { * * @param context used to retrieve the application context. */ - public Loader(Context context) { + public Loader(@NonNull Context context) { mContext = context.getApplicationContext(); } @@ -121,7 +123,7 @@ public class Loader<D> { * * @param data the result of the load */ - public void deliverResult(D data) { + public void deliverResult(@Nullable D data) { if (mListener != null) { mListener.onLoadComplete(this, data); } @@ -142,6 +144,7 @@ public class Loader<D> { /** * @return an application context retrieved from the Context passed to the constructor. */ + @NonNull public Context getContext() { return mContext; } @@ -160,7 +163,7 @@ public class Loader<D> { * * <p>Must be called from the process's main thread. */ - public void registerListener(int id, OnLoadCompleteListener<D> listener) { + public void registerListener(int id, @NonNull OnLoadCompleteListener<D> listener) { if (mListener != null) { throw new IllegalStateException("There is already a listener registered"); } @@ -173,7 +176,7 @@ public class Loader<D> { * * Must be called from the process's main thread. */ - public void unregisterListener(OnLoadCompleteListener<D> listener) { + public void unregisterListener(@NonNull OnLoadCompleteListener<D> listener) { if (mListener == null) { throw new IllegalStateException("No listener register"); } @@ -192,7 +195,7 @@ public class Loader<D> { * * @param listener The listener to register. */ - public void registerOnLoadCanceledListener(OnLoadCanceledListener<D> listener) { + public void registerOnLoadCanceledListener(@NonNull OnLoadCanceledListener<D> listener) { if (mOnLoadCanceledListener != null) { throw new IllegalStateException("There is already a listener registered"); } @@ -207,7 +210,7 @@ public class Loader<D> { * * @param listener The listener to unregister. */ - public void unregisterOnLoadCanceledListener(OnLoadCanceledListener<D> listener) { + public void unregisterOnLoadCanceledListener(@NonNull OnLoadCanceledListener<D> listener) { if (mOnLoadCanceledListener == null) { throw new IllegalStateException("No listener register"); } @@ -493,7 +496,8 @@ public class Loader<D> { * For debugging, converts an instance of the Loader's data class to * a string that can be printed. Must handle a null data. */ - public String dataToString(D data) { + @NonNull + public String dataToString(@Nullable D data) { StringBuilder sb = new StringBuilder(64); DebugUtils.buildShortClassTag(data, sb); sb.append("}"); diff --git a/android/support/v4/content/LocalBroadcastManager.java b/android/support/v4/content/LocalBroadcastManager.java index 324bb304..aaaf8be3 100644 --- a/android/support/v4/content/LocalBroadcastManager.java +++ b/android/support/v4/content/LocalBroadcastManager.java @@ -16,10 +16,6 @@ package android.support.v4.content; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Set; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -27,8 +23,13 @@ import android.content.IntentFilter; import android.net.Uri; import android.os.Handler; import android.os.Message; +import android.support.annotation.NonNull; import android.util.Log; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; + /** * Helper to register for and send broadcasts of Intents to local objects * within your process. This has a number of advantages over sending @@ -98,7 +99,8 @@ public final class LocalBroadcastManager { private static final Object mLock = new Object(); private static LocalBroadcastManager mInstance; - public static LocalBroadcastManager getInstance(Context context) { + @NonNull + public static LocalBroadcastManager getInstance(@NonNull Context context) { synchronized (mLock) { if (mInstance == null) { mInstance = new LocalBroadcastManager(context.getApplicationContext()); @@ -132,7 +134,8 @@ public final class LocalBroadcastManager { * * @see #unregisterReceiver */ - public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + public void registerReceiver(@NonNull BroadcastReceiver receiver, + @NonNull IntentFilter filter) { synchronized (mReceivers) { ReceiverRecord entry = new ReceiverRecord(filter, receiver); ArrayList<ReceiverRecord> filters = mReceivers.get(receiver); @@ -162,7 +165,7 @@ public final class LocalBroadcastManager { * * @see #registerReceiver */ - public void unregisterReceiver(BroadcastReceiver receiver) { + public void unregisterReceiver(@NonNull BroadcastReceiver receiver) { synchronized (mReceivers) { final ArrayList<ReceiverRecord> filters = mReceivers.remove(receiver); if (filters == null) { @@ -205,7 +208,7 @@ public final class LocalBroadcastManager { * broadcast receivers. (Note tha delivery may not ultimately take place if one of those * receivers is unregistered before it is dispatched.) */ - public boolean sendBroadcast(Intent intent) { + public boolean sendBroadcast(@NonNull Intent intent) { synchronized (mReceivers) { final String action = intent.getAction(); final String type = intent.resolveTypeIfNeeded( @@ -281,7 +284,7 @@ public final class LocalBroadcastManager { * the Intent this function will block and immediately dispatch them before * returning. */ - public void sendBroadcastSync(Intent intent) { + public void sendBroadcastSync(@NonNull Intent intent) { if (sendBroadcast(intent)) { executePendingBroadcasts(); } diff --git a/android/support/v4/content/MimeTypeFilter.java b/android/support/v4/content/MimeTypeFilter.java index 8734c4d2..8a90c62a 100644 --- a/android/support/v4/content/MimeTypeFilter.java +++ b/android/support/v4/content/MimeTypeFilter.java @@ -87,6 +87,7 @@ public final class MimeTypeFilter { * Matches one nullable MIME type against an array of MIME type filters. * @return The first matching filter, or null if nothing matches. */ + @Nullable public static String matches( @Nullable String mimeType, @NonNull String[] filters) { if (mimeType == null) { @@ -108,6 +109,7 @@ public final class MimeTypeFilter { * Matches multiple MIME types against an array of MIME type filters. * @return The first matching MIME type, or null if nothing matches. */ + @Nullable public static String matches( @Nullable String[] mimeTypes, @NonNull String filter) { if (mimeTypes == null) { @@ -129,6 +131,7 @@ public final class MimeTypeFilter { * Matches multiple MIME types against an array of MIME type filters. * @return The list of matching MIME types, or empty array if nothing matches. */ + @NonNull public static String[] matchesMany( @Nullable String[] mimeTypes, @NonNull String filter) { if (mimeTypes == null) { diff --git a/android/support/v4/content/PermissionChecker.java b/android/support/v4/content/PermissionChecker.java index 08662734..c9f18a9f 100644 --- a/android/support/v4/content/PermissionChecker.java +++ b/android/support/v4/content/PermissionChecker.java @@ -24,6 +24,7 @@ import android.os.Binder; import android.os.Process; import android.support.annotation.IntDef; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.app.AppOpsManagerCompat; @@ -91,7 +92,7 @@ public final class PermissionChecker { */ @PermissionResult public static int checkPermission(@NonNull Context context, @NonNull String permission, - int pid, int uid, String packageName) { + int pid, int uid, @Nullable String packageName) { if (context.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_DENIED) { return PERMISSION_DENIED; } @@ -146,7 +147,7 @@ public final class PermissionChecker { */ @PermissionResult public static int checkCallingPermission(@NonNull Context context, - @NonNull String permission, String packageName) { + @NonNull String permission, @Nullable String packageName) { if (Binder.getCallingPid() == Process.myPid()) { return PERMISSION_DENIED; } diff --git a/android/support/v4/content/SharedPreferencesCompat.java b/android/support/v4/content/SharedPreferencesCompat.java index 7d51fed5..1d43c2eb 100644 --- a/android/support/v4/content/SharedPreferencesCompat.java +++ b/android/support/v4/content/SharedPreferencesCompat.java @@ -19,8 +19,18 @@ package android.support.v4.content; import android.content.SharedPreferences; import android.support.annotation.NonNull; +/** + * @deprecated This compatibility class is no longer required. Use {@link SharedPreferences} + * directly. + */ +@Deprecated public final class SharedPreferencesCompat { + /** + * @deprecated This compatibility class is no longer required. Use + * {@link SharedPreferences.Editor} directly. + */ + @Deprecated public final static class EditorCompat { private static EditorCompat sInstance; @@ -46,14 +56,22 @@ public final class SharedPreferencesCompat { private EditorCompat() { mHelper = new Helper(); } - + /** + * @deprecated This compatibility class is no longer required. Use + * {@link SharedPreferences.Editor} directly. + */ + @Deprecated public static EditorCompat getInstance() { if (sInstance == null) { sInstance = new EditorCompat(); } return sInstance; } - + /** + * @deprecated This compatibility method is no longer required. Use + * {@link SharedPreferences.Editor#apply()} directly. + */ + @Deprecated public void apply(@NonNull SharedPreferences.Editor editor) { // Note that this redirection is needed to not break the public API chain // of getInstance().apply() calls. Otherwise this method could (and should) diff --git a/android/support/v4/content/WakefulBroadcastReceiver.java b/android/support/v4/content/WakefulBroadcastReceiver.java index b0cd653f..8ec3eee6 100644 --- a/android/support/v4/content/WakefulBroadcastReceiver.java +++ b/android/support/v4/content/WakefulBroadcastReceiver.java @@ -45,7 +45,7 @@ import android.util.SparseArray; * {@link WakefulBroadcastReceiver#startWakefulService startWakefulService()} * holds an extra identifying the wake lock.</p> * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/content/SimpleWakefulReceiver.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/content/SimpleWakefulReceiver.java * complete} * * <p>The service (in this example, an {@link android.app.IntentService}) does @@ -55,7 +55,7 @@ import android.util.SparseArray; * is the same intent that the {@link WakefulBroadcastReceiver} originally * passed in.</p> * - * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/content/SimpleWakefulService.java + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/content/SimpleWakefulService.java * complete} * * @deprecated As of {@link android.os.Build.VERSION_CODES#O Android O}, background check diff --git a/android/support/v4/content/pm/ShortcutInfoCompat.java b/android/support/v4/content/pm/ShortcutInfoCompat.java index 3ae74701..63585e17 100644 --- a/android/support/v4/content/pm/ShortcutInfoCompat.java +++ b/android/support/v4/content/pm/ShortcutInfoCompat.java @@ -18,17 +18,20 @@ package android.support.v4.content.pm; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; +import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; +import android.support.annotation.VisibleForTesting; import android.support.v4.graphics.drawable.IconCompat; import android.text.TextUtils; import java.util.Arrays; /** - * Helper for accessing features in {@link android.content.pm.ShortcutInfo}. + * Helper for accessing features in {@link ShortcutInfo}. */ public class ShortcutInfoCompat { @@ -43,6 +46,7 @@ public class ShortcutInfoCompat { private CharSequence mDisabledMessage; private IconCompat mIcon; + private boolean mIsAlwaysBadged; private ShortcutInfoCompat() { } @@ -69,11 +73,26 @@ public class ShortcutInfoCompat { return builder.build(); } + @VisibleForTesting Intent addToIntent(Intent outIntent) { outIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, mIntents[mIntents.length - 1]) .putExtra(Intent.EXTRA_SHORTCUT_NAME, mLabel.toString()); if (mIcon != null) { - mIcon.addToShortcutIntent(outIntent); + Drawable badge = null; + if (mIsAlwaysBadged) { + PackageManager pm = mContext.getPackageManager(); + if (mActivity != null) { + try { + badge = pm.getActivityIcon(mActivity); + } catch (PackageManager.NameNotFoundException e) { + // Ignore + } + } + if (badge == null) { + badge = mContext.getApplicationInfo().loadIcon(pm); + } + } + mIcon.addToShortcutIntent(outIntent, badge); } return outIntent; } @@ -250,7 +269,7 @@ public class ShortcutInfoCompat { * on the launcher. * * @see ShortcutInfo#getActivity() - * @see android.content.pm.ShortcutInfo.Builder#setActivity(ComponentName) + * @see ShortcutInfo.Builder#setActivity(ComponentName) */ @NonNull public Builder setActivity(@NonNull ComponentName activity) { @@ -259,6 +278,23 @@ public class ShortcutInfoCompat { } /** + * Badges the icon before passing it over to the Launcher. + * <p> + * Launcher automatically badges {@link ShortcutInfo}, so only the legacy shortcut icon, + * {@link Intent.ShortcutIconResource} is badged. This field is ignored when using + * {@link ShortcutInfo} on API 25 and above. + * <p> + * If the shortcut is associated with an activity, the activity icon is used as the badge, + * otherwise application icon is used. + * + * @see #setActivity(ComponentName) + */ + public Builder setAlwaysBadged() { + mInfo.mIsAlwaysBadged = true; + return this; + } + + /** * Creates a {@link ShortcutInfoCompat} instance. */ @NonNull diff --git a/android/support/v4/content/res/FontResourcesParserCompat.java b/android/support/v4/content/res/FontResourcesParserCompat.java index 7fe86ada..8ad07d31 100644 --- a/android/support/v4/content/res/FontResourcesParserCompat.java +++ b/android/support/v4/content/res/FontResourcesParserCompat.java @@ -252,10 +252,19 @@ public class FontResourcesParserCompat { throws XmlPullParserException, IOException { AttributeSet attrs = Xml.asAttributeSet(parser); TypedArray array = resources.obtainAttributes(attrs, R.styleable.FontFamilyFont); - int weight = array.getInt(R.styleable.FontFamilyFont_fontWeight, NORMAL_WEIGHT); - boolean isItalic = ITALIC == array.getInt(R.styleable.FontFamilyFont_fontStyle, 0); - int resourceId = array.getResourceId(R.styleable.FontFamilyFont_font, 0); - String filename = array.getString(R.styleable.FontFamilyFont_font); + final int weightAttr = array.hasValue(R.styleable.FontFamilyFont_fontWeight) + ? R.styleable.FontFamilyFont_fontWeight + : R.styleable.FontFamilyFont_android_fontWeight; + int weight = array.getInt(weightAttr, NORMAL_WEIGHT); + final int styleAttr = array.hasValue(R.styleable.FontFamilyFont_fontStyle) + ? R.styleable.FontFamilyFont_fontStyle + : R.styleable.FontFamilyFont_android_fontStyle; + boolean isItalic = ITALIC == array.getInt(styleAttr, 0); + final int resourceAttr = array.hasValue(R.styleable.FontFamilyFont_font) + ? R.styleable.FontFamilyFont_font + : R.styleable.FontFamilyFont_android_font; + int resourceId = array.getResourceId(resourceAttr, 0); + String filename = array.getString(resourceAttr); array.recycle(); while (parser.next() != XmlPullParser.END_TAG) { skip(parser); diff --git a/android/support/v4/content/res/ResourcesCompat.java b/android/support/v4/content/res/ResourcesCompat.java index 43d78d0e..4c70ce93 100644 --- a/android/support/v4/content/res/ResourcesCompat.java +++ b/android/support/v4/content/res/ResourcesCompat.java @@ -27,6 +27,8 @@ import android.content.res.Resources.Theme; import android.content.res.XmlResourceParser; import android.graphics.Typeface; import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; @@ -36,9 +38,11 @@ import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.content.res.FontResourcesParserCompat.FamilyResourceEntry; import android.support.v4.graphics.TypefaceCompat; +import android.support.v4.provider.FontsContractCompat.FontRequestCallback; +import android.support.v4.provider.FontsContractCompat.FontRequestCallback.FontRequestFailReason; +import android.support.v4.util.Preconditions; import android.util.Log; import android.util.TypedValue; -import android.widget.TextView; import org.xmlpull.v1.XmlPullParserException; @@ -175,8 +179,12 @@ public final class ResourcesCompat { /** * Returns a font Typeface associated with a particular resource ID. * <p> + * This method will block the calling thread to retrieve the requested font, including if it + * is from a font provider. If you wish to not have this behavior, use + * {@link #getFont(Context, int, FontCallback, Handler)} instead. + * <p> * Prior to API level 23, font resources with more than one font in a family will only load the - * first font in that family. + * font closest to a regular weight typeface. * * @param context A context to retrieve the Resources from. * @param id The desired resource identifier of a {@link Typeface}, @@ -184,8 +192,9 @@ public final class ResourcesCompat { * package, type, and resource entry. The value 0 is an invalid * identifier. * @return A font Typeface object. - * @throws NotFoundException Throws NotFoundException if the given ID does - * not exist. + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @see #getFont(Context, int, FontCallback, Handler) */ @Nullable public static Typeface getFont(@NonNull Context context, @FontRes int id) @@ -193,34 +202,155 @@ public final class ResourcesCompat { if (context.isRestricted()) { return null; } - return loadFont(context, id, new TypedValue(), Typeface.NORMAL, null); + return loadFont(context, id, new TypedValue(), Typeface.NORMAL, null /* callback */, + null /* handler */, false /* isXmlRequest */); + } + + /** + * Interface used to receive asynchronous font fetching events. + */ + public abstract static class FontCallback { + + /** + * Called when an asynchronous font was finished loading. + * + * @param typeface The font that was loaded. + */ + public abstract void onFontRetrieved(@NonNull Typeface typeface); + + /** + * Called when an asynchronous font failed to load. + * + * @param reason The reason the font failed to load. One of + * {@link FontRequestFailReason#FAIL_REASON_PROVIDER_NOT_FOUND}, + * {@link FontRequestFailReason#FAIL_REASON_WRONG_CERTIFICATES}, + * {@link FontRequestFailReason#FAIL_REASON_FONT_LOAD_ERROR}, + * {@link FontRequestFailReason#FAIL_REASON_SECURITY_VIOLATION}, + * {@link FontRequestFailReason#FAIL_REASON_FONT_NOT_FOUND}, + * {@link FontRequestFailReason#FAIL_REASON_FONT_UNAVAILABLE} or + * {@link FontRequestFailReason#FAIL_REASON_MALFORMED_QUERY}. + */ + public abstract void onFontRetrievalFailed(@FontRequestFailReason int reason); + + /** + * Call {@link #onFontRetrieved(Typeface)} on the handler given, or the Ui Thread if it is + * null. + * @hide + */ + @RestrictTo(LIBRARY_GROUP) + public final void callbackSuccessAsync(final Typeface typeface, @Nullable Handler handler) { + if (handler == null) { + handler = new Handler(Looper.getMainLooper()); + } + handler.post(new Runnable() { + @Override + public void run() { + onFontRetrieved(typeface); + } + }); + } + + /** + * Call {@link #onFontRetrievalFailed(int)} on the handler given, or the Ui Thread if it is + * null. + * @hide + */ + @RestrictTo(LIBRARY_GROUP) + public final void callbackFailAsync( + @FontRequestFailReason final int reason, @Nullable Handler handler) { + if (handler == null) { + handler = new Handler(Looper.getMainLooper()); + } + handler.post(new Runnable() { + @Override + public void run() { + onFontRetrievalFailed(reason); + } + }); + } } - /** @hide */ + /** + * Returns a font Typeface associated with a particular resource ID asynchronously. + * <p> + * Prior to API level 23, font resources with more than one font in a family will only load the + * font closest to a regular weight typeface. + * </p> + * + * @param context A context to retrieve the Resources from. + * @param id The desired resource identifier of a {@link Typeface}, as generated by the aapt + * tool. This integer encodes the package, type, and resource entry. The value 0 is an + * invalid identifier. + * @param fontCallback A callback to receive async fetching of this font. The callback will be + * triggered on the UI thread. + * @param handler A handler for the thread the callback should be called on. If null, the + * callback will be called on the UI thread. + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + */ + public static void getFont(@NonNull Context context, @FontRes int id, + @NonNull FontCallback fontCallback, @Nullable Handler handler) + throws NotFoundException { + Preconditions.checkNotNull(fontCallback); + if (context.isRestricted()) { + fontCallback.callbackFailAsync( + FontRequestCallback.FAIL_REASON_SECURITY_VIOLATION, handler); + return; + } + loadFont(context, id, new TypedValue(), Typeface.NORMAL, fontCallback, handler, + false /* isXmlRequest */); + } + + /** + * Used by TintTypedArray. + * + * @hide + */ @RestrictTo(LIBRARY_GROUP) public static Typeface getFont(@NonNull Context context, @FontRes int id, TypedValue value, - int style, @Nullable TextView targetView) throws NotFoundException { + int style) throws NotFoundException { if (context.isRestricted()) { return null; } - return loadFont(context, id, value, style, targetView); + return loadFont(context, id, value, style, null /* callback */, null /* handler */, + true /* isXmlRequest */); } + /** + * + * @param context The Context to get Resources from + * @param id The Resource id to load + * @param value A TypedValue to use in the fetching + * @param style The font style to load + * @param fontCallback A callback to trigger when the font is fetched or an error occurs + * @param handler A handler to the thread the callback should be called on + * @param isRequestFromLayoutInflator Whether this request originated from XML. This is used to + * determine if we use or ignore the fontProviderFetchStrategy attribute in + * font provider XML fonts. + * @return + */ private static Typeface loadFont(@NonNull Context context, int id, TypedValue value, - int style, @Nullable TextView targetView) { + int style, @Nullable FontCallback fontCallback, @Nullable Handler handler, + boolean isRequestFromLayoutInflator) { final Resources resources = context.getResources(); resources.getValue(id, value, true); - Typeface typeface = loadFont(context, resources, value, id, style, targetView); - if (typeface != null) { - return typeface; + Typeface typeface = loadFont(context, resources, value, id, style, fontCallback, handler, + isRequestFromLayoutInflator); + if (typeface == null && fontCallback == null) { + throw new NotFoundException("Font resource ID #0x" + + Integer.toHexString(id) + " could not be retrieved."); } - throw new NotFoundException("Font resource ID #0x" - + Integer.toHexString(id)); + return typeface; } + /** + * Load the given font. This method will always return null for asynchronous requests, which + * provide a fontCallback, as there is no immediate result. When the callback is not provided, + * the request is treated as synchronous and fails if async loading is required. + */ private static Typeface loadFont( @NonNull Context context, Resources wrapper, TypedValue value, int id, int style, - @Nullable TextView targetView) { + @Nullable FontCallback fontCallback, @Nullable Handler handler, + boolean isRequestFromLayoutInflator) { if (value.string == null) { throw new NotFoundException("Resource \"" + wrapper.getResourceName(id) + "\" (" + Integer.toHexString(id) + ") is not a Font: " + value); @@ -228,13 +358,20 @@ public final class ResourcesCompat { final String file = value.string.toString(); if (!file.startsWith("res/")) { - // Early exit if the specified string is unlikely to the resource path. + // Early exit if the specified string is unlikely to be a resource path. + if (fontCallback != null) { + fontCallback.callbackFailAsync( + FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler); + } return null; } + Typeface typeface = TypefaceCompat.findFromCache(wrapper, id, style); - Typeface cached = TypefaceCompat.findFromCache(wrapper, id, style); - if (cached != null) { - return cached; + if (typeface != null) { + if (fontCallback != null) { + fontCallback.callbackSuccessAsync(typeface, handler); + } + return typeface; } try { @@ -244,17 +381,35 @@ public final class ResourcesCompat { FontResourcesParserCompat.parse(rp, wrapper); if (familyEntry == null) { Log.e(TAG, "Failed to find font-family tag"); + if (fontCallback != null) { + fontCallback.callbackFailAsync( + FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler); + } return null; } - return TypefaceCompat.createFromResourcesFamilyXml( - context, familyEntry, wrapper, id, style, targetView); + return TypefaceCompat.createFromResourcesFamilyXml(context, familyEntry, wrapper, + id, style, fontCallback, handler, isRequestFromLayoutInflator); + } + typeface = TypefaceCompat.createFromResourcesFontFile( + context, wrapper, id, file, style); + if (fontCallback != null) { + if (typeface != null) { + fontCallback.callbackSuccessAsync(typeface, handler); + } else { + fontCallback.callbackFailAsync( + FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler); + } } - return TypefaceCompat.createFromResourcesFontFile(context, wrapper, id, file, style); + return typeface; } catch (XmlPullParserException e) { Log.e(TAG, "Failed to parse xml resource " + file, e); } catch (IOException e) { Log.e(TAG, "Failed to read xml resource " + file, e); } + if (fontCallback != null) { + fontCallback.callbackFailAsync( + FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler); + } return null; } diff --git a/android/support/v4/content/res/TypedArrayUtils.java b/android/support/v4/content/res/TypedArrayUtils.java index e4d6b292..64cb981b 100644 --- a/android/support/v4/content/res/TypedArrayUtils.java +++ b/android/support/v4/content/res/TypedArrayUtils.java @@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable; import android.support.annotation.AnyRes; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.annotation.StyleableRes; import android.util.AttributeSet; @@ -80,7 +81,7 @@ public class TypedArrayUtils { * {@code defaultValue} if it does not exist. */ public static boolean getNamedBoolean(@NonNull TypedArray a, @NonNull XmlPullParser parser, - String attrName, @StyleableRes int resId, boolean defaultValue) { + @NonNull String attrName, @StyleableRes int resId, boolean defaultValue) { final boolean hasAttr = hasAttribute(parser, attrName); if (!hasAttr) { return defaultValue; @@ -97,7 +98,7 @@ public class TypedArrayUtils { * {@code defaultValue} if it does not exist. */ public static int getNamedInt(@NonNull TypedArray a, @NonNull XmlPullParser parser, - String attrName, @StyleableRes int resId, int defaultValue) { + @NonNull String attrName, @StyleableRes int resId, int defaultValue) { final boolean hasAttr = hasAttribute(parser, attrName); if (!hasAttr) { return defaultValue; @@ -115,7 +116,7 @@ public class TypedArrayUtils { */ @ColorInt public static int getNamedColor(@NonNull TypedArray a, @NonNull XmlPullParser parser, - String attrName, @StyleableRes int resId, @ColorInt int defaultValue) { + @NonNull String attrName, @StyleableRes int resId, @ColorInt int defaultValue) { final boolean hasAttr = hasAttribute(parser, attrName); if (!hasAttr) { return defaultValue; @@ -133,7 +134,7 @@ public class TypedArrayUtils { */ @AnyRes public static int getNamedResourceId(@NonNull TypedArray a, @NonNull XmlPullParser parser, - String attrName, @StyleableRes int resId, @AnyRes int defaultValue) { + @NonNull String attrName, @StyleableRes int resId, @AnyRes int defaultValue) { final boolean hasAttr = hasAttribute(parser, attrName); if (!hasAttr) { return defaultValue; @@ -149,8 +150,9 @@ public class TypedArrayUtils { * @return a string value in the {@link TypedArray} with the specified {@code resId}, or * null if it does not exist. */ + @Nullable public static String getNamedString(@NonNull TypedArray a, @NonNull XmlPullParser parser, - String attrName, @StyleableRes int resId) { + @NonNull String attrName, @StyleableRes int resId) { final boolean hasAttr = hasAttribute(parser, attrName); if (!hasAttr) { return null; @@ -164,8 +166,9 @@ public class TypedArrayUtils { * and return a temporary object holding its data. This object is only * valid until the next call on to {@link TypedArray}. */ - public static TypedValue peekNamedValue(TypedArray a, XmlPullParser parser, String attrName, - int resId) { + @Nullable + public static TypedValue peekNamedValue(@NonNull TypedArray a, @NonNull XmlPullParser parser, + @NonNull String attrName, int resId) { final boolean hasAttr = hasAttribute(parser, attrName); if (!hasAttr) { return null; @@ -178,8 +181,9 @@ public class TypedArrayUtils { * Obtains styled attributes from the theme, if available, or unstyled * resources if the theme is null. */ - public static TypedArray obtainAttributes( - Resources res, Resources.Theme theme, AttributeSet set, int[] attrs) { + @NonNull + public static TypedArray obtainAttributes(@NonNull Resources res, + @Nullable Resources.Theme theme, @NonNull AttributeSet set, @NonNull int[] attrs) { if (theme == null) { return res.obtainAttributes(set, attrs); } @@ -190,7 +194,7 @@ public class TypedArrayUtils { * @return a boolean value of {@code index}. If it does not exist, a boolean value of * {@code fallbackIndex}. If it still does not exist, {@code defaultValue}. */ - public static boolean getBoolean(TypedArray a, @StyleableRes int index, + public static boolean getBoolean(@NonNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex, boolean defaultValue) { boolean val = a.getBoolean(fallbackIndex, defaultValue); return a.getBoolean(index, val); @@ -200,7 +204,8 @@ public class TypedArrayUtils { * @return a drawable value of {@code index}. If it does not exist, a drawable value of * {@code fallbackIndex}. If it still does not exist, {@code null}. */ - public static Drawable getDrawable(TypedArray a, @StyleableRes int index, + @Nullable + public static Drawable getDrawable(@NonNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex) { Drawable val = a.getDrawable(index); if (val == null) { @@ -213,7 +218,7 @@ public class TypedArrayUtils { * @return an int value of {@code index}. If it does not exist, an int value of * {@code fallbackIndex}. If it still does not exist, {@code defaultValue}. */ - public static int getInt(TypedArray a, @StyleableRes int index, + public static int getInt(@NonNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex, int defaultValue) { int val = a.getInt(fallbackIndex, defaultValue); return a.getInt(index, val); @@ -224,7 +229,7 @@ public class TypedArrayUtils { * {@code fallbackIndex}. If it still does not exist, {@code defaultValue}. */ @AnyRes - public static int getResourceId(TypedArray a, @StyleableRes int index, + public static int getResourceId(@NonNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex, @AnyRes int defaultValue) { int val = a.getResourceId(fallbackIndex, defaultValue); return a.getResourceId(index, val); @@ -234,7 +239,8 @@ public class TypedArrayUtils { * @return a string value of {@code index}. If it does not exist, a string value of * {@code fallbackIndex}. If it still does not exist, {@code null}. */ - public static String getString(TypedArray a, @StyleableRes int index, + @Nullable + public static String getString(@NonNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex) { String val = a.getString(index); if (val == null) { @@ -249,7 +255,8 @@ public class TypedArrayUtils { * @return a text value of {@code index}. If it does not exist, a text value of * {@code fallbackIndex}. If it still does not exist, {@code null}. */ - public static CharSequence getText(TypedArray a, @StyleableRes int index, + @Nullable + public static CharSequence getText(@NonNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex) { CharSequence val = a.getText(index); if (val == null) { @@ -264,7 +271,8 @@ public class TypedArrayUtils { * @return a string array value of {@code index}. If it does not exist, a string array value * of {@code fallbackIndex}. If it still does not exist, {@code null}. */ - public static CharSequence[] getTextArray(TypedArray a, @StyleableRes int index, + @Nullable + public static CharSequence[] getTextArray(@NonNull TypedArray a, @StyleableRes int index, @StyleableRes int fallbackIndex) { CharSequence[] val = a.getTextArray(index); if (val == null) { @@ -277,7 +285,7 @@ public class TypedArrayUtils { * @return The resource ID value in the {@code context} specified by {@code attr}. If it does * not exist, {@code fallbackAttr}. */ - public static int getAttr(Context context, int attr, int fallbackAttr) { + public static int getAttr(@NonNull Context context, int attr, int fallbackAttr) { TypedValue value = new TypedValue(); context.getTheme().resolveAttribute(attr, value, true); if (value.resourceId != 0) { diff --git a/android/support/v4/graphics/BitmapCompat.java b/android/support/v4/graphics/BitmapCompat.java index 20caf80a..acc37b1f 100644 --- a/android/support/v4/graphics/BitmapCompat.java +++ b/android/support/v4/graphics/BitmapCompat.java @@ -17,6 +17,7 @@ package android.support.v4.graphics; import android.graphics.Bitmap; import android.os.Build; +import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; /** @@ -71,11 +72,11 @@ public final class BitmapCompat { } } - public static boolean hasMipMap(Bitmap bitmap) { + public static boolean hasMipMap(@NonNull Bitmap bitmap) { return IMPL.hasMipMap(bitmap); } - public static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + public static void setHasMipMap(@NonNull Bitmap bitmap, boolean hasMipMap) { IMPL.setHasMipMap(bitmap, hasMipMap); } @@ -86,7 +87,7 @@ public final class BitmapCompat { * @param bitmap the bitmap in which to return its allocation size * @return the allocation size in bytes */ - public static int getAllocationByteCount(Bitmap bitmap) { + public static int getAllocationByteCount(@NonNull Bitmap bitmap) { return IMPL.getAllocationByteCount(bitmap); } diff --git a/android/support/v4/graphics/TypefaceCompat.java b/android/support/v4/graphics/TypefaceCompat.java index 6d114b69..3c55df62 100644 --- a/android/support/v4/graphics/TypefaceCompat.java +++ b/android/support/v4/graphics/TypefaceCompat.java @@ -23,16 +23,18 @@ import android.content.res.Resources; import android.graphics.Typeface; import android.os.Build; import android.os.CancellationSignal; +import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; +import android.support.v4.content.res.FontResourcesParserCompat; import android.support.v4.content.res.FontResourcesParserCompat.FamilyResourceEntry; import android.support.v4.content.res.FontResourcesParserCompat.FontFamilyFilesResourceEntry; import android.support.v4.content.res.FontResourcesParserCompat.ProviderResourceEntry; +import android.support.v4.content.res.ResourcesCompat; import android.support.v4.provider.FontsContractCompat; import android.support.v4.provider.FontsContractCompat.FontInfo; import android.support.v4.util.LruCache; -import android.widget.TextView; /** * Helper for accessing features in {@link Typeface}. @@ -82,7 +84,8 @@ public class TypefaceCompat { * * @return null if not found. */ - public static Typeface findFromCache(Resources resources, int id, int style) { + @Nullable + public static Typeface findFromCache(@NonNull Resources resources, int id, int style) { return sTypefaceCache.get(createResourceUid(resources, id, style)); } @@ -103,18 +106,35 @@ public class TypefaceCompat { * * @return null if failed to create. */ + @Nullable public static Typeface createFromResourcesFamilyXml( - Context context, FamilyResourceEntry entry, Resources resources, int id, int style, - @Nullable TextView targetView) { + @NonNull Context context, @NonNull FamilyResourceEntry entry, + @NonNull Resources resources, int id, int style, + @Nullable ResourcesCompat.FontCallback fontCallback, @Nullable Handler handler, + boolean isRequestFromLayoutInflator) { Typeface typeface; if (entry instanceof ProviderResourceEntry) { ProviderResourceEntry providerEntry = (ProviderResourceEntry) entry; - typeface = FontsContractCompat.getFontSync(context, - providerEntry.getRequest(), targetView, providerEntry.getFetchStrategy(), - providerEntry.getTimeout(), style); + final boolean isBlocking = isRequestFromLayoutInflator + ? providerEntry.getFetchStrategy() + == FontResourcesParserCompat.FETCH_STRATEGY_BLOCKING + : fontCallback == null; + final int timeout = isRequestFromLayoutInflator ? providerEntry.getTimeout() + : FontResourcesParserCompat.INFINITE_TIMEOUT_VALUE; + typeface = FontsContractCompat.getFontSync(context, providerEntry.getRequest(), + fontCallback, handler, isBlocking, timeout, style); } else { typeface = sTypefaceCompatImpl.createFromFontFamilyFilesResourceEntry( context, (FontFamilyFilesResourceEntry) entry, resources, style); + if (fontCallback != null) { + if (typeface != null) { + fontCallback.callbackSuccessAsync(typeface, handler); + } else { + fontCallback.callbackFailAsync( + FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, + handler); + } + } } if (typeface != null) { sTypefaceCache.put(createResourceUid(resources, id, style), typeface); @@ -127,11 +147,13 @@ public class TypefaceCompat { */ @Nullable public static Typeface createFromResourcesFontFile( - Context context, Resources resources, int id, String path, int style) { + @NonNull Context context, @NonNull Resources resources, int id, String path, + int style) { Typeface typeface = sTypefaceCompatImpl.createFromResourcesFontFile( context, resources, id, path, style); if (typeface != null) { - sTypefaceCache.put(createResourceUid(resources, id, style), typeface); + final String resourceUid = createResourceUid(resources, id, style); + sTypefaceCache.put(resourceUid, typeface); } return typeface; } @@ -139,7 +161,8 @@ public class TypefaceCompat { /** * Create a Typeface from a given FontInfo list and a map that matches them to ByteBuffers. */ - public static Typeface createFromFontInfo(Context context, + @Nullable + public static Typeface createFromFontInfo(@NonNull Context context, @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts, int style) { return sTypefaceCompatImpl.createFromFontInfo(context, cancellationSignal, fonts, style); } diff --git a/android/support/v4/graphics/TypefaceCompatApi24Impl.java b/android/support/v4/graphics/TypefaceCompatApi24Impl.java index a107859e..89a6ec40 100644 --- a/android/support/v4/graphics/TypefaceCompatApi24Impl.java +++ b/android/support/v4/graphics/TypefaceCompatApi24Impl.java @@ -145,7 +145,8 @@ class TypefaceCompatApi24Impl extends TypefaceCompatBaseImpl { return null; } } - return createFromFamiliesWithDefault(family); + final Typeface typeface = createFromFamiliesWithDefault(family); + return Typeface.create(typeface, style); } @Override diff --git a/android/support/v4/graphics/TypefaceCompatApi26Impl.java b/android/support/v4/graphics/TypefaceCompatApi26Impl.java index c7066c8d..1b55a2e0 100644 --- a/android/support/v4/graphics/TypefaceCompatApi26Impl.java +++ b/android/support/v4/graphics/TypefaceCompatApi26Impl.java @@ -118,7 +118,7 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { */ private static boolean isFontFamilyPrivateAPIAvailable() { if (sAddFontFromAssetManager == null) { - Log.w(TAG, "Unable to collect necessary private methods." + Log.w(TAG, "Unable to collect necessary private methods. " + "Fallback to legacy implementation."); } return sAddFontFromAssetManager != null; @@ -273,7 +273,8 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl { if (!freeze(fontFamily)) { return null; } - return createFromFamiliesWithDefault(fontFamily); + final Typeface typeface = createFromFamiliesWithDefault(fontFamily); + return Typeface.create(typeface, style); } /** diff --git a/android/support/v4/graphics/drawable/IconCompat.java b/android/support/v4/graphics/drawable/IconCompat.java index 0a99fede..359c96b3 100644 --- a/android/support/v4/graphics/drawable/IconCompat.java +++ b/android/support/v4/graphics/drawable/IconCompat.java @@ -18,6 +18,7 @@ package android.support.v4.graphics.drawable; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; +import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -27,13 +28,17 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Shader; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; +import android.support.v4.content.ContextCompat; /** * Helper for accessing features in {@link android.graphics.drawable.Icon}. @@ -202,25 +207,63 @@ public class IconCompat { } /** + * Use {@link #addToShortcutIntent(Intent, Drawable)} instead * @hide */ @RestrictTo(LIBRARY_GROUP) - public void addToShortcutIntent(Intent outIntent) { + @Deprecated + public void addToShortcutIntent(@NonNull Intent outIntent) { + addToShortcutIntent(outIntent, null); + } + + /** + * @hide + */ + @RestrictTo(LIBRARY_GROUP) + public void addToShortcutIntent(@NonNull Intent outIntent, @Nullable Drawable badge) { + Bitmap icon; switch (mType) { case TYPE_BITMAP: - outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, (Bitmap) mObj1); + icon = (Bitmap) mObj1; + if (badge != null) { + // Do not modify the original icon when applying a badge + icon = icon.copy(icon.getConfig(), true); + } break; case TYPE_ADAPTIVE_BITMAP: - outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, - createLegacyIconFromAdaptiveIcon((Bitmap) mObj1, true)); + icon = createLegacyIconFromAdaptiveIcon((Bitmap) mObj1, true); break; case TYPE_RESOURCE: - outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, - Intent.ShortcutIconResource.fromContext((Context) mObj1, mInt1)); + if (badge == null) { + outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext((Context) mObj1, mInt1)); + return; + } else { + Context context = (Context) mObj1; + Drawable dr = ContextCompat.getDrawable(context, mInt1); + if (dr.getIntrinsicWidth() <= 0 || dr.getIntrinsicHeight() <= 0) { + int size = ((ActivityManager) context.getSystemService( + Context.ACTIVITY_SERVICE)).getLauncherLargeIconSize(); + icon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + } else { + icon = Bitmap.createBitmap(dr.getIntrinsicWidth(), dr.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + } + dr.setBounds(0, 0, icon.getWidth(), icon.getHeight()); + dr.draw(new Canvas(icon)); + } break; default: throw new IllegalArgumentException("Icon type not supported for intent shortcuts"); } + if (badge != null) { + // Badge the icon + int w = icon.getWidth(); + int h = icon.getHeight(); + badge.setBounds(w / 2, h / 2, w, h); + badge.draw(new Canvas(icon)); + } + outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon); } /** diff --git a/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java b/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java index d5155613..795126d2 100644 --- a/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java +++ b/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java @@ -27,6 +27,8 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.util.DisplayMetrics; import android.view.Gravity; @@ -66,6 +68,7 @@ public abstract class RoundedBitmapDrawable extends Drawable { /** * Returns the paint used to render this drawable. */ + @NonNull public final Paint getPaint() { return mPaint; } @@ -73,6 +76,7 @@ public abstract class RoundedBitmapDrawable extends Drawable { /** * Returns the bitmap used by this drawable to render. May be null. */ + @Nullable public final Bitmap getBitmap() { return mBitmap; } @@ -92,7 +96,7 @@ public abstract class RoundedBitmapDrawable extends Drawable { * @see android.graphics.Bitmap#setDensity(int) * @see android.graphics.Bitmap#getDensity() */ - public void setTargetDensity(Canvas canvas) { + public void setTargetDensity(@NonNull Canvas canvas) { setTargetDensity(canvas.getDensity()); } @@ -104,7 +108,7 @@ public abstract class RoundedBitmapDrawable extends Drawable { * @see android.graphics.Bitmap#setDensity(int) * @see android.graphics.Bitmap#getDensity() */ - public void setTargetDensity(DisplayMetrics metrics) { + public void setTargetDensity(@NonNull DisplayMetrics metrics) { setTargetDensity(metrics.densityDpi); } @@ -253,7 +257,7 @@ public abstract class RoundedBitmapDrawable extends Drawable { } @Override - public void draw(Canvas canvas) { + public void draw(@NonNull Canvas canvas) { final Bitmap bitmap = mBitmap; if (bitmap == null) { return; diff --git a/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java b/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java index 5e144c76..7790055c 100644 --- a/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java +++ b/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java @@ -21,11 +21,15 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Rect; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.graphics.BitmapCompat; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewCompat; import android.util.Log; +import java.io.InputStream; + /** * Constructs {@link RoundedBitmapDrawable RoundedBitmapDrawable} objects, * either from Bitmaps directly, or from streams and files. @@ -63,7 +67,8 @@ public final class RoundedBitmapDrawableFactory { * Returns a new drawable by creating it from a bitmap, setting initial target density based on * the display metrics of the resources. */ - public static RoundedBitmapDrawable create(Resources res, Bitmap bitmap) { + @NonNull + public static RoundedBitmapDrawable create(@NonNull Resources res, @Nullable Bitmap bitmap) { if (Build.VERSION.SDK_INT >= 21) { return new RoundedBitmapDrawable21(res, bitmap); } @@ -73,8 +78,8 @@ public final class RoundedBitmapDrawableFactory { /** * Returns a new drawable, creating it by opening a given file path and decoding the bitmap. */ - public static RoundedBitmapDrawable create(Resources res, - String filepath) { + @NonNull + public static RoundedBitmapDrawable create(@NonNull Resources res, @NonNull String filepath) { final RoundedBitmapDrawable drawable = create(res, BitmapFactory.decodeFile(filepath)); if (drawable.getBitmap() == null) { Log.w(TAG, "RoundedBitmapDrawable cannot decode " + filepath); @@ -86,8 +91,8 @@ public final class RoundedBitmapDrawableFactory { /** * Returns a new drawable, creating it by decoding a bitmap from the given input stream. */ - public static RoundedBitmapDrawable create(Resources res, - java.io.InputStream is) { + @NonNull + public static RoundedBitmapDrawable create(@NonNull Resources res, @NonNull InputStream is) { final RoundedBitmapDrawable drawable = create(res, BitmapFactory.decodeStream(is)); if (drawable.getBitmap() == null) { Log.w(TAG, "RoundedBitmapDrawable cannot decode " + is); diff --git a/android/support/v4/hardware/display/DisplayManagerCompat.java b/android/support/v4/hardware/display/DisplayManagerCompat.java index 50d246be..22d39acb 100644 --- a/android/support/v4/hardware/display/DisplayManagerCompat.java +++ b/android/support/v4/hardware/display/DisplayManagerCompat.java @@ -19,6 +19,8 @@ package android.support.v4.hardware.display; import android.content.Context; import android.hardware.display.DisplayManager; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.view.Display; import android.view.WindowManager; @@ -52,7 +54,8 @@ public abstract class DisplayManagerCompat { /** * Gets an instance of the display manager given the context. */ - public static DisplayManagerCompat getInstance(Context context) { + @NonNull + public static DisplayManagerCompat getInstance(@NonNull Context context) { synchronized (sInstances) { DisplayManagerCompat instance = sInstances.get(context); if (instance == null) { @@ -76,6 +79,7 @@ public abstract class DisplayManagerCompat { * @param displayId The logical display id. * @return The display object, or null if there is no valid display with the given id. */ + @Nullable public abstract Display getDisplay(int displayId); /** @@ -83,6 +87,7 @@ public abstract class DisplayManagerCompat { * * @return An array containing all displays. */ + @NonNull public abstract Display[] getDisplays(); /** @@ -101,6 +106,7 @@ public abstract class DisplayManagerCompat { * * @see #DISPLAY_CATEGORY_PRESENTATION */ + @NonNull public abstract Display[] getDisplays(String category); private static class DisplayManagerCompatApi14Impl extends DisplayManagerCompat { diff --git a/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java b/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java index 5e23c689..68f94768 100644 --- a/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java +++ b/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java @@ -44,7 +44,8 @@ public final class FingerprintManagerCompat { private final Context mContext; /** Get a {@link FingerprintManagerCompat} instance for a provided context. */ - public static FingerprintManagerCompat from(Context context) { + @NonNull + public static FingerprintManagerCompat from(@NonNull Context context) { return new FingerprintManagerCompat(context); } @@ -119,8 +120,9 @@ public final class FingerprintManagerCompat { } } + @Nullable @RequiresApi(23) - private static FingerprintManager getFingerprintManagerOrNull(Context context) { + private static FingerprintManager getFingerprintManagerOrNull(@NonNull Context context) { if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { return context.getSystemService(FingerprintManager.class); } else { @@ -195,20 +197,20 @@ public final class FingerprintManagerCompat { private final Cipher mCipher; private final Mac mMac; - public CryptoObject(Signature signature) { + public CryptoObject(@NonNull Signature signature) { mSignature = signature; mCipher = null; mMac = null; } - public CryptoObject(Cipher cipher) { + public CryptoObject(@NonNull Cipher cipher) { mCipher = cipher; mSignature = null; mMac = null; } - public CryptoObject(Mac mac) { + public CryptoObject(@NonNull Mac mac) { mMac = mac; mCipher = null; mSignature = null; @@ -218,18 +220,21 @@ public final class FingerprintManagerCompat { * Get {@link Signature} object. * @return {@link Signature} object or null if this doesn't contain one. */ + @Nullable public Signature getSignature() { return mSignature; } /** * Get {@link Cipher} object. * @return {@link Cipher} object or null if this doesn't contain one. */ + @Nullable public Cipher getCipher() { return mCipher; } /** * Get {@link Mac} object. * @return {@link Mac} object or null if this doesn't contain one. */ + @Nullable public Mac getMac() { return mMac; } } diff --git a/android/support/v4/media/session/MediaControllerCompat.java b/android/support/v4/media/session/MediaControllerCompat.java index cea4771a..2509cd49 100644 --- a/android/support/v4/media/session/MediaControllerCompat.java +++ b/android/support/v4/media/session/MediaControllerCompat.java @@ -70,7 +70,7 @@ import java.util.List; * <li>{@link #getPlaybackState()}.{@link PlaybackStateCompat#getExtras() getExtras()}</li> * <li>{@link #isCaptioningEnabled()}</li> * <li>{@link #getRepeatMode()}</li> - * <li>{@link #isShuffleModeEnabled()}</li> + * <li>{@link #getShuffleMode()}</li> * </ul></p> * * <div class="special reference"> @@ -439,18 +439,6 @@ public final class MediaControllerCompat { } /** - * Returns whether the shuffle mode is enabled for this session. - * - * @return {@code true} if the shuffle mode is enabled, {@code false} if it is disabled, not - * set, or the session is not ready. - * @deprecated Use {@link #getShuffleMode} instead. - */ - @Deprecated - public boolean isShuffleModeEnabled() { - return mImpl.isShuffleModeEnabled(); - } - - /** * Gets the shuffle mode for this session. * * @return The latest shuffle mode set to the session, or @@ -760,16 +748,6 @@ public final class MediaControllerCompat { /** * Override to handle changes to the shuffle mode. * - * @param enabled {@code true} if the shuffle mode is enabled, {@code false} otherwise. - * @deprecated Use {@link #onShuffleModeChanged(int)} instead. - */ - @Deprecated - public void onShuffleModeChanged(boolean enabled) { - } - - /** - * Override to handle changes to the shuffle mode. - * * @param shuffleMode The shuffle mode. Must be one of the followings: * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE}, * {@link PlaybackStateCompat#SHUFFLE_MODE_ALL}, @@ -963,12 +941,8 @@ public final class MediaControllerCompat { } @Override - public void onShuffleModeChangedDeprecated(boolean enabled) throws RemoteException { - MediaControllerCompat.Callback callback = mCallback.get(); - if (callback != null) { - callback.postToHandler( - MessageHandler.MSG_UPDATE_SHUFFLE_MODE_DEPRECATED, enabled, null); - } + public void onShuffleModeChangedRemoved(boolean enabled) throws RemoteException { + // Do nothing. } @Override @@ -1020,7 +994,6 @@ public final class MediaControllerCompat { private static final int MSG_UPDATE_EXTRAS = 7; private static final int MSG_DESTROYED = 8; private static final int MSG_UPDATE_REPEAT_MODE = 9; - private static final int MSG_UPDATE_SHUFFLE_MODE_DEPRECATED = 10; private static final int MSG_UPDATE_CAPTIONING_ENABLED = 11; private static final int MSG_UPDATE_SHUFFLE_MODE = 12; private static final int MSG_SESSION_READY = 13; @@ -1058,9 +1031,6 @@ public final class MediaControllerCompat { case MSG_UPDATE_REPEAT_MODE: onRepeatModeChanged((int) msg.obj); break; - case MSG_UPDATE_SHUFFLE_MODE_DEPRECATED: - onShuffleModeChanged((boolean) msg.obj); - break; case MSG_UPDATE_SHUFFLE_MODE: onShuffleModeChanged((int) msg.obj); break; @@ -1264,15 +1234,6 @@ public final class MediaControllerCompat { /** * Sets the shuffle mode for this session. * - * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable. - * @deprecated Use {@link #setShuffleMode} instead. - */ - @Deprecated - public abstract void setShuffleModeEnabled(boolean enabled); - - /** - * Sets the shuffle mode for this session. - * * @param shuffleMode The shuffle mode. Must be one of the followings: * {@link PlaybackStateCompat#SHUFFLE_MODE_NONE}, * {@link PlaybackStateCompat#SHUFFLE_MODE_ALL}, @@ -1414,7 +1375,6 @@ public final class MediaControllerCompat { int getRatingType(); boolean isCaptioningEnabled(); int getRepeatMode(); - boolean isShuffleModeEnabled(); int getShuffleMode(); long getFlags(); PlaybackInfo getPlaybackInfo(); @@ -1610,16 +1570,6 @@ public final class MediaControllerCompat { } @Override - public boolean isShuffleModeEnabled() { - try { - return mBinder.isShuffleModeEnabledDeprecated(); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in isShuffleModeEnabled.", e); - } - return false; - } - - @Override public int getShuffleMode() { try { return mBinder.getShuffleMode(); @@ -1899,15 +1849,6 @@ public final class MediaControllerCompat { } @Override - public void setShuffleModeEnabled(boolean enabled) { - try { - mBinder.setShuffleModeEnabledDeprecated(enabled); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in setShuffleModeEnabled.", e); - } - } - - @Override public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { try { mBinder.setShuffleMode(shuffleMode); @@ -2122,18 +2063,6 @@ public final class MediaControllerCompat { } @Override - public boolean isShuffleModeEnabled() { - if (mExtraBinder != null) { - try { - return mExtraBinder.isShuffleModeEnabledDeprecated(); - } catch (RemoteException e) { - Log.e(TAG, "Dead object in isShuffleModeEnabled.", e); - } - } - return false; - } - - @Override public int getShuffleMode() { if (mExtraBinder != null) { try { @@ -2391,13 +2320,6 @@ public final class MediaControllerCompat { } @Override - public void setShuffleModeEnabled(boolean enabled) { - Bundle bundle = new Bundle(); - bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED, enabled); - sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE_ENABLED, bundle); - } - - @Override public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { Bundle bundle = new Bundle(); bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE, shuffleMode); diff --git a/android/support/v4/media/session/MediaSessionCompat.java b/android/support/v4/media/session/MediaSessionCompat.java index ea5bedae..8b91413d 100644 --- a/android/support/v4/media/session/MediaSessionCompat.java +++ b/android/support/v4/media/session/MediaSessionCompat.java @@ -259,12 +259,6 @@ public class MediaSessionCompat { "android.support.v4.media.session.action.SET_REPEAT_MODE"; /** - * Custom action to invoke setShuffleModeEnabled() for the forward compatibility. - */ - static final String ACTION_SET_SHUFFLE_MODE_ENABLED = - "android.support.v4.media.session.action.SET_SHUFFLE_MODE_ENABLED"; - - /** * Custom action to invoke setShuffleMode() for the forward compatibility. */ static final String ACTION_SET_SHUFFLE_MODE = @@ -321,13 +315,6 @@ public class MediaSessionCompat { "android.support.v4.media.session.action.ARGUMENT_REPEAT_MODE"; /** - * Argument for use with {@link #ACTION_SET_SHUFFLE_MODE_ENABLED} indicating that shuffle mode - * is enabled. - */ - static final String ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED = - "android.support.v4.media.session.action.ARGUMENT_SHUFFLE_MODE_ENABLED"; - - /** * Argument for use with {@link #ACTION_SET_SHUFFLE_MODE} indicating shuffle mode. */ static final String ACTION_ARGUMENT_SHUFFLE_MODE = @@ -704,20 +691,6 @@ public class MediaSessionCompat { /** * Sets the shuffle mode for this session. * <p> - * Note that if this method is not called before, - * {@link MediaControllerCompat#isShuffleModeEnabled} will return {@code false}. - * - * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable. - * @deprecated Use {@link #setShuffleMode} instead. - */ - @Deprecated - public void setShuffleModeEnabled(boolean enabled) { - mImpl.setShuffleModeEnabled(enabled); - } - - /** - * Sets the shuffle mode for this session. - * <p> * Note that if this method is not called before, {@link MediaControllerCompat#getShuffleMode} * will return {@link PlaybackStateCompat#SHUFFLE_MODE_NONE}. * @@ -1134,20 +1107,6 @@ public class MediaSessionCompat { /** * Override to handle the setting of the shuffle mode. * <p> - * You should call {@link #setShuffleModeEnabled} before the end of this method in order to - * notify the change to the {@link MediaControllerCompat}, or - * {@link MediaControllerCompat#isShuffleModeEnabled} could return an invalid value. - * - * @param enabled true when the shuffle mode is enabled, false otherwise. - * @deprecated Use {@link #onSetShuffleMode} instead. - */ - @Deprecated - public void onSetShuffleModeEnabled(boolean enabled) { - } - - /** - * Override to handle the setting of the shuffle mode. - * <p> * You should call {@link #setShuffleMode} before the end of this method in order to * notify the change to the {@link MediaControllerCompat}, or * {@link MediaControllerCompat#getShuffleMode} could return an invalid value. @@ -1386,9 +1345,6 @@ public class MediaSessionCompat { } else if (action.equals(ACTION_SET_REPEAT_MODE)) { int repeatMode = extras.getInt(ACTION_ARGUMENT_REPEAT_MODE); Callback.this.onSetRepeatMode(repeatMode); - } else if (action.equals(ACTION_SET_SHUFFLE_MODE_ENABLED)) { - boolean enabled = extras.getBoolean(ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED); - Callback.this.onSetShuffleModeEnabled(enabled); } else if (action.equals(ACTION_SET_SHUFFLE_MODE)) { int shuffleMode = extras.getInt(ACTION_ARGUMENT_SHUFFLE_MODE); Callback.this.onSetShuffleMode(shuffleMode); @@ -1797,7 +1753,6 @@ public class MediaSessionCompat { void setRatingType(@RatingCompat.Style int type); void setCaptioningEnabled(boolean enabled); void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode); - void setShuffleModeEnabled(boolean enabled); void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode); void setExtras(Bundle extras); @@ -1844,7 +1799,6 @@ public class MediaSessionCompat { boolean mCaptioningEnabled; @PlaybackStateCompat.RepeatMode int mRepeatMode; @PlaybackStateCompat.ShuffleMode int mShuffleMode; - boolean mShuffleModeEnabled; Bundle mExtras; int mVolumeType; @@ -2254,14 +2208,6 @@ public class MediaSessionCompat { } @Override - public void setShuffleModeEnabled(boolean enabled) { - if (mShuffleModeEnabled != enabled) { - mShuffleModeEnabled = enabled; - sendShuffleModeEnabled(enabled); - } - } - - @Override public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { if (mShuffleMode != shuffleMode) { mShuffleMode = shuffleMode; @@ -2460,18 +2406,6 @@ public class MediaSessionCompat { mControllerCallbacks.finishBroadcast(); } - private void sendShuffleModeEnabled(boolean enabled) { - int size = mControllerCallbacks.beginBroadcast(); - for (int i = size - 1; i >= 0; i--) { - IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); - try { - cb.onShuffleModeChangedDeprecated(enabled); - } catch (RemoteException e) { - } - } - mControllerCallbacks.finishBroadcast(); - } - private void sendShuffleMode(int shuffleMode) { int size = mControllerCallbacks.beginBroadcast(); for (int i = size - 1; i >= 0; i--) { @@ -2695,8 +2629,8 @@ public class MediaSessionCompat { } @Override - public void setShuffleModeEnabledDeprecated(boolean enabled) throws RemoteException { - postToHandler(MessageHandler.MSG_SET_SHUFFLE_MODE_ENABLED, enabled); + public void setShuffleModeEnabledRemoved(boolean enabled) throws RemoteException { + // Do nothing. } @Override @@ -2783,8 +2717,8 @@ public class MediaSessionCompat { } @Override - public boolean isShuffleModeEnabledDeprecated() { - return mShuffleModeEnabled; + public boolean isShuffleModeEnabledRemoved() { + return false; } @Override @@ -2837,7 +2771,6 @@ public class MediaSessionCompat { private static final int MSG_MEDIA_BUTTON = 21; private static final int MSG_SET_VOLUME = 22; private static final int MSG_SET_REPEAT_MODE = 23; - private static final int MSG_SET_SHUFFLE_MODE_ENABLED = 24; private static final int MSG_ADD_QUEUE_ITEM = 25; private static final int MSG_ADD_QUEUE_ITEM_AT = 26; private static final int MSG_REMOVE_QUEUE_ITEM = 27; @@ -2978,9 +2911,6 @@ public class MediaSessionCompat { case MSG_SET_REPEAT_MODE: cb.onSetRepeatMode(msg.arg1); break; - case MSG_SET_SHUFFLE_MODE_ENABLED: - cb.onSetShuffleModeEnabled((boolean) msg.obj); - break; case MSG_SET_SHUFFLE_MODE: cb.onSetShuffleMode(msg.arg1); break; @@ -3206,7 +3136,6 @@ public class MediaSessionCompat { @RatingCompat.Style int mRatingType; boolean mCaptioningEnabled; @PlaybackStateCompat.RepeatMode int mRepeatMode; - boolean mShuffleModeEnabled; @PlaybackStateCompat.ShuffleMode int mShuffleMode; public MediaSessionImplApi21(Context context, String tag) { @@ -3381,22 +3310,6 @@ public class MediaSessionCompat { } @Override - public void setShuffleModeEnabled(boolean enabled) { - if (mShuffleModeEnabled != enabled) { - mShuffleModeEnabled = enabled; - int size = mExtraControllerCallbacks.beginBroadcast(); - for (int i = size - 1; i >= 0; i--) { - IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); - try { - cb.onShuffleModeChangedDeprecated(enabled); - } catch (RemoteException e) { - } - } - mExtraControllerCallbacks.finishBroadcast(); - } - } - - @Override public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { if (mShuffleMode != shuffleMode) { mShuffleMode = shuffleMode; @@ -3625,9 +3538,8 @@ public class MediaSessionCompat { } @Override - public void setShuffleModeEnabledDeprecated(boolean enabled) throws RemoteException { - // Will not be called. - throw new AssertionError(); + public void setShuffleModeEnabledRemoved(boolean enabled) throws RemoteException { + // Do nothing. } @Override @@ -3713,8 +3625,8 @@ public class MediaSessionCompat { } @Override - public boolean isShuffleModeEnabledDeprecated() { - return mShuffleModeEnabled; + public boolean isShuffleModeEnabledRemoved() { + return false; } @Override diff --git a/android/support/v4/media/session/PlaybackStateCompat.java b/android/support/v4/media/session/PlaybackStateCompat.java index eee09c5c..d7634b00 100644 --- a/android/support/v4/media/session/PlaybackStateCompat.java +++ b/android/support/v4/media/session/PlaybackStateCompat.java @@ -202,6 +202,7 @@ public final class PlaybackStateCompat implements Parcelable { * @see Builder#setActions(long) * @deprecated Use {@link #ACTION_SET_SHUFFLE_MODE} instead. */ + @Deprecated public static final long ACTION_SET_SHUFFLE_MODE_ENABLED = 1 << 19; /** diff --git a/android/support/v4/net/ConnectivityManagerCompat.java b/android/support/v4/net/ConnectivityManagerCompat.java index cdddb680..c08cac83 100644 --- a/android/support/v4/net/ConnectivityManagerCompat.java +++ b/android/support/v4/net/ConnectivityManagerCompat.java @@ -32,6 +32,8 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Build; import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresPermission; import android.support.annotation.RestrictTo; @@ -91,7 +93,7 @@ public final class ConnectivityManagerCompat { */ @SuppressWarnings("deprecation") @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) - public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + public static boolean isActiveNetworkMetered(@NonNull ConnectivityManager cm) { if (Build.VERSION.SDK_INT >= 16) { return cm.isActiveNetworkMetered(); } else { @@ -128,8 +130,10 @@ public final class ConnectivityManagerCompat { * potentially-stale value from * {@link ConnectivityManager#EXTRA_NETWORK_INFO}. May be {@code null}. */ + @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) - public static NetworkInfo getNetworkInfoFromBroadcast(ConnectivityManager cm, Intent intent) { + public static NetworkInfo getNetworkInfoFromBroadcast(@NonNull ConnectivityManager cm, + @NonNull Intent intent) { final NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); if (info != null) { return cm.getNetworkInfo(info.getType()); @@ -147,7 +151,7 @@ public final class ConnectivityManagerCompat { * or {@link #RESTRICT_BACKGROUND_STATUS_WHITELISTED} */ @RestrictBackgroundStatus - public static int getRestrictBackgroundStatus(ConnectivityManager cm) { + public static int getRestrictBackgroundStatus(@NonNull ConnectivityManager cm) { if (Build.VERSION.SDK_INT >= 24) { return cm.getRestrictBackgroundStatus(); } else { diff --git a/android/support/v4/net/TrafficStatsCompat.java b/android/support/v4/net/TrafficStatsCompat.java index 1049fa53..b74b8d02 100644 --- a/android/support/v4/net/TrafficStatsCompat.java +++ b/android/support/v4/net/TrafficStatsCompat.java @@ -19,6 +19,7 @@ package android.support.v4.net; import android.net.TrafficStats; import android.os.Build; import android.os.ParcelFileDescriptor; +import android.support.annotation.NonNull; import java.net.DatagramSocket; import java.net.Socket; @@ -131,7 +132,7 @@ public final class TrafficStatsCompat { * * @see #setThreadStatsTag(int) */ - public static void tagDatagramSocket(DatagramSocket socket) throws SocketException { + public static void tagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException { if (Build.VERSION.SDK_INT >= 24) { TrafficStats.tagDatagramSocket(socket); } else { @@ -148,7 +149,7 @@ public final class TrafficStatsCompat { /** * Remove any statistics parameters from the given {@link DatagramSocket}. */ - public static void untagDatagramSocket(DatagramSocket socket) throws SocketException { + public static void untagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException { if (Build.VERSION.SDK_INT >= 24) { TrafficStats.untagDatagramSocket(socket); } else { diff --git a/android/support/v4/provider/FontRequest.java b/android/support/v4/provider/FontRequest.java index cb32f067..b14f85e4 100644 --- a/android/support/v4/provider/FontRequest.java +++ b/android/support/v4/provider/FontRequest.java @@ -89,6 +89,7 @@ public final class FontRequest { * Returns the selected font provider's authority. This tells the system what font provider * it should request the font from. */ + @NonNull public String getProviderAuthority() { return mProviderAuthority; } @@ -97,6 +98,7 @@ public final class FontRequest { * Returns the selected font provider's package. This helps the system verify that the provider * identified by the given authority is the one requested. */ + @NonNull public String getProviderPackage() { return mProviderPackage; } @@ -105,6 +107,7 @@ public final class FontRequest { * Returns the query string. Refer to your font provider's documentation on the format of this * string. */ + @NonNull public String getQuery() { return mQuery; } diff --git a/android/support/v4/provider/FontsContractCompat.java b/android/support/v4/provider/FontsContractCompat.java index 6ad46a17..9ef1b0b0 100644 --- a/android/support/v4/provider/FontsContractCompat.java +++ b/android/support/v4/provider/FontsContractCompat.java @@ -17,7 +17,6 @@ package android.support.v4.provider; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import static android.support.v4.content.res.FontResourcesParserCompat.FetchStrategy; import android.annotation.SuppressLint; import android.content.ContentResolver; @@ -46,17 +45,16 @@ import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; import android.support.v4.content.res.FontResourcesParserCompat; +import android.support.v4.content.res.ResourcesCompat; import android.support.v4.graphics.TypefaceCompat; import android.support.v4.graphics.TypefaceCompatUtil; import android.support.v4.provider.SelfDestructiveThread.ReplyCallback; import android.support.v4.util.LruCache; import android.support.v4.util.Preconditions; import android.support.v4.util.SimpleArrayMap; -import android.widget.TextView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -166,11 +164,11 @@ public class FontsContractCompat { // space open for new provider codes, these should all be negative numbers. /** @hide */ @RestrictTo(LIBRARY_GROUP) - public static final int RESULT_CODE_PROVIDER_NOT_FOUND = -1; + /* package */ static final int RESULT_CODE_PROVIDER_NOT_FOUND = -1; /** @hide */ @RestrictTo(LIBRARY_GROUP) - public static final int RESULT_CODE_WRONG_CERTIFICATES = -2; - // Note -3 is used by Typeface to indicate the font failed to load. + /* package */ static final int RESULT_CODE_WRONG_CERTIFICATES = -2; + // Note -3 is used by FontRequestCallback to indicate the font failed to load. private static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16); @@ -179,51 +177,87 @@ public class FontsContractCompat { new SelfDestructiveThread("fonts", Process.THREAD_PRIORITY_BACKGROUND, BACKGROUND_THREAD_KEEP_ALIVE_DURATION_MS); - private static Typeface getFontInternal(final Context context, final FontRequest request, + @NonNull + private static TypefaceResult getFontInternal(final Context context, final FontRequest request, int style) { FontFamilyResult result; try { result = fetchFonts(context, null /* CancellationSignal */, request); } catch (PackageManager.NameNotFoundException e) { - return null; + return new TypefaceResult(null, FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND); } if (result.getStatusCode() == FontFamilyResult.STATUS_OK) { - return TypefaceCompat.createFromFontInfo(context, null /* CancellationSignal */, - result.getFonts(), style); + final Typeface typeface = TypefaceCompat.createFromFontInfo( + context, null /* CancellationSignal */, result.getFonts(), style); + return new TypefaceResult(typeface, typeface != null + ? FontRequestCallback.RESULT_OK + : FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR); } - return null; + int resultCode = result.getStatusCode() == FontFamilyResult.STATUS_WRONG_CERTIFICATES + ? FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES + : FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR; + return new TypefaceResult(null, resultCode); } private static final Object sLock = new Object(); @GuardedBy("sLock") - private static final SimpleArrayMap<String, ArrayList<ReplyCallback<Typeface>>> + private static final SimpleArrayMap<String, ArrayList<ReplyCallback<TypefaceResult>>> sPendingReplies = new SimpleArrayMap<>(); + private static final class TypefaceResult { + final Typeface mTypeface; + @FontRequestCallback.FontRequestFailReason final int mResult; + + TypefaceResult(@Nullable Typeface typeface, + @FontRequestCallback.FontRequestFailReason int result) { + mTypeface = typeface; + mResult = result; + } + } + + /** + * Used for tests, should not be used otherwise. + * @hide + **/ + @RestrictTo(LIBRARY_GROUP) + public static final void resetCache() { + sTypefaceCache.evictAll(); + } + /** @hide */ @RestrictTo(LIBRARY_GROUP) public static Typeface getFontSync(final Context context, final FontRequest request, - final @Nullable TextView targetView, @FetchStrategy int strategy, int timeout, + final @Nullable ResourcesCompat.FontCallback fontCallback, + final @Nullable Handler handler, boolean isBlockingFetch, int timeout, final int style) { final String id = request.getIdentifier() + "-" + style; Typeface cached = sTypefaceCache.get(id); if (cached != null) { + if (fontCallback != null) { + fontCallback.onFontRetrieved(cached); + } return cached; } - final boolean isBlockingFetch = - strategy == FontResourcesParserCompat.FETCH_STRATEGY_BLOCKING; - if (isBlockingFetch && timeout == FontResourcesParserCompat.INFINITE_TIMEOUT_VALUE) { // Wait forever. No need to post to the thread. - return getFontInternal(context, request, style); + TypefaceResult typefaceResult = getFontInternal(context, request, style); + if (fontCallback != null) { + if (typefaceResult.mResult == FontFamilyResult.STATUS_OK) { + fontCallback.callbackSuccessAsync(typefaceResult.mTypeface, handler); + } else { + fontCallback.callbackFailAsync(typefaceResult.mResult, handler); + } + } + return typefaceResult.mTypeface; } - final Callable<Typeface> fetcher = new Callable<Typeface>() { + final Callable<TypefaceResult> fetcher = new Callable<TypefaceResult>() { @Override - public Typeface call() throws Exception { - Typeface typeface = getFontInternal(context, request, style); - if (typeface != null) { - sTypefaceCache.put(id, typeface); + public TypefaceResult call() throws Exception { + TypefaceResult typeface = getFontInternal(context, request, style); + if (typeface.mTypeface != null) { + sTypefaceCache.put(id, typeface.mTypeface); } return typeface; } @@ -231,37 +265,42 @@ public class FontsContractCompat { if (isBlockingFetch) { try { - return sBackgroundThread.postAndWait(fetcher, timeout); + return sBackgroundThread.postAndWait(fetcher, timeout).mTypeface; } catch (InterruptedException e) { return null; } } else { - final WeakReference<TextView> textViewWeak = new WeakReference<TextView>(targetView); - final ReplyCallback<Typeface> reply = new ReplyCallback<Typeface>() { - @Override - public void onReply(final Typeface typeface) { - final TextView textView = textViewWeak.get(); - if (textView != null) { - targetView.setTypeface(typeface, style); - } - } - }; + final ReplyCallback<TypefaceResult> reply = fontCallback == null ? null + : new ReplyCallback<TypefaceResult>() { + @Override + public void onReply(final TypefaceResult typeface) { + if (typeface.mResult == FontFamilyResult.STATUS_OK) { + fontCallback.callbackSuccessAsync(typeface.mTypeface, handler); + } else { + fontCallback.callbackFailAsync(typeface.mResult, handler); + } + } + }; synchronized (sLock) { if (sPendingReplies.containsKey(id)) { // Already requested. Do not request the same provider again and insert the // reply to the queue instead. - sPendingReplies.get(id).add(reply); + if (reply != null) { + sPendingReplies.get(id).add(reply); + } return null; } - ArrayList<ReplyCallback<Typeface>> pendingReplies = new ArrayList<>(); - pendingReplies.add(reply); - sPendingReplies.put(id, pendingReplies); + if (reply != null) { + ArrayList<ReplyCallback<TypefaceResult>> pendingReplies = new ArrayList<>(); + pendingReplies.add(reply); + sPendingReplies.put(id, pendingReplies); + } } - sBackgroundThread.postAndReply(fetcher, new ReplyCallback<Typeface>() { + sBackgroundThread.postAndReply(fetcher, new ReplyCallback<TypefaceResult>() { @Override - public void onReply(final Typeface typeface) { - final ArrayList<ReplyCallback<Typeface>> replies; + public void onReply(final TypefaceResult typeface) { + final ArrayList<ReplyCallback<TypefaceResult>> replies; synchronized (sLock) { replies = sPendingReplies.get(id); sPendingReplies.remove(id); @@ -269,7 +308,7 @@ public class FontsContractCompat { for (int i = 0; i < replies.size(); ++i) { replies.get(i).onReply(typeface); } - }; + } }); return null; } @@ -292,8 +331,9 @@ public class FontsContractCompat { * @param weight An integer that indicates the font weight. * @param italic A boolean that indicates the font is italic style or not. * @param resultCode A boolean that indicates the font contents is ready. + * + * @hide */ - /** @hide */ @RestrictTo(LIBRARY_GROUP) public FontInfo(@NonNull Uri uri, @IntRange(from = 0) int ttcIndex, @IntRange(from = 1, to = 1000) int weight, @@ -396,6 +436,9 @@ public class FontsContractCompat { * Interface used to receive asynchronously fetched typefaces. */ public static class FontRequestCallback { + /** @hide */ + @RestrictTo(LIBRARY_GROUP) + public static final int RESULT_OK = Columns.RESULT_CODE_OK; /** * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given * provider was not found on the device. @@ -412,6 +455,11 @@ public class FontsContractCompat { */ public static final int FAIL_REASON_FONT_LOAD_ERROR = -3; /** + * Constant that signals that the font was not loaded due to security issues. This usually + * means the font was attempted to load on a restricted context. + */ + public static final int FAIL_REASON_SECURITY_VIOLATION = -4; + /** * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font * provider did not return any results for the given query. */ @@ -431,9 +479,10 @@ public class FontsContractCompat { @RestrictTo(LIBRARY_GROUP) @IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR, FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE, - FAIL_REASON_MALFORMED_QUERY, FAIL_REASON_WRONG_CERTIFICATES }) + FAIL_REASON_MALFORMED_QUERY, FAIL_REASON_WRONG_CERTIFICATES, + FAIL_REASON_SECURITY_VIOLATION, RESULT_OK }) @Retention(RetentionPolicy.SOURCE) - @interface FontRequestFailReason {} + public @interface FontRequestFailReason {} public FontRequestCallback() {} @@ -600,6 +649,7 @@ public class FontsContractCompat { * @param fonts An array of {@link FontInfo} to be used to create a Typeface. * @return A Typeface object. Returns null if typeface creation fails. */ + @Nullable public static Typeface buildTypeface(@NonNull Context context, @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts) { return TypefaceCompat.createFromFontInfo(context, cancellationSignal, fonts, diff --git a/android/support/v4/provider/SelfDestructiveThread.java b/android/support/v4/provider/SelfDestructiveThread.java index 885799be..7cfe1f8c 100644 --- a/android/support/v4/provider/SelfDestructiveThread.java +++ b/android/support/v4/provider/SelfDestructiveThread.java @@ -129,7 +129,7 @@ public class SelfDestructiveThread { /** * Execute the specific callable object on this thread and call the reply callback on the - * calling thread once it finishs. + * calling thread once it finishes. */ public <T> void postAndReply(final Callable<T> callable, final ReplyCallback<T> reply) { final Handler callingHandler = new Handler(); diff --git a/android/support/v4/util/AtomicFile.java b/android/support/v4/util/AtomicFile.java index 275f4e20..aefe7050 100644 --- a/android/support/v4/util/AtomicFile.java +++ b/android/support/v4/util/AtomicFile.java @@ -16,6 +16,8 @@ package android.support.v4.util; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import java.io.File; @@ -48,7 +50,7 @@ public class AtomicFile { * Create a new AtomicFile for a file located at the given File path. * The secondary backup file will be the same file path with ".bak" appended. */ - public AtomicFile(File baseName) { + public AtomicFile(@NonNull File baseName) { mBaseName = baseName; mBackupName = new File(baseName.getPath() + ".bak"); } @@ -57,6 +59,7 @@ public class AtomicFile { * Return the path to the base file. You should not generally use this, * as the data at that path may not be valid. */ + @NonNull public File getBaseFile() { return mBaseName; } @@ -83,6 +86,7 @@ public class AtomicFile { * safe (or will be lost). You must do your own threading protection for * access to AtomicFile. */ + @NonNull public FileOutputStream startWrite() throws IOException { // Rename the current file so it may be used as a backup during the next read if (mBaseName.exists()) { @@ -95,7 +99,7 @@ public class AtomicFile { mBaseName.delete(); } } - FileOutputStream str = null; + FileOutputStream str; try { str = new FileOutputStream(mBaseName); } catch (FileNotFoundException e) { @@ -118,7 +122,7 @@ public class AtomicFile { * commit the new data. The next attempt to read the atomic file * will return the new file stream. */ - public void finishWrite(FileOutputStream str) { + public void finishWrite(@Nullable FileOutputStream str) { if (str != null) { sync(str); try { @@ -135,7 +139,7 @@ public class AtomicFile { * returned by {@link #startWrite()}. This will close the current * write stream, and roll back to the previous state of the file. */ - public void failWrite(FileOutputStream str) { + public void failWrite(@Nullable FileOutputStream str) { if (str != null) { sync(str); try { @@ -160,6 +164,7 @@ public class AtomicFile { * be dropped. You must do your own threading protection for access to * AtomicFile. */ + @NonNull public FileInputStream openRead() throws FileNotFoundException { if (mBackupName.exists()) { mBaseName.delete(); @@ -172,6 +177,7 @@ public class AtomicFile { * A convenience for {@link #openRead()} that also reads all of the * file contents into a byte array which is returned. */ + @NonNull public byte[] readFully() throws IOException { FileInputStream stream = openRead(); try { @@ -200,11 +206,9 @@ public class AtomicFile { } } - static boolean sync(FileOutputStream stream) { + private static boolean sync(@NonNull FileOutputStream stream) { try { - if (stream != null) { - stream.getFD().sync(); - } + stream.getFD().sync(); return true; } catch (IOException e) { } diff --git a/android/support/v4/util/ObjectsCompat.java b/android/support/v4/util/ObjectsCompat.java index 46500605..b6c740e5 100644 --- a/android/support/v4/util/ObjectsCompat.java +++ b/android/support/v4/util/ObjectsCompat.java @@ -16,6 +16,7 @@ package android.support.v4.util; import android.os.Build; +import android.support.annotation.Nullable; import java.util.Objects; @@ -43,7 +44,7 @@ public class ObjectsCompat { * and {@code false} otherwise * @see Object#equals(Object) */ - public static boolean equals(Object a, Object b) { + public static boolean equals(@Nullable Object a, @Nullable Object b) { if (Build.VERSION.SDK_INT >= 19) { return Objects.equals(a, b); } else { diff --git a/android/support/v4/util/Pair.java b/android/support/v4/util/Pair.java index 46ea5cd6..9047aec1 100644 --- a/android/support/v4/util/Pair.java +++ b/android/support/v4/util/Pair.java @@ -16,14 +16,17 @@ package android.support.v4.util; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + /** * Container to ease passing around a tuple of two objects. This object provides a sensible * implementation of equals(), returning true if equals() is true on each of the contained * objects. */ public class Pair<F, S> { - public final F first; - public final S second; + public final @Nullable F first; + public final @Nullable S second; /** * Constructor for a Pair. @@ -31,7 +34,7 @@ public class Pair<F, S> { * @param first the first object in the Pair * @param second the second object in the pair */ - public Pair(F first, S second) { + public Pair(@Nullable F first, @Nullable S second) { this.first = first; this.second = second; } @@ -78,7 +81,8 @@ public class Pair<F, S> { * @param b the second object in the pair * @return a Pair that is templatized with the types of a and b */ - public static <A, B> Pair <A, B> create(A a, B b) { + @NonNull + public static <A, B> Pair <A, B> create(@Nullable A a, @Nullable B b) { return new Pair<A, B>(a, b); } } diff --git a/android/support/v4/util/Pools.java b/android/support/v4/util/Pools.java index 68826601..99fd888a 100644 --- a/android/support/v4/util/Pools.java +++ b/android/support/v4/util/Pools.java @@ -18,6 +18,9 @@ package android.support.v4.util; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + /** * Helper class for creating pools of objects. An example use looks like this: * <pre> @@ -53,6 +56,7 @@ public final class Pools { /** * @return An instance from the pool if such, null otherwise. */ + @Nullable public T acquire(); /** @@ -63,7 +67,7 @@ public final class Pools { * * @throws IllegalStateException If the instance is already in the pool. */ - public boolean release(T instance); + public boolean release(@NonNull T instance); } private Pools() { @@ -108,7 +112,7 @@ public final class Pools { } @Override - public boolean release(T instance) { + public boolean release(@NonNull T instance) { if (isInPool(instance)) { throw new IllegalStateException("Already in the pool!"); } @@ -120,7 +124,7 @@ public final class Pools { return false; } - private boolean isInPool(T instance) { + private boolean isInPool(@NonNull T instance) { for (int i = 0; i < mPoolSize; i++) { if (mPool[i] == instance) { return true; @@ -157,7 +161,7 @@ public final class Pools { } @Override - public boolean release(T element) { + public boolean release(@NonNull T element) { synchronized (mLock) { return super.release(element); } diff --git a/android/support/v4/view/AbsSavedState.java b/android/support/v4/view/AbsSavedState.java index 4cf38ac8..86f491f6 100644 --- a/android/support/v4/view/AbsSavedState.java +++ b/android/support/v4/view/AbsSavedState.java @@ -18,6 +18,8 @@ package android.support.v4.view; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; /** * A {@link Parcelable} implementation that should be used by inheritance @@ -40,7 +42,7 @@ public abstract class AbsSavedState implements Parcelable { * * @param superState The state of the superclass of this view */ - protected AbsSavedState(Parcelable superState) { + protected AbsSavedState(@NonNull Parcelable superState) { if (superState == null) { throw new IllegalArgumentException("superState must not be null"); } @@ -52,7 +54,7 @@ public abstract class AbsSavedState implements Parcelable { * * @param source parcel to read from */ - protected AbsSavedState(Parcel source) { + protected AbsSavedState(@NonNull Parcel source) { this(source, null); } @@ -62,11 +64,12 @@ public abstract class AbsSavedState implements Parcelable { * @param source parcel to read from * @param loader ClassLoader to use for reading */ - protected AbsSavedState(Parcel source, ClassLoader loader) { + protected AbsSavedState(@NonNull Parcel source, @Nullable ClassLoader loader) { Parcelable superState = source.readParcelable(loader); mSuperState = superState != null ? superState : EMPTY_STATE; } + @Nullable public final Parcelable getSuperState() { return mSuperState; } diff --git a/android/support/v4/view/AsyncLayoutInflater.java b/android/support/v4/view/AsyncLayoutInflater.java index e194a508..d26e5b82 100644 --- a/android/support/v4/view/AsyncLayoutInflater.java +++ b/android/support/v4/view/AsyncLayoutInflater.java @@ -107,7 +107,8 @@ public final class AsyncLayoutInflater { }; public interface OnInflateFinishedListener { - void onInflateFinished(View view, int resid, ViewGroup parent); + void onInflateFinished(@NonNull View view, @LayoutRes int resid, + @Nullable ViewGroup parent); } private static class InflateRequest { diff --git a/android/support/v4/view/PagerAdapter.java b/android/support/v4/view/PagerAdapter.java index 1b1dc3de..a8fb099c 100644 --- a/android/support/v4/view/PagerAdapter.java +++ b/android/support/v4/view/PagerAdapter.java @@ -19,6 +19,8 @@ package android.support.v4.view; import android.database.DataSetObservable; import android.database.DataSetObserver; import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.View; import android.view.ViewGroup; @@ -92,7 +94,7 @@ public abstract class PagerAdapter { * @param container The containing View which is displaying this adapter's * page views. */ - public void startUpdate(ViewGroup container) { + public void startUpdate(@NonNull ViewGroup container) { startUpdate((View) container); } @@ -107,7 +109,8 @@ public abstract class PagerAdapter { * @return Returns an Object representing the new page. This does not * need to be a View, but can be some other container of the page. */ - public Object instantiateItem(ViewGroup container, int position) { + @NonNull + public Object instantiateItem(@NonNull ViewGroup container, int position) { return instantiateItem((View) container, position); } @@ -121,7 +124,7 @@ public abstract class PagerAdapter { * @param object The same object that was returned by * {@link #instantiateItem(View, int)}. */ - public void destroyItem(ViewGroup container, int position, Object object) { + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { destroyItem((View) container, position, object); } @@ -134,7 +137,7 @@ public abstract class PagerAdapter { * @param object The same object that was returned by * {@link #instantiateItem(View, int)}. */ - public void setPrimaryItem(ViewGroup container, int position, Object object) { + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { setPrimaryItem((View) container, position, object); } @@ -145,7 +148,7 @@ public abstract class PagerAdapter { * @param container The containing View which is displaying this adapter's * page views. */ - public void finishUpdate(ViewGroup container) { + public void finishUpdate(@NonNull ViewGroup container) { finishUpdate((View) container); } @@ -157,7 +160,7 @@ public abstract class PagerAdapter { * @deprecated Use {@link #startUpdate(ViewGroup)} */ @Deprecated - public void startUpdate(View container) { + public void startUpdate(@NonNull View container) { } /** @@ -174,7 +177,8 @@ public abstract class PagerAdapter { * @deprecated Use {@link #instantiateItem(ViewGroup, int)} */ @Deprecated - public Object instantiateItem(View container, int position) { + @NonNull + public Object instantiateItem(@NonNull View container, int position) { throw new UnsupportedOperationException( "Required method instantiateItem was not overridden"); } @@ -192,7 +196,7 @@ public abstract class PagerAdapter { * @deprecated Use {@link #destroyItem(ViewGroup, int, Object)} */ @Deprecated - public void destroyItem(View container, int position, Object object) { + public void destroyItem(@NonNull View container, int position, @NonNull Object object) { throw new UnsupportedOperationException("Required method destroyItem was not overridden"); } @@ -208,7 +212,7 @@ public abstract class PagerAdapter { * @deprecated Use {@link #setPrimaryItem(ViewGroup, int, Object)} */ @Deprecated - public void setPrimaryItem(View container, int position, Object object) { + public void setPrimaryItem(@NonNull View container, int position, @NonNull Object object) { } /** @@ -221,7 +225,7 @@ public abstract class PagerAdapter { * @deprecated Use {@link #finishUpdate(ViewGroup)} */ @Deprecated - public void finishUpdate(View container) { + public void finishUpdate(@NonNull View container) { } /** @@ -233,7 +237,7 @@ public abstract class PagerAdapter { * @param object Object to check for association with <code>view</code> * @return true if <code>view</code> is associated with the key object <code>object</code> */ - public abstract boolean isViewFromObject(View view, Object object); + public abstract boolean isViewFromObject(@NonNull View view, @NonNull Object object); /** * Save any instance state associated with this adapter and its pages that should be @@ -241,6 +245,7 @@ public abstract class PagerAdapter { * * @return Saved state for this adapter */ + @Nullable public Parcelable saveState() { return null; } @@ -252,7 +257,7 @@ public abstract class PagerAdapter { * @param state State previously saved by a call to {@link #saveState()} * @param loader A ClassLoader that should be used to instantiate any restored objects */ - public void restoreState(Parcelable state, ClassLoader loader) { + public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) { } /** @@ -270,7 +275,7 @@ public abstract class PagerAdapter { * {@link #POSITION_UNCHANGED} if the object's position has not changed, * or {@link #POSITION_NONE} if the item is no longer present. */ - public int getItemPosition(Object object) { + public int getItemPosition(@NonNull Object object) { return POSITION_UNCHANGED; } @@ -292,7 +297,7 @@ public abstract class PagerAdapter { * * @param observer The {@link android.database.DataSetObserver} which will receive callbacks. */ - public void registerDataSetObserver(DataSetObserver observer) { + public void registerDataSetObserver(@NonNull DataSetObserver observer) { mObservable.registerObserver(observer); } @@ -301,7 +306,7 @@ public abstract class PagerAdapter { * * @param observer The {@link android.database.DataSetObserver} which will be unregistered. */ - public void unregisterDataSetObserver(DataSetObserver observer) { + public void unregisterDataSetObserver(@NonNull DataSetObserver observer) { mObservable.unregisterObserver(observer); } @@ -320,6 +325,7 @@ public abstract class PagerAdapter { * @param position The position of the title requested * @return A title for the requested page */ + @Nullable public CharSequence getPageTitle(int position) { return null; } diff --git a/android/support/v4/view/PagerTabStrip.java b/android/support/v4/view/PagerTabStrip.java index f4c0b212..6c885722 100644 --- a/android/support/v4/view/PagerTabStrip.java +++ b/android/support/v4/view/PagerTabStrip.java @@ -24,6 +24,8 @@ import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.view.MotionEvent; @@ -76,11 +78,11 @@ public class PagerTabStrip extends PagerTitleStrip { private float mInitialMotionY; private int mTouchSlop; - public PagerTabStrip(Context context) { + public PagerTabStrip(@NonNull Context context) { this(context, null); } - public PagerTabStrip(Context context, AttributeSet attrs) { + public PagerTabStrip(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); mIndicatorColor = mTextColor; diff --git a/android/support/v4/view/PagerTitleStrip.java b/android/support/v4/view/PagerTitleStrip.java index b63e4f42..79a6240c 100644 --- a/android/support/v4/view/PagerTitleStrip.java +++ b/android/support/v4/view/PagerTitleStrip.java @@ -22,6 +22,8 @@ import android.database.DataSetObserver; import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.FloatRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.widget.TextViewCompat; import android.text.TextUtils.TruncateAt; import android.text.method.SingleLineTransformationMethod; @@ -102,11 +104,11 @@ public class PagerTitleStrip extends ViewGroup { text.setTransformationMethod(new SingleLineAllCapsTransform(text.getContext())); } - public PagerTitleStrip(Context context) { + public PagerTitleStrip(@NonNull Context context) { this(context, null); } - public PagerTitleStrip(Context context, AttributeSet attrs) { + public PagerTitleStrip(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); addView(mPrevText = new TextView(context)); diff --git a/android/support/v4/view/ViewCompat.java b/android/support/v4/view/ViewCompat.java index e7443d7d..34a198a1 100644 --- a/android/support/v4/view/ViewCompat.java +++ b/android/support/v4/view/ViewCompat.java @@ -3029,7 +3029,10 @@ public class ViewCompat { * {@link ViewGroup#getChildDrawingOrder(int, int)}, false otherwise * * <p>Prior to API 7 this will have no effect.</p> + * + * @deprecated Use {@link ViewGroup#setChildrenDrawingOrderEnabled(boolean)} directly. */ + @Deprecated public static void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { IMPL.setChildrenDrawingOrderEnabled(viewGroup, enabled); } diff --git a/android/support/v4/view/ViewPager.java b/android/support/v4/view/ViewPager.java index 8cd973c8..36d8696c 100644 --- a/android/support/v4/view/ViewPager.java +++ b/android/support/v4/view/ViewPager.java @@ -347,7 +347,7 @@ public class ViewPager extends ViewGroup { * position of the pager. 0 is front and center. 1 is one full * page position to the right, and -1 is one page position to the left. */ - void transformPage(View page, float position); + void transformPage(@NonNull View page, float position); } /** @@ -381,12 +381,12 @@ public class ViewPager extends ViewGroup { public @interface DecorView { } - public ViewPager(Context context) { + public ViewPager(@NonNull Context context) { super(context); initViewPager(); } - public ViewPager(Context context, AttributeSet attrs) { + public ViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); initViewPager(); } @@ -496,7 +496,7 @@ public class ViewPager extends ViewGroup { * * @param adapter Adapter to use */ - public void setAdapter(PagerAdapter adapter) { + public void setAdapter(@Nullable PagerAdapter adapter) { if (mAdapter != null) { mAdapter.setViewPagerObserver(null); mAdapter.startUpdate(this); @@ -561,6 +561,7 @@ public class ViewPager extends ViewGroup { * * @return The currently registered PagerAdapter */ + @Nullable public PagerAdapter getAdapter() { return mAdapter; } @@ -712,7 +713,7 @@ public class ViewPager extends ViewGroup { * * @param listener listener to add */ - public void addOnPageChangeListener(OnPageChangeListener listener) { + public void addOnPageChangeListener(@NonNull OnPageChangeListener listener) { if (mOnPageChangeListeners == null) { mOnPageChangeListeners = new ArrayList<>(); } @@ -725,7 +726,7 @@ public class ViewPager extends ViewGroup { * * @param listener listener to remove */ - public void removeOnPageChangeListener(OnPageChangeListener listener) { + public void removeOnPageChangeListener(@NonNull OnPageChangeListener listener) { if (mOnPageChangeListeners != null) { mOnPageChangeListeners.remove(listener); } @@ -757,7 +758,8 @@ public class ViewPager extends ViewGroup { * to be drawn from last to first instead of first to last. * @param transformer PageTransformer that will modify each page's animation properties */ - public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { + public void setPageTransformer(boolean reverseDrawingOrder, + @Nullable PageTransformer transformer) { setPageTransformer(reverseDrawingOrder, transformer, View.LAYER_TYPE_HARDWARE); } @@ -774,8 +776,8 @@ public class ViewPager extends ViewGroup { * {@link View#LAYER_TYPE_SOFTWARE}, or * {@link View#LAYER_TYPE_NONE}. */ - public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer, - int pageLayerType) { + public void setPageTransformer(boolean reverseDrawingOrder, + @Nullable PageTransformer transformer, int pageLayerType) { final boolean hasTransformer = transformer != null; final boolean needsPopulate = hasTransformer != (mPageTransformer != null); mPageTransformer = transformer; @@ -881,7 +883,7 @@ public class ViewPager extends ViewGroup { * * @param d Drawable to display between pages */ - public void setPageMarginDrawable(Drawable d) { + public void setPageMarginDrawable(@Nullable Drawable d) { mMarginDrawable = d; if (d != null) refreshDrawableState(); setWillNotDraw(d == null); @@ -1383,7 +1385,7 @@ public class ViewPager extends ViewGroup { Parcelable adapterState; ClassLoader loader; - public SavedState(Parcelable superState) { + public SavedState(@NonNull Parcelable superState) { super(superState); } @@ -2744,7 +2746,7 @@ public class ViewPager extends ViewGroup { * @param event The key event to execute. * @return Return true if the event was handled, else false. */ - public boolean executeKeyEvent(KeyEvent event) { + public boolean executeKeyEvent(@NonNull KeyEvent event) { boolean handled = false; if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (event.getKeyCode()) { diff --git a/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java b/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java index f9329faa..327be017 100644 --- a/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java +++ b/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java @@ -1863,7 +1863,7 @@ public class AccessibilityNodeInfoCompat { } /** - * Sets whether this node is visible to the user. + * Gets whether this node is visible to the user. * * @return Whether the node is visible to the user. */ diff --git a/android/support/v4/widget/AutoScrollHelper.java b/android/support/v4/widget/AutoScrollHelper.java index d0407be1..60d208d8 100644 --- a/android/support/v4/widget/AutoScrollHelper.java +++ b/android/support/v4/widget/AutoScrollHelper.java @@ -18,6 +18,7 @@ package android.support.v4.widget; import android.content.res.Resources; import android.os.SystemClock; +import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.util.DisplayMetrics; import android.view.MotionEvent; @@ -205,7 +206,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * * @param target The view to automatically scroll. */ - public AutoScrollHelper(View target) { + public AutoScrollHelper(@NonNull View target) { mTarget = target; final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); @@ -289,6 +290,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * {@link #NO_MAX} to leave the relative value unconstrained. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) { mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f; mMaximumVelocity[VERTICAL] = verticalMax / 1000f; @@ -307,6 +309,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * {@link #NO_MIN} to leave the relative value unconstrained. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) { mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f; mMinimumVelocity[VERTICAL] = verticalMin / 1000f; @@ -328,6 +331,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * ignore. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) { mRelativeVelocity[HORIZONTAL] = horizontal / 1000f; mRelativeVelocity[VERTICAL] = vertical / 1000f; @@ -349,6 +353,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * @param type The type of edge to use. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setEdgeType(int type) { mEdgeType = type; return this; @@ -368,6 +373,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * maximum value. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) { mRelativeEdges[HORIZONTAL] = horizontal; mRelativeEdges[VERTICAL] = vertical; @@ -390,6 +396,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * value. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) { mMaximumEdges[HORIZONTAL] = horizontalMax; mMaximumEdges[VERTICAL] = verticalMax; @@ -407,6 +414,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * @param delayMillis The activation delay in milliseconds. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setActivationDelay(int delayMillis) { mActivationDelay = delayMillis; return this; @@ -422,6 +430,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * @param durationMillis The ramp-up duration in milliseconds. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setRampUpDuration(int durationMillis) { mScroller.setRampUpDuration(durationMillis); return this; @@ -437,6 +446,7 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { * @param durationMillis The ramp-down duration in milliseconds. * @return The scroll helper, which may used to chain setter calls. */ + @NonNull public AutoScrollHelper setRampDownDuration(int durationMillis) { mScroller.setRampDownDuration(durationMillis); return this; diff --git a/android/support/v4/widget/CircularProgressDrawable.java b/android/support/v4/widget/CircularProgressDrawable.java index ac295414..20556690 100644 --- a/android/support/v4/widget/CircularProgressDrawable.java +++ b/android/support/v4/widget/CircularProgressDrawable.java @@ -132,7 +132,7 @@ public class CircularProgressDrawable extends Drawable implements Animatable { /** * @param context application context */ - public CircularProgressDrawable(Context context) { + public CircularProgressDrawable(@NonNull Context context) { mResources = Preconditions.checkNotNull(context).getResources(); mRing = new Ring(); @@ -215,7 +215,7 @@ public class CircularProgressDrawable extends Drawable implements Animatable { * * @param strokeCap stroke cap */ - public void setStrokeCap(Paint.Cap strokeCap) { + public void setStrokeCap(@NonNull Paint.Cap strokeCap) { mRing.setStrokeCap(strokeCap); invalidateSelf(); } @@ -225,6 +225,7 @@ public class CircularProgressDrawable extends Drawable implements Animatable { * * @return stroke cap */ + @NonNull public Paint.Cap getStrokeCap() { return mRing.getStrokeCap(); } @@ -373,6 +374,7 @@ public class CircularProgressDrawable extends Drawable implements Animatable { * * @return list of ARGB colors */ + @NonNull public int[] getColorSchemeColors() { return mRing.getColors(); } @@ -383,7 +385,7 @@ public class CircularProgressDrawable extends Drawable implements Animatable { * * @param colors list of ARGB colors to be used in the spinner */ - public void setColorSchemeColors(int... colors) { + public void setColorSchemeColors(@NonNull int... colors) { mRing.setColors(colors); mRing.setColorIndex(0); invalidateSelf(); diff --git a/android/support/v4/widget/ContentLoadingProgressBar.java b/android/support/v4/widget/ContentLoadingProgressBar.java index 98b63a47..356c7b9e 100644 --- a/android/support/v4/widget/ContentLoadingProgressBar.java +++ b/android/support/v4/widget/ContentLoadingProgressBar.java @@ -17,6 +17,8 @@ package android.support.v4.widget; import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import android.widget.ProgressBar; @@ -61,11 +63,11 @@ public class ContentLoadingProgressBar extends ProgressBar { } }; - public ContentLoadingProgressBar(Context context) { + public ContentLoadingProgressBar(@NonNull Context context) { this(context, null); } - public ContentLoadingProgressBar(Context context, AttributeSet attrs) { + public ContentLoadingProgressBar(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs, 0); } diff --git a/android/support/v4/widget/DrawerLayout.java b/android/support/v4/widget/DrawerLayout.java index c7b40e96..a73e1f10 100644 --- a/android/support/v4/widget/DrawerLayout.java +++ b/android/support/v4/widget/DrawerLayout.java @@ -248,7 +248,7 @@ public class DrawerLayout extends ViewGroup { * @param drawerView The child view that was moved * @param slideOffset The new offset of this drawer within its range, from 0-1 */ - void onDrawerSlide(View drawerView, float slideOffset); + void onDrawerSlide(@NonNull View drawerView, float slideOffset); /** * Called when a drawer has settled in a completely open state. @@ -256,14 +256,14 @@ public class DrawerLayout extends ViewGroup { * * @param drawerView Drawer view that is now open */ - void onDrawerOpened(View drawerView); + void onDrawerOpened(@NonNull View drawerView); /** * Called when a drawer has settled in a completely closed state. * * @param drawerView Drawer view that is now closed */ - void onDrawerClosed(View drawerView); + void onDrawerClosed(@NonNull View drawerView); /** * Called when the drawer motion state changes. The new state will @@ -296,15 +296,15 @@ public class DrawerLayout extends ViewGroup { } } - public DrawerLayout(Context context) { + public DrawerLayout(@NonNull Context context) { this(context, null); } - public DrawerLayout(Context context, AttributeSet attrs) { + public DrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { + public DrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); final float density = getResources().getDisplayMetrics().density; @@ -626,7 +626,7 @@ public class DrawerLayout extends ViewGroup { * @see #LOCK_MODE_LOCKED_CLOSED * @see #LOCK_MODE_LOCKED_OPEN */ - public void setDrawerLockMode(@LockMode int lockMode, View drawerView) { + public void setDrawerLockMode(@LockMode int lockMode, @NonNull View drawerView) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a " + "drawer with appropriate layout_gravity"); @@ -700,7 +700,7 @@ public class DrawerLayout extends ViewGroup { * {@link #LOCK_MODE_LOCKED_OPEN}. */ @LockMode - public int getDrawerLockMode(View drawerView) { + public int getDrawerLockMode(@NonNull View drawerView) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a drawer"); } @@ -718,7 +718,7 @@ public class DrawerLayout extends ViewGroup { * drawer to set the title for. * @param title The title for the drawer. */ - public void setDrawerTitle(@EdgeGravity int edgeGravity, CharSequence title) { + public void setDrawerTitle(@EdgeGravity int edgeGravity, @Nullable CharSequence title) { final int absGravity = GravityCompat.getAbsoluteGravity( edgeGravity, ViewCompat.getLayoutDirection(this)); if (absGravity == Gravity.LEFT) { @@ -1276,7 +1276,7 @@ public class DrawerLayout extends ViewGroup { * * @param bg Background drawable to draw behind the status bar */ - public void setStatusBarBackground(Drawable bg) { + public void setStatusBarBackground(@Nullable Drawable bg) { mStatusBarBackground = bg; invalidate(); } @@ -1286,6 +1286,7 @@ public class DrawerLayout extends ViewGroup { * * @return The status bar background drawable, or null if none set */ + @Nullable public Drawable getStatusBarBackgroundDrawable() { return mStatusBarBackground; } @@ -1577,7 +1578,7 @@ public class DrawerLayout extends ViewGroup { * * @param drawerView Drawer view to open */ - public void openDrawer(View drawerView) { + public void openDrawer(@NonNull View drawerView) { openDrawer(drawerView, true); } @@ -1587,7 +1588,7 @@ public class DrawerLayout extends ViewGroup { * @param drawerView Drawer view to open * @param animate Whether opening of the drawer should be animated. */ - public void openDrawer(View drawerView, boolean animate) { + public void openDrawer(@NonNull View drawerView, boolean animate) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); } @@ -1646,7 +1647,7 @@ public class DrawerLayout extends ViewGroup { * * @param drawerView Drawer view to close */ - public void closeDrawer(View drawerView) { + public void closeDrawer(@NonNull View drawerView) { closeDrawer(drawerView, true); } @@ -1656,7 +1657,7 @@ public class DrawerLayout extends ViewGroup { * @param drawerView Drawer view to close * @param animate Whether closing of the drawer should be animated. */ - public void closeDrawer(View drawerView, boolean animate) { + public void closeDrawer(@NonNull View drawerView, boolean animate) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); } @@ -1718,7 +1719,7 @@ public class DrawerLayout extends ViewGroup { * @return true if the given drawer view is in an open state * @see #isDrawerVisible(android.view.View) */ - public boolean isDrawerOpen(View drawer) { + public boolean isDrawerOpen(@NonNull View drawer) { if (!isDrawerView(drawer)) { throw new IllegalArgumentException("View " + drawer + " is not a drawer"); } @@ -1751,7 +1752,7 @@ public class DrawerLayout extends ViewGroup { * @return true if the given drawer is visible on-screen * @see #isDrawerOpen(android.view.View) */ - public boolean isDrawerVisible(View drawer) { + public boolean isDrawerVisible(@NonNull View drawer) { if (!isDrawerView(drawer)) { throw new IllegalArgumentException("View " + drawer + " is not a drawer"); } @@ -2001,7 +2002,7 @@ public class DrawerLayout extends ViewGroup { @LockMode int lockModeStart; @LockMode int lockModeEnd; - public SavedState(Parcel in, ClassLoader loader) { + public SavedState(@NonNull Parcel in, @Nullable ClassLoader loader) { super(in, loader); openDrawerGravity = in.readInt(); lockModeLeft = in.readInt(); @@ -2010,7 +2011,7 @@ public class DrawerLayout extends ViewGroup { lockModeEnd = in.readInt(); } - public SavedState(Parcelable superState) { + public SavedState(@NonNull Parcelable superState) { super(superState); } @@ -2218,7 +2219,7 @@ public class DrawerLayout extends ViewGroup { boolean isPeeking; int openState; - public LayoutParams(Context c, AttributeSet attrs) { + public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) { super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); @@ -2235,16 +2236,16 @@ public class DrawerLayout extends ViewGroup { this.gravity = gravity; } - public LayoutParams(LayoutParams source) { + public LayoutParams(@NonNull LayoutParams source) { super(source); this.gravity = source.gravity; } - public LayoutParams(ViewGroup.LayoutParams source) { + public LayoutParams(@NonNull ViewGroup.LayoutParams source) { super(source); } - public LayoutParams(ViewGroup.MarginLayoutParams source) { + public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) { super(source); } } diff --git a/android/support/v4/widget/EdgeEffectCompat.java b/android/support/v4/widget/EdgeEffectCompat.java index 9293e60f..0d370a80 100644 --- a/android/support/v4/widget/EdgeEffectCompat.java +++ b/android/support/v4/widget/EdgeEffectCompat.java @@ -18,6 +18,7 @@ package android.support.v4.widget; import android.content.Context; import android.graphics.Canvas; import android.os.Build; +import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.widget.EdgeEffect; @@ -170,7 +171,8 @@ public final class EdgeEffectCompat { * * @see {@link EdgeEffect#onPull(float, float)} */ - public static void onPull(EdgeEffect edgeEffect, float deltaDistance, float displacement) { + public static void onPull(@NonNull EdgeEffect edgeEffect, float deltaDistance, + float displacement) { IMPL.onPull(edgeEffect, deltaDistance, displacement); } diff --git a/android/support/v4/widget/ExploreByTouchHelper.java b/android/support/v4/widget/ExploreByTouchHelper.java index 8a29eff6..2b5ed0a1 100644 --- a/android/support/v4/widget/ExploreByTouchHelper.java +++ b/android/support/v4/widget/ExploreByTouchHelper.java @@ -129,7 +129,7 @@ public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { * * @param host view whose virtual view hierarchy is exposed by this helper */ - public ExploreByTouchHelper(View host) { + public ExploreByTouchHelper(@NonNull View host) { if (host == null) { throw new IllegalArgumentException("View may not be null"); } @@ -1107,7 +1107,8 @@ public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { * populate the event * @param event The event to populate */ - protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + protected void onPopulateEventForVirtualView(int virtualViewId, + @NonNull AccessibilityEvent event) { // Default implementation is no-op. } @@ -1119,7 +1120,7 @@ public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { * * @param event the event to populate with information about the host view */ - protected void onPopulateEventForHost(AccessibilityEvent event) { + protected void onPopulateEventForHost(@NonNull AccessibilityEvent event) { // Default implementation is no-op. } @@ -1187,7 +1188,7 @@ public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { * @param node The node to populate */ protected abstract void onPopulateNodeForVirtualView( - int virtualViewId, AccessibilityNodeInfoCompat node); + int virtualViewId, @NonNull AccessibilityNodeInfoCompat node); /** * Populates an {@link AccessibilityNodeInfoCompat} with information @@ -1197,7 +1198,7 @@ public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { * * @param node the node to populate with information about the host view */ - protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) { + protected void onPopulateNodeForHost(@NonNull AccessibilityNodeInfoCompat node) { // Default implementation is no-op. } @@ -1225,7 +1226,7 @@ public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { * @return true if the action was performed */ protected abstract boolean onPerformActionForVirtualView( - int virtualViewId, int action, Bundle arguments); + int virtualViewId, int action, @Nullable Bundle arguments); /** * Exposes a virtual view hierarchy to the accessibility framework. diff --git a/android/support/v4/widget/ImageViewCompat.java b/android/support/v4/widget/ImageViewCompat.java index acaaf636..b517de5f 100644 --- a/android/support/v4/widget/ImageViewCompat.java +++ b/android/support/v4/widget/ImageViewCompat.java @@ -20,6 +20,8 @@ import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.widget.ImageView; @@ -130,21 +132,24 @@ public class ImageViewCompat { /** * Return the tint applied to the image drawable, if specified. */ - public static ColorStateList getImageTintList(ImageView view) { + @Nullable + public static ColorStateList getImageTintList(@NonNull ImageView view) { return IMPL.getImageTintList(view); } /** * Applies a tint to the image drawable. */ - public static void setImageTintList(ImageView view, ColorStateList tintList) { + public static void setImageTintList(@NonNull ImageView view, + @Nullable ColorStateList tintList) { IMPL.setImageTintList(view, tintList); } /** * Return the blending mode used to apply the tint to the image drawable, if specified. */ - public static PorterDuff.Mode getImageTintMode(ImageView view) { + @Nullable + public static PorterDuff.Mode getImageTintMode(@NonNull ImageView view) { return IMPL.getImageTintMode(view); } @@ -153,7 +158,7 @@ public class ImageViewCompat { * {@link #setImageTintList(android.widget.ImageView, android.content.res.ColorStateList)} * to the image drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. */ - public static void setImageTintMode(ImageView view, PorterDuff.Mode mode) { + public static void setImageTintMode(@NonNull ImageView view, @Nullable PorterDuff.Mode mode) { IMPL.setImageTintMode(view, mode); } diff --git a/android/support/v4/widget/ListPopupWindowCompat.java b/android/support/v4/widget/ListPopupWindowCompat.java index ab86e58b..45327331 100644 --- a/android/support/v4/widget/ListPopupWindowCompat.java +++ b/android/support/v4/widget/ListPopupWindowCompat.java @@ -17,6 +17,8 @@ package android.support.v4.widget; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.View; import android.view.View.OnTouchListener; import android.widget.ListPopupWindow; @@ -88,8 +90,9 @@ public final class ListPopupWindowCompat { * @return a touch listener that controls drag-to-open behavior, or {@code null} on * unsupported APIs */ + @Nullable public static OnTouchListener createDragToOpenListener( - ListPopupWindow listPopupWindow, View src) { + @NonNull ListPopupWindow listPopupWindow, @NonNull View src) { if (Build.VERSION.SDK_INT >= 19) { return listPopupWindow.createDragToOpenListener(src); } else { diff --git a/android/support/v4/widget/ListViewAutoScrollHelper.java b/android/support/v4/widget/ListViewAutoScrollHelper.java index 73d18cec..c373f27d 100644 --- a/android/support/v4/widget/ListViewAutoScrollHelper.java +++ b/android/support/v4/widget/ListViewAutoScrollHelper.java @@ -16,6 +16,7 @@ package android.support.v4.widget; +import android.support.annotation.NonNull; import android.view.View; import android.widget.ListView; @@ -26,7 +27,7 @@ import android.widget.ListView; public class ListViewAutoScrollHelper extends AutoScrollHelper { private final ListView mTarget; - public ListViewAutoScrollHelper(ListView target) { + public ListViewAutoScrollHelper(@NonNull ListView target) { super(target); mTarget = target; diff --git a/android/support/v4/widget/NestedScrollView.java b/android/support/v4/widget/NestedScrollView.java index 517686f3..73ff0848 100644 --- a/android/support/v4/widget/NestedScrollView.java +++ b/android/support/v4/widget/NestedScrollView.java @@ -26,6 +26,8 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.InputDeviceCompat; @@ -181,15 +183,16 @@ public class NestedScrollView extends FrameLayout implements NestedScrollingPare private OnScrollChangeListener mOnScrollChangeListener; - public NestedScrollView(Context context) { + public NestedScrollView(@NonNull Context context) { this(context, null); } - public NestedScrollView(Context context, AttributeSet attrs) { + public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { super(context, attrs, defStyleAttr); initScrollView(); @@ -441,7 +444,7 @@ public class NestedScrollView extends FrameLayout implements NestedScrollingPare * @see android.view.View#getScrollX() * @see android.view.View#getScrollY() */ - public void setOnScrollChangeListener(OnScrollChangeListener l) { + public void setOnScrollChangeListener(@Nullable OnScrollChangeListener l) { mOnScrollChangeListener = l; } @@ -552,7 +555,7 @@ public class NestedScrollView extends FrameLayout implements NestedScrollingPare * @param event The key event to execute. * @return Return true if the event was handled, else false. */ - public boolean executeKeyEvent(KeyEvent event) { + public boolean executeKeyEvent(@NonNull KeyEvent event) { mTempRect.setEmpty(); if (!canScroll()) { diff --git a/android/support/v4/widget/PopupMenuCompat.java b/android/support/v4/widget/PopupMenuCompat.java index 639a84ba..10c5ff3c 100644 --- a/android/support/v4/widget/PopupMenuCompat.java +++ b/android/support/v4/widget/PopupMenuCompat.java @@ -17,6 +17,8 @@ package android.support.v4.widget; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.View.OnTouchListener; import android.widget.PopupMenu; @@ -47,7 +49,8 @@ public final class PopupMenuCompat { * @return a touch listener that controls drag-to-open behavior, or {@code null} on * unsupported APIs */ - public static OnTouchListener getDragToOpenListener(Object popupMenu) { + @Nullable + public static OnTouchListener getDragToOpenListener(@NonNull Object popupMenu) { if (Build.VERSION.SDK_INT >= 19) { return ((PopupMenu) popupMenu).getDragToOpenListener(); } else { diff --git a/android/support/v4/widget/PopupWindowCompat.java b/android/support/v4/widget/PopupWindowCompat.java index d846b409..d9de3db6 100644 --- a/android/support/v4/widget/PopupWindowCompat.java +++ b/android/support/v4/widget/PopupWindowCompat.java @@ -17,6 +17,7 @@ package android.support.v4.widget; import android.os.Build; +import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewCompat; @@ -213,8 +214,8 @@ public final class PopupWindowCompat { * @param yoff A vertical offset from the anchor in pixels * @param gravity Alignment of the popup relative to the anchor */ - public static void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, - int gravity) { + public static void showAsDropDown(@NonNull PopupWindow popup, @NonNull View anchor, + int xoff, int yoff, int gravity) { IMPL.showAsDropDown(popup, anchor, xoff, yoff, gravity); } @@ -224,7 +225,7 @@ public final class PopupWindowCompat { * * @param overlapAnchor Whether the popup should overlap its anchor. */ - public static void setOverlapAnchor(PopupWindow popupWindow, boolean overlapAnchor) { + public static void setOverlapAnchor(@NonNull PopupWindow popupWindow, boolean overlapAnchor) { IMPL.setOverlapAnchor(popupWindow, overlapAnchor); } @@ -234,7 +235,7 @@ public final class PopupWindowCompat { * * @return Whether the popup should overlap its anchor. */ - public static boolean getOverlapAnchor(PopupWindow popupWindow) { + public static boolean getOverlapAnchor(@NonNull PopupWindow popupWindow) { return IMPL.getOverlapAnchor(popupWindow); } @@ -247,7 +248,7 @@ public final class PopupWindowCompat { * * @see android.view.WindowManager.LayoutParams#type */ - public static void setWindowLayoutType(PopupWindow popupWindow, int layoutType) { + public static void setWindowLayoutType(@NonNull PopupWindow popupWindow, int layoutType) { IMPL.setWindowLayoutType(popupWindow, layoutType); } @@ -256,7 +257,7 @@ public final class PopupWindowCompat { * * @see #setWindowLayoutType(PopupWindow popupWindow, int) */ - public static int getWindowLayoutType(PopupWindow popupWindow) { + public static int getWindowLayoutType(@NonNull PopupWindow popupWindow) { return IMPL.getWindowLayoutType(popupWindow); } } diff --git a/android/support/v4/widget/SlidingPaneLayout.java b/android/support/v4/widget/SlidingPaneLayout.java index 602df705..5676ccf8 100644 --- a/android/support/v4/widget/SlidingPaneLayout.java +++ b/android/support/v4/widget/SlidingPaneLayout.java @@ -30,6 +30,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v4.content.ContextCompat; import android.support.v4.view.AbsSavedState; @@ -213,20 +215,20 @@ public class SlidingPaneLayout extends ViewGroup { * @param panel The child view that was moved * @param slideOffset The new offset of this sliding pane within its range, from 0-1 */ - void onPanelSlide(View panel, float slideOffset); + void onPanelSlide(@NonNull View panel, float slideOffset); /** * Called when a sliding pane becomes slid completely open. The pane may or may not * be interactive at this point depending on how much of the pane is visible. * @param panel The child view that was slid to an open position, revealing other panes */ - void onPanelOpened(View panel); + void onPanelOpened(@NonNull View panel); /** * Called when a sliding pane becomes slid completely closed. The pane is now guaranteed * to be interactive. It may now obscure other views in the layout. * @param panel The child view that was slid to a closed position */ - void onPanelClosed(View panel); + void onPanelClosed(@NonNull View panel); } /** @@ -245,15 +247,15 @@ public class SlidingPaneLayout extends ViewGroup { } } - public SlidingPaneLayout(Context context) { + public SlidingPaneLayout(@NonNull Context context) { this(context, null); } - public SlidingPaneLayout(Context context, AttributeSet attrs) { + public SlidingPaneLayout(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public SlidingPaneLayout(Context context, AttributeSet attrs, int defStyle) { + public SlidingPaneLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final float density = context.getResources().getDisplayMetrics().density; @@ -324,7 +326,7 @@ public class SlidingPaneLayout extends ViewGroup { return mCoveredFadeColor; } - public void setPanelSlideListener(PanelSlideListener listener) { + public void setPanelSlideListener(@Nullable PanelSlideListener listener) { mPanelSlideListener = listener; } @@ -1081,7 +1083,7 @@ public class SlidingPaneLayout extends ViewGroup { * * @param d drawable to use as a shadow */ - public void setShadowDrawableLeft(Drawable d) { + public void setShadowDrawableLeft(@Nullable Drawable d) { mShadowDrawableLeft = d; } @@ -1091,7 +1093,7 @@ public class SlidingPaneLayout extends ViewGroup { * * @param d drawable to use as a shadow */ - public void setShadowDrawableRight(Drawable d) { + public void setShadowDrawableRight(@Nullable Drawable d) { mShadowDrawableRight = d; } @@ -1410,20 +1412,20 @@ public class SlidingPaneLayout extends ViewGroup { super(width, height); } - public LayoutParams(android.view.ViewGroup.LayoutParams source) { + public LayoutParams(@NonNull android.view.ViewGroup.LayoutParams source) { super(source); } - public LayoutParams(MarginLayoutParams source) { + public LayoutParams(@NonNull MarginLayoutParams source) { super(source); } - public LayoutParams(LayoutParams source) { + public LayoutParams(@NonNull LayoutParams source) { super(source); this.weight = source.weight; } - public LayoutParams(Context c, AttributeSet attrs) { + public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) { super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); diff --git a/android/support/v4/widget/Space.java b/android/support/v4/widget/Space.java index 77a2d2ee..7d37a72b 100644 --- a/android/support/v4/widget/Space.java +++ b/android/support/v4/widget/Space.java @@ -19,6 +19,8 @@ package android.support.v4.widget; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.View; @@ -28,18 +30,18 @@ import android.view.View; */ public class Space extends View { - public Space(Context context, AttributeSet attrs, int defStyle) { + public Space(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); if (getVisibility() == VISIBLE) { setVisibility(INVISIBLE); } } - public Space(Context context, AttributeSet attrs) { + public Space(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public Space(Context context) { + public Space(@NonNull Context context) { this(context, null); } diff --git a/android/support/v4/widget/SwipeRefreshLayout.java b/android/support/v4/widget/SwipeRefreshLayout.java index d36ae225..ca04e46a 100644 --- a/android/support/v4/widget/SwipeRefreshLayout.java +++ b/android/support/v4/widget/SwipeRefreshLayout.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; @@ -316,7 +317,7 @@ public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingPare * * @param context */ - public SwipeRefreshLayout(Context context) { + public SwipeRefreshLayout(@NonNull Context context) { this(context, null); } @@ -326,7 +327,7 @@ public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingPare * @param context * @param attrs */ - public SwipeRefreshLayout(Context context, AttributeSet attrs) { + public SwipeRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); @@ -341,7 +342,7 @@ public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingPare mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); createProgressView(); - ViewCompat.setChildrenDrawingOrderEnabled(this, true); + setChildrenDrawingOrderEnabled(true); // the absolute offset has to take into account that the circle starts at an offset mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density); mTotalDragDistance = mSpinnerOffsetEnd; @@ -387,7 +388,7 @@ public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingPare * Set the listener to be notified when a refresh is triggered via the swipe * gesture. */ - public void setOnRefreshListener(OnRefreshListener listener) { + public void setOnRefreshListener(@Nullable OnRefreshListener listener) { mListener = listener; } @@ -1189,6 +1190,6 @@ public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingPare * * @return Whether it is possible for the child view of parent layout to scroll up. */ - boolean canChildScrollUp(SwipeRefreshLayout parent, @Nullable View child); + boolean canChildScrollUp(@NonNull SwipeRefreshLayout parent, @Nullable View child); } } diff --git a/android/support/v4/widget/TextViewCompat.java b/android/support/v4/widget/TextViewCompat.java index 8d9e4ab0..dc87a38b 100644 --- a/android/support/v4/widget/TextViewCompat.java +++ b/android/support/v4/widget/TextViewCompat.java @@ -479,6 +479,7 @@ public final class TextViewCompat { /** * Returns drawables for the start, top, end, and bottom borders from the given text view. */ + @NonNull public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) { return IMPL.getCompoundDrawablesRelative(textView); } @@ -493,7 +494,8 @@ public final class TextViewCompat { * * @attr name android:autoSizeTextType */ - public static void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) { + public static void setAutoSizeTextTypeWithDefaults(@NonNull TextView textView, + int autoSizeTextType) { IMPL.setAutoSizeTextTypeWithDefaults(textView, autoSizeTextType); } @@ -519,7 +521,7 @@ public final class TextViewCompat { * @attr name android:autoSizeStepGranularity */ public static void setAutoSizeTextTypeUniformWithConfiguration( - TextView textView, + @NonNull TextView textView, int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, @@ -542,7 +544,7 @@ public final class TextViewCompat { * @attr name android:autoSizeTextType * @attr name android:autoSizePresetSizes */ - public static void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView, + public static void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull TextView textView, @NonNull int[] presetSizes, int unit) throws IllegalArgumentException { IMPL.setAutoSizeTextTypeUniformWithPresetSizes(textView, presetSizes, unit); } @@ -556,7 +558,7 @@ public final class TextViewCompat { * * @attr name android:autoSizeTextType */ - public static int getAutoSizeTextType(TextView textView) { + public static int getAutoSizeTextType(@NonNull TextView textView) { return IMPL.getAutoSizeTextType(textView); } @@ -565,7 +567,7 @@ public final class TextViewCompat { * * @attr name android:autoSizeStepGranularity */ - public static int getAutoSizeStepGranularity(TextView textView) { + public static int getAutoSizeStepGranularity(@NonNull TextView textView) { return IMPL.getAutoSizeStepGranularity(textView); } @@ -575,7 +577,7 @@ public final class TextViewCompat { * * @attr name android:autoSizeMinTextSize */ - public static int getAutoSizeMinTextSize(TextView textView) { + public static int getAutoSizeMinTextSize(@NonNull TextView textView) { return IMPL.getAutoSizeMinTextSize(textView); } @@ -585,7 +587,7 @@ public final class TextViewCompat { * * @attr name android:autoSizeMaxTextSize */ - public static int getAutoSizeMaxTextSize(TextView textView) { + public static int getAutoSizeMaxTextSize(@NonNull TextView textView) { return IMPL.getAutoSizeMaxTextSize(textView); } @@ -594,7 +596,8 @@ public final class TextViewCompat { * * @attr name android:autoSizePresetSizes */ - public static int[] getAutoSizeTextAvailableSizes(TextView textView) { + @NonNull + public static int[] getAutoSizeTextAvailableSizes(@NonNull TextView textView) { return IMPL.getAutoSizeTextAvailableSizes(textView); } } diff --git a/android/support/v4/widget/ViewDragHelper.java b/android/support/v4/widget/ViewDragHelper.java index c222c175..09c6f663 100644 --- a/android/support/v4/widget/ViewDragHelper.java +++ b/android/support/v4/widget/ViewDragHelper.java @@ -18,6 +18,8 @@ package android.support.v4.widget; import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.view.ViewCompat; import android.util.Log; import android.view.MotionEvent; @@ -167,7 +169,9 @@ public class ViewDragHelper { * @param dx Change in X position from the last call * @param dy Change in Y position from the last call */ - public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {} + public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, + int dy) { + } /** * Called when a child view is captured for dragging or settling. The ID of the pointer @@ -178,7 +182,7 @@ public class ViewDragHelper { * @param capturedChild Child view that was captured * @param activePointerId Pointer id tracking the child capture */ - public void onViewCaptured(View capturedChild, int activePointerId) {} + public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {} /** * Called when the child view is no longer being actively dragged. @@ -198,7 +202,7 @@ public class ViewDragHelper { * @param xvel X velocity of the pointer as it left the screen in pixels per second. * @param yvel Y velocity of the pointer as it left the screen in pixels per second. */ - public void onViewReleased(View releasedChild, float xvel, float yvel) {} + public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {} /** * Called when one of the subscribed edges in the parent view has been touched @@ -256,7 +260,7 @@ public class ViewDragHelper { * @param child Child view to check * @return range of horizontal motion in pixels */ - public int getViewHorizontalDragRange(View child) { + public int getViewHorizontalDragRange(@NonNull View child) { return 0; } @@ -267,7 +271,7 @@ public class ViewDragHelper { * @param child Child view to check * @return range of vertical motion in pixels */ - public int getViewVerticalDragRange(View child) { + public int getViewVerticalDragRange(@NonNull View child) { return 0; } @@ -287,7 +291,7 @@ public class ViewDragHelper { * @param pointerId ID of the pointer attempting the capture * @return true if capture should be allowed, false otherwise */ - public abstract boolean tryCaptureView(View child, int pointerId); + public abstract boolean tryCaptureView(@NonNull View child, int pointerId); /** * Restrict the motion of the dragged child view along the horizontal axis. @@ -300,7 +304,7 @@ public class ViewDragHelper { * @param dx Proposed change in position for left * @return The new clamped position for left */ - public int clampViewPositionHorizontal(View child, int left, int dx) { + public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) { return 0; } @@ -315,7 +319,7 @@ public class ViewDragHelper { * @param dy Proposed change in position for top * @return The new clamped position for top */ - public int clampViewPositionVertical(View child, int top, int dy) { + public int clampViewPositionVertical(@NonNull View child, int top, int dy) { return 0; } } @@ -345,7 +349,7 @@ public class ViewDragHelper { * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ - public static ViewDragHelper create(ViewGroup forParent, Callback cb) { + public static ViewDragHelper create(@NonNull ViewGroup forParent, @NonNull Callback cb) { return new ViewDragHelper(forParent.getContext(), forParent, cb); } @@ -358,7 +362,8 @@ public class ViewDragHelper { * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ - public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { + public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity, + @NonNull Callback cb) { final ViewDragHelper helper = create(forParent, cb); helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); return helper; @@ -372,7 +377,8 @@ public class ViewDragHelper { * @param context Context to initialize config-dependent params from * @param forParent Parent view to monitor */ - private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { + private ViewDragHelper(@NonNull Context context, @NonNull ViewGroup forParent, + @NonNull Callback cb) { if (forParent == null) { throw new IllegalArgumentException("Parent view may not be null"); } @@ -458,7 +464,7 @@ public class ViewDragHelper { * @param childView Child view to capture * @param activePointerId ID of the pointer that is dragging the captured child view */ - public void captureChildView(View childView, int activePointerId) { + public void captureChildView(@NonNull View childView, int activePointerId) { if (childView.getParent() != mParentView) { throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); @@ -473,6 +479,7 @@ public class ViewDragHelper { /** * @return The currently captured view, or null if no view has been captured. */ + @Nullable public View getCapturedView() { return mCapturedView; } @@ -537,7 +544,7 @@ public class ViewDragHelper { * @param finalTop Final top position of child * @return true if animation should continue through {@link #continueSettling(boolean)} calls */ - public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { + public boolean smoothSlideViewTo(@NonNull View child, int finalLeft, int finalTop) { mCapturedView = child; mActivePointerId = INVALID_POINTER; @@ -918,7 +925,7 @@ public class ViewDragHelper { * @param y Y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */ - protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { + protected boolean canScroll(@NonNull View v, boolean checkV, int dx, int dy, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); @@ -948,7 +955,7 @@ public class ViewDragHelper { * @param ev MotionEvent provided to onInterceptTouchEvent * @return true if the parent view should return true from onInterceptTouchEvent */ - public boolean shouldInterceptTouchEvent(MotionEvent ev) { + public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) { final int action = ev.getActionMasked(); final int actionIndex = ev.getActionIndex(); @@ -1082,7 +1089,7 @@ public class ViewDragHelper { * * @param ev The touch event received by the parent view */ - public void processTouchEvent(MotionEvent ev) { + public void processTouchEvent(@NonNull MotionEvent ev) { final int action = ev.getActionMasked(); final int actionIndex = ev.getActionIndex(); @@ -1453,7 +1460,7 @@ public class ViewDragHelper { * @param y Y position to test in the parent's coordinate system * @return true if the supplied view is under the given point, false otherwise */ - public boolean isViewUnder(View view, int x, int y) { + public boolean isViewUnder(@Nullable View view, int x, int y) { if (view == null) { return false; } @@ -1471,6 +1478,7 @@ public class ViewDragHelper { * @param y Y position to test in the parent's coordinate system * @return The topmost child view under (x, y) or null if none found. */ + @Nullable public View findTopChildUnder(int x, int y) { final int childCount = mParentView.getChildCount(); for (int i = childCount - 1; i >= 0; i--) { diff --git a/android/support/v7/app/NotificationCompat.java b/android/support/v7/app/NotificationCompat.java deleted file mode 100644 index 6b2b8591..00000000 --- a/android/support/v7/app/NotificationCompat.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.v7.app; - -import android.content.Context; - -/** - * An extension of {@link android.support.v4.app.NotificationCompat} which adds additional styles. - * @deprecated Use {@link android.support.v4.app.NotificationCompat}. - */ -@Deprecated -public class NotificationCompat extends android.support.v4.app.NotificationCompat { - - /** - * @deprecated Use the static classes in {@link android.support.v4.app.NotificationCompat}. - */ - @Deprecated - public NotificationCompat() { - } - - /** - * @deprecated All {@link android.support.v4.app.NotificationCompat.Style styles} can now be - * used with {@link android.support.v4.app.NotificationCompat.Builder}. - */ - @Deprecated - public static class Builder extends android.support.v4.app.NotificationCompat.Builder { - - /** - * @inheritDoc - * @deprecated Use {@link android.support.v4.app.NotificationCompat.Builder - * #NotificationCompat.Builder(Context, String)} - */ - @Deprecated - public Builder(Context context) { - super(context); - } - } -} diff --git a/android/support/v7/graphics/Palette.java b/android/support/v7/graphics/Palette.java index b7fb0545..e716fb50 100644 --- a/android/support/v7/graphics/Palette.java +++ b/android/support/v7/graphics/Palette.java @@ -80,7 +80,7 @@ public final class Palette { /** * Called when the {@link Palette} has been generated. */ - void onGenerated(Palette palette); + void onGenerated(@NonNull Palette palette); } static final int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112; @@ -95,7 +95,8 @@ public final class Palette { /** * Start generating a {@link Palette} with the returned {@link Builder} instance. */ - public static Builder from(Bitmap bitmap) { + @NonNull + public static Builder from(@NonNull Bitmap bitmap) { return new Builder(bitmap); } @@ -104,7 +105,8 @@ public final class Palette { * This is useful for testing, or if you want to resurrect a {@link Palette} instance from a * list of swatches. Will return null if the {@code swatches} is null. */ - public static Palette from(List<Swatch> swatches) { + @NonNull + public static Palette from(@NonNull List<Swatch> swatches) { return new Builder(swatches).generate(); } @@ -484,6 +486,7 @@ public final class Palette { * hsv[1] is Saturation [0...1] * hsv[2] is Lightness [0...1] */ + @NonNull public float[] getHsl() { if (mHsl == null) { mHsl = new float[3]; @@ -610,7 +613,7 @@ public final class Palette { /** * Construct a new {@link Builder} using a source {@link Bitmap} */ - public Builder(Bitmap bitmap) { + public Builder(@NonNull Bitmap bitmap) { if (bitmap == null || bitmap.isRecycled()) { throw new IllegalArgumentException("Bitmap is not valid"); } @@ -631,7 +634,7 @@ public final class Palette { * Construct a new {@link Builder} using a list of {@link Swatch} instances. * Typically only used for testing. */ - public Builder(List<Swatch> swatches) { + public Builder(@NonNull List<Swatch> swatches) { if (swatches == null || swatches.isEmpty()) { throw new IllegalArgumentException("List of Swatches is not valid"); } @@ -850,7 +853,8 @@ public final class Palette { * generated. */ @NonNull - public AsyncTask<Bitmap, Void, Palette> generate(final PaletteAsyncListener listener) { + public AsyncTask<Bitmap, Void, Palette> generate( + @NonNull final PaletteAsyncListener listener) { if (listener == null) { throw new IllegalArgumentException("listener can not be null"); } @@ -943,7 +947,7 @@ public final class Palette { * * @see Builder#addFilter(Filter) */ - boolean isAllowed(int rgb, float[] hsl); + boolean isAllowed(@ColorInt int rgb, @NonNull float[] hsl); } /** diff --git a/android/support/v7/graphics/Target.java b/android/support/v7/graphics/Target.java index 640970b8..0eff90b4 100644 --- a/android/support/v7/graphics/Target.java +++ b/android/support/v7/graphics/Target.java @@ -17,6 +17,7 @@ package android.support.v7.graphics; import android.support.annotation.FloatRange; +import android.support.annotation.NonNull; /** * A class which allows custom selection of colors in a {@link Palette}'s generation. Instances @@ -122,7 +123,7 @@ public final class Target { setDefaultWeights(); } - Target(Target from) { + Target(@NonNull Target from) { System.arraycopy(from.mSaturationTargets, 0, mSaturationTargets, 0, mSaturationTargets.length); System.arraycopy(from.mLightnessTargets, 0, mLightnessTargets, 0, @@ -295,13 +296,14 @@ public final class Target { /** * Create a new builder based on an existing {@link Target}. */ - public Builder(Target target) { + public Builder(@NonNull Target target) { mTarget = new Target(target); } /** * Set the minimum saturation value for this target. */ + @NonNull public Builder setMinimumSaturation(@FloatRange(from = 0, to = 1) float value) { mTarget.mSaturationTargets[INDEX_MIN] = value; return this; @@ -310,6 +312,7 @@ public final class Target { /** * Set the target/ideal saturation value for this target. */ + @NonNull public Builder setTargetSaturation(@FloatRange(from = 0, to = 1) float value) { mTarget.mSaturationTargets[INDEX_TARGET] = value; return this; @@ -318,6 +321,7 @@ public final class Target { /** * Set the maximum saturation value for this target. */ + @NonNull public Builder setMaximumSaturation(@FloatRange(from = 0, to = 1) float value) { mTarget.mSaturationTargets[INDEX_MAX] = value; return this; @@ -326,6 +330,7 @@ public final class Target { /** * Set the minimum lightness value for this target. */ + @NonNull public Builder setMinimumLightness(@FloatRange(from = 0, to = 1) float value) { mTarget.mLightnessTargets[INDEX_MIN] = value; return this; @@ -334,6 +339,7 @@ public final class Target { /** * Set the target/ideal lightness value for this target. */ + @NonNull public Builder setTargetLightness(@FloatRange(from = 0, to = 1) float value) { mTarget.mLightnessTargets[INDEX_TARGET] = value; return this; @@ -342,6 +348,7 @@ public final class Target { /** * Set the maximum lightness value for this target. */ + @NonNull public Builder setMaximumLightness(@FloatRange(from = 0, to = 1) float value) { mTarget.mLightnessTargets[INDEX_MAX] = value; return this; @@ -358,6 +365,7 @@ public final class Target { * * @see #setTargetSaturation(float) */ + @NonNull public Builder setSaturationWeight(@FloatRange(from = 0) float weight) { mTarget.mWeights[INDEX_WEIGHT_SAT] = weight; return this; @@ -374,6 +382,7 @@ public final class Target { * * @see #setTargetLightness(float) */ + @NonNull public Builder setLightnessWeight(@FloatRange(from = 0) float weight) { mTarget.mWeights[INDEX_WEIGHT_LUMA] = weight; return this; @@ -389,6 +398,7 @@ public final class Target { * <p>A weight of 0 means that it has no weight, and thus has no * bearing on the selection.</p> */ + @NonNull public Builder setPopulationWeight(@FloatRange(from = 0) float weight) { mTarget.mWeights[INDEX_WEIGHT_POP] = weight; return this; @@ -401,6 +411,7 @@ public final class Target { * @param exclusive true if any the color is exclusive to this target, or false is the * color can be selected for other targets. */ + @NonNull public Builder setExclusive(boolean exclusive) { mTarget.mIsExclusive = exclusive; return this; @@ -409,6 +420,7 @@ public final class Target { /** * Builds and returns the resulting {@link Target}. */ + @NonNull public Target build() { return mTarget; } diff --git a/android/support/v7/preference/Preference.java b/android/support/v7/preference/Preference.java index cfc4311f..fa8461db 100644 --- a/android/support/v7/preference/Preference.java +++ b/android/support/v7/preference/Preference.java @@ -31,7 +31,6 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.content.ContextCompat; -import android.support.v4.content.SharedPreferencesCompat; import android.support.v4.content.res.TypedArrayUtils; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.text.TextUtils; @@ -1543,7 +1542,7 @@ public class Preference implements Comparable<Preference> { private void tryCommit(@NonNull SharedPreferences.Editor editor) { if (mPreferenceManager.shouldCommit()) { - SharedPreferencesCompat.EditorCompat.getInstance().apply(editor); + editor.apply(); } } diff --git a/android/support/v7/preference/PreferenceFragmentCompat.java b/android/support/v7/preference/PreferenceFragmentCompat.java index 4fb9ff8d..6094217e 100644 --- a/android/support/v7/preference/PreferenceFragmentCompat.java +++ b/android/support/v7/preference/PreferenceFragmentCompat.java @@ -92,13 +92,13 @@ import android.view.ViewGroup; * <p>The following sample code shows a simple preference fragment that is * populated from a resource. The resource it loads is:</p> * - * {@sample frameworks/support/samples/SupportPreferenceDemos/res/xml/preferences.xml preferences} + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml preferences} * * <p>The fragment implementation itself simply populates the preferences * when created. Note that the preferences framework takes care of loading * the current values out of the app preferences and writing them when changed:</p> * - * {@sample frameworks/support/samples/SupportPreferenceDemos/src/com/example/android/supportpreference/FragmentSupportPreferencesCompat.java + * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesCompat.java * support_fragment_compat} * * @see Preference @@ -321,7 +321,7 @@ public abstract class PreferenceFragmentCompat extends Fragment implements } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (mHavePrefs) { diff --git a/android/support/v7/preference/PreferenceManager.java b/android/support/v7/preference/PreferenceManager.java index 83af86ca..19b6908a 100644 --- a/android/support/v7/preference/PreferenceManager.java +++ b/android/support/v7/preference/PreferenceManager.java @@ -25,7 +25,6 @@ import android.os.Build; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.content.ContextCompat; -import android.support.v4.content.SharedPreferencesCompat; import android.text.TextUtils; /** @@ -464,10 +463,9 @@ public class PreferenceManager { pm.setSharedPreferencesMode(sharedPreferencesMode); pm.inflateFromResource(context, resId, null); - SharedPreferences.Editor editor = - defaultValueSp.edit().putBoolean(KEY_HAS_SET_DEFAULT_VALUES, true); - - SharedPreferencesCompat.EditorCompat.getInstance().apply(editor); + defaultValueSp.edit() + .putBoolean(KEY_HAS_SET_DEFAULT_VALUES, true) + .apply(); } } @@ -511,7 +509,7 @@ public class PreferenceManager { private void setNoCommit(boolean noCommit) { if (!noCommit && mEditor != null) { - SharedPreferencesCompat.EditorCompat.getInstance().apply(mEditor); + mEditor.apply(); } mNoCommit = noCommit; } diff --git a/android/support/v7/recyclerview/extensions/ListAdapter.java b/android/support/v7/recyclerview/extensions/ListAdapter.java index e08cb53c..8b28072e 100644 --- a/android/support/v7/recyclerview/extensions/ListAdapter.java +++ b/android/support/v7/recyclerview/extensions/ListAdapter.java @@ -66,27 +66,21 @@ import java.util.List; * public void onBindViewHolder(UserViewHolder holder, int position) { * holder.bindTo(getItem(position)); * } - * } - * - * {@literal @}Entity - * class User { - * // ... simple POJO code omitted ... - * - * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<Customer>() { - * {@literal @}Override - * public boolean areItemsTheSame( - * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { - * // User properties may have changed if reloaded from the DB, but ID is fixed - * return oldUser.getId() == newUser.getId(); - * } - * {@literal @}Override - * public boolean areContentsTheSame( - * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { - * // NOTE: if you use equals, your object must properly override Object#equals() - * // Incorrectly returning false here will result in too many animations. - * return oldUser.equals(newUser); - * } - * } + * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() { + * {@literal @}Override + * public boolean areItemsTheSame( + * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { + * // User properties may have changed if reloaded from the DB, but ID is fixed + * return oldUser.getId() == newUser.getId(); + * } + * {@literal @}Override + * public boolean areContentsTheSame( + * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { + * // NOTE: if you use equals, your object must properly override Object#equals() + * // Incorrectly returning false here will result in too many animations. + * return oldUser.equals(newUser); + * } + * } * }</pre> * * Advanced users that wish for more control over adapter behavior, or to provide a specific base diff --git a/android/support/v7/recyclerview/extensions/ListAdapterConfig.java b/android/support/v7/recyclerview/extensions/ListAdapterConfig.java index f861242b..25697a11 100644 --- a/android/support/v7/recyclerview/extensions/ListAdapterConfig.java +++ b/android/support/v7/recyclerview/extensions/ListAdapterConfig.java @@ -16,7 +16,7 @@ package android.support.v7.recyclerview.extensions; -import android.arch.core.executor.AppToolkitTaskExecutor; +import android.arch.core.executor.ArchTaskExecutor; import java.util.concurrent.Executor; @@ -118,10 +118,10 @@ public final class ListAdapterConfig<T> { throw new IllegalArgumentException("Must provide a diffCallback"); } if (mBackgroundThreadExecutor == null) { - mBackgroundThreadExecutor = AppToolkitTaskExecutor.getIOThreadExecutor(); + mBackgroundThreadExecutor = ArchTaskExecutor.getIOThreadExecutor(); } if (mMainThreadExecutor == null) { - mMainThreadExecutor = AppToolkitTaskExecutor.getMainThreadExecutor(); + mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor(); } return new ListAdapterConfig<>( mMainThreadExecutor, diff --git a/android/support/v7/recyclerview/extensions/ListAdapterHelper.java b/android/support/v7/recyclerview/extensions/ListAdapterHelper.java index b47b833a..d0c7bb3e 100644 --- a/android/support/v7/recyclerview/extensions/ListAdapterHelper.java +++ b/android/support/v7/recyclerview/extensions/ListAdapterHelper.java @@ -84,27 +84,21 @@ import java.util.List; * User user = mHelper.getItem(position); * holder.bindTo(user); * } - * } - * - * {@literal @}Entity - * class User { - * // ... simple POJO code omitted ... - * - * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<Customer>() { - * {@literal @}Override - * public boolean areItemsTheSame( - * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { - * // User properties may have changed if reloaded from the DB, but ID is fixed - * return oldUser.getId() == newUser.getId(); - * } - * {@literal @}Override - * public boolean areContentsTheSame( - * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { - * // NOTE: if you use equals, your object must properly override Object#equals() - * // Incorrectly returning false here will result in too many animations. - * return oldUser.equals(newUser); - * } - * } + * public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() { + * {@literal @}Override + * public boolean areItemsTheSame( + * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { + * // User properties may have changed if reloaded from the DB, but ID is fixed + * return oldUser.getId() == newUser.getId(); + * } + * {@literal @}Override + * public boolean areContentsTheSame( + * {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) { + * // NOTE: if you use equals, your object must properly override Object#equals() + * // Incorrectly returning false here will result in too many animations. + * return oldUser.equals(newUser); + * } + * } * }</pre> * * @param <T> Type of the lists this helper will receive. diff --git a/android/support/v7/widget/AppCompatTextHelper.java b/android/support/v7/widget/AppCompatTextHelper.java index 75fa38ff..51510aa2 100644 --- a/android/support/v7/widget/AppCompatTextHelper.java +++ b/android/support/v7/widget/AppCompatTextHelper.java @@ -214,7 +214,7 @@ class AppCompatTextHelper { : R.styleable.TextAppearance_fontFamily; if (!context.isRestricted()) { try { - mFontTypeface = a.getFont(fontFamilyId, mStyle, mView); + mFontTypeface = a.getFont(fontFamilyId, mStyle); } catch (UnsupportedOperationException | Resources.NotFoundException e) { // Expected if it is not a font resource. } diff --git a/android/support/v7/widget/CardView.java b/android/support/v7/widget/CardView.java index 3df45d9f..58a04f0a 100644 --- a/android/support/v7/widget/CardView.java +++ b/android/support/v7/widget/CardView.java @@ -24,6 +24,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.cardview.R; import android.util.AttributeSet; @@ -106,17 +107,17 @@ public class CardView extends FrameLayout { final Rect mShadowBounds = new Rect(); - public CardView(Context context) { + public CardView(@NonNull Context context) { super(context); initialize(context, null, 0); } - public CardView(Context context, AttributeSet attrs) { + public CardView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); initialize(context, attrs, 0); } - public CardView(Context context, AttributeSet attrs, int defStyleAttr) { + public CardView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(context, attrs, defStyleAttr); } @@ -300,6 +301,7 @@ public class CardView extends FrameLayout { * * @return The background color state list of the CardView. */ + @NonNull public ColorStateList getCardBackgroundColor() { return IMPL.getBackgroundColor(mCardViewDelegate); } diff --git a/android/support/v7/widget/TintTypedArray.java b/android/support/v7/widget/TintTypedArray.java index 2213dd30..22709551 100644 --- a/android/support/v7/widget/TintTypedArray.java +++ b/android/support/v7/widget/TintTypedArray.java @@ -25,7 +25,6 @@ import android.content.res.TypedArray; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; @@ -34,7 +33,6 @@ import android.support.v4.content.res.ResourcesCompat; import android.support.v7.content.res.AppCompatResources; import android.util.AttributeSet; import android.util.TypedValue; -import android.widget.TextView; /** * A class that wraps a {@link android.content.res.TypedArray} and provides the same public API @@ -98,9 +96,9 @@ public class TintTypedArray { * * @param index Index of attribute to retrieve. * @param style A style value used for selecting best match font from the list of family. Note - * that this value will be ignored if the platform supports font family(API 24 or later). - * @param targetView A text view to be applied this font. If async loading is specified in XML, - * this view will be refreshed with result typeface. + * that this value will be ignored if the platform supports font family (API 24 or later). + * @param fontCallback A callback to receive async fetching of this font. If async loading is + * specified in XML, this callback will be triggered. * * @return Typeface for the attribute, or {@code null} if not defined. * @throws RuntimeException if the TypedArray has already been recycled. @@ -108,7 +106,7 @@ public class TintTypedArray { * not a font resource. */ @Nullable - public Typeface getFont(@StyleableRes int index, int style, @NonNull TextView targetView) { + public Typeface getFont(@StyleableRes int index, int style) { final int resourceId = mWrapped.getResourceId(index, 0); if (resourceId == 0) { return null; @@ -116,7 +114,7 @@ public class TintTypedArray { if (mTypedValue == null) { mTypedValue = new TypedValue(); } - return ResourcesCompat.getFont(mContext, resourceId, mTypedValue, style, targetView); + return ResourcesCompat.getFont(mContext, resourceId, mTypedValue, style); } public int length() { diff --git a/android/support/v7/widget/helper/ItemTouchHelper.java b/android/support/v7/widget/helper/ItemTouchHelper.java index b0a2cb36..aee48dfa 100644 --- a/android/support/v7/widget/helper/ItemTouchHelper.java +++ b/android/support/v7/widget/helper/ItemTouchHelper.java @@ -292,6 +292,11 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration */ GestureDetectorCompat mGestureDetector; + /** + * Callback for when long press occurs. + */ + private ItemTouchHelperGestureListener mItemTouchHelperGestureListener; + private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) { @@ -468,7 +473,7 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration mRecyclerView.addItemDecoration(this); mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); mRecyclerView.addOnChildAttachStateChangeListener(this); - initGestureDetector(); + startGestureDetection(); } private void destroyCallbacks() { @@ -485,14 +490,23 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration mOverdrawChild = null; mOverdrawChildPosition = -1; releaseVelocityTracker(); + stopGestureDetection(); + } + + private void startGestureDetection() { + mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener(); + mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(), + mItemTouchHelperGestureListener); } - private void initGestureDetector() { + private void stopGestureDetection() { + if (mItemTouchHelperGestureListener != null) { + mItemTouchHelperGestureListener.doNotReactToLongPress(); + mItemTouchHelperGestureListener = null; + } if (mGestureDetector != null) { - return; + mGestureDetector = null; } - mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(), - new ItemTouchHelperGestureListener()); } private void getSelectedDxDy(float[] outPosition) { @@ -2242,9 +2256,33 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { + /** + * Whether to execute code in response to the the invoking of + * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)}. + * + * It is necessary to control this here because + * {@link GestureDetector.SimpleOnGestureListener} can only be set on a + * {@link GestureDetector} in a GestureDetector's constructor, a GestureDetector will call + * onLongPress if an {@link MotionEvent#ACTION_DOWN} event is not followed by another event + * that would cancel it (like {@link MotionEvent#ACTION_UP} or + * {@link MotionEvent#ACTION_CANCEL}), the long press responding to the long press event + * needs to be cancellable to prevent unexpected behavior. + * + * @see #doNotReactToLongPress() + */ + private boolean mShouldReactToLongPress = true; + ItemTouchHelperGestureListener() { } + /** + * Call to prevent executing code in response to + * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)} being called. + */ + void doNotReactToLongPress() { + mShouldReactToLongPress = false; + } + @Override public boolean onDown(MotionEvent e) { return true; @@ -2252,6 +2290,9 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration @Override public void onLongPress(MotionEvent e) { + if (!mShouldReactToLongPress) { + return; + } View child = findChildView(e); if (child != null) { ViewHolder vh = mRecyclerView.getChildViewHolder(child); diff --git a/android/support/wear/ambient/AmbientDelegate.java b/android/support/wear/ambient/AmbientDelegate.java new file mode 100644 index 00000000..49012908 --- /dev/null +++ b/android/support/wear/ambient/AmbientDelegate.java @@ -0,0 +1,207 @@ +/* + * 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.wear.ambient; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.google.android.wearable.compat.WearableActivityController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; + +/** + * Provides compatibility for ambient mode. + */ +final class AmbientDelegate { + + private static final String TAG = "AmbientDelegate"; + + private WearableActivityController mWearableController; + + private static boolean sInitAutoResumeEnabledMethod; + private static boolean sHasAutoResumeEnabledMethod; + private final WearableControllerProvider mWearableControllerProvider; + private final AmbientCallback mCallback; + private final WeakReference<Activity> mActivity; + + /** + * AmbientCallback must be implemented by all users of the delegate. + */ + interface AmbientCallback { + /** + * Called when an activity is entering ambient mode. This event is sent while an activity is + * running (after onResume, before onPause). All drawing should complete by the conclusion + * of this method. Note that {@code invalidate()} calls will be executed before resuming + * lower-power mode. + * <p> + * <p><em>Derived classes must call through to the super class's implementation of this + * method. If they do not, an exception will be thrown.</em> + * + * @param ambientDetails bundle containing information about the display being used. + * It includes information about low-bit color and burn-in protection. + */ + void onEnterAmbient(Bundle ambientDetails); + + /** + * Called when the system is updating the display for ambient mode. Activities may use this + * opportunity to update or invalidate views. + */ + void onUpdateAmbient(); + + /** + * Called when an activity should exit ambient mode. This event is sent while an activity is + * running (after onResume, before onPause). + * <p> + * <p><em>Derived classes must call through to the super class's implementation of this + * method. If they do not, an exception will be thrown.</em> + */ + void onExitAmbient(); + } + + AmbientDelegate(@Nullable Activity activity, + @NonNull WearableControllerProvider wearableControllerProvider, + @NonNull AmbientCallback callback) { + mActivity = new WeakReference<>(activity); + mCallback = callback; + mWearableControllerProvider = wearableControllerProvider; + } + + /** + * Receives and handles the onCreate call from the associated {@link AmbientMode} + */ + void onCreate() { + Activity activity = mActivity.get(); + if (activity != null) { + mWearableController = + mWearableControllerProvider.getWearableController(activity, mCallback); + } + if (mWearableController != null) { + mWearableController.onCreate(); + } + } + + /** + * Receives and handles the onResume call from the associated {@link AmbientMode} + */ + void onResume() { + if (mWearableController != null) { + mWearableController.onResume(); + } + } + + /** + * Receives and handles the onPause call from the associated {@link AmbientMode} + */ + void onPause() { + if (mWearableController != null) { + mWearableController.onPause(); + } + } + + /** + * Receives and handles the onStop call from the associated {@link AmbientMode} + */ + void onStop() { + if (mWearableController != null) { + mWearableController.onStop(); + } + } + + /** + * Receives and handles the onDestroy call from the associated {@link AmbientMode} + */ + void onDestroy() { + if (mWearableController != null) { + mWearableController.onDestroy(); + } + } + + /** + * Sets that this activity should remain displayed when the system enters ambient mode. The + * default is false. In this case, the activity is stopped when the system enters ambient mode. + */ + void setAmbientEnabled() { + if (mWearableController != null) { + mWearableController.setAmbientEnabled(); + } + } + + /** + * Sets whether this activity's task should be moved to the front when the system exits ambient + * mode. If true, the activity's task may be moved to the front if it was the last activity to + * be running when ambient started, depending on how much time the system spent in ambient mode. + */ + void setAutoResumeEnabled(boolean enabled) { + if (mWearableController != null) { + if (hasSetAutoResumeEnabledMethod()) { + mWearableController.setAutoResumeEnabled(enabled); + } + } + } + + /** + * @return {@code true} if the activity is currently in ambient. + */ + boolean isAmbient() { + if (mWearableController != null) { + return mWearableController.isAmbient(); + } + return false; + } + + /** + * Dump the current state of the wearableController responsible for implementing the Ambient + * mode. + */ + void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + if (mWearableController != null) { + mWearableController.dump(prefix, fd, writer, args); + } + } + + private boolean hasSetAutoResumeEnabledMethod() { + if (!sInitAutoResumeEnabledMethod) { + sInitAutoResumeEnabledMethod = true; + try { + Method method = + WearableActivityController.class + .getDeclaredMethod("setAutoResumeEnabled", boolean.class); + // Proguard is sneaky -- it will actually rewrite strings it finds in addition to + // function names. Therefore add a "." prefix to the method name check to ensure the + // function was not renamed by proguard. + if (!(".setAutoResumeEnabled".equals("." + method.getName()))) { + throw new NoSuchMethodException(); + } + sHasAutoResumeEnabledMethod = true; + } catch (NoSuchMethodException e) { + Log.w( + "WearableActivity", + "Could not find a required method for auto-resume " + + "support, likely due to proguard optimization. Please add " + + "com.google.android.wearable:wearable jar to the list of library " + + "jars for your project"); + sHasAutoResumeEnabledMethod = false; + } + } + return sHasAutoResumeEnabledMethod; + } +} diff --git a/android/support/wear/ambient/AmbientMode.java b/android/support/wear/ambient/AmbientMode.java new file mode 100644 index 00000000..7fbbbb3f --- /dev/null +++ b/android/support/wear/ambient/AmbientMode.java @@ -0,0 +1,286 @@ +/* + * 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.wear.ambient; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +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; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Use this as a headless Fragment to add ambient support to an Activity on Wearable devices. + * <p> + * The application that uses this should add the {@link android.Manifest.permission#WAKE_LOCK} + * permission to its manifest. + * <p> + * The primary entry point for this code is the {@link #attachAmbientSupport(Activity)} method. + * It should be called with an {@link Activity} as an argument and that {@link Activity} will then + * be able to receive ambient lifecycle events through an {@link AmbientCallback}. The + * {@link Activity} will also receive a {@link AmbientController} object from the attachment which + * can be used to query the current status of the ambient mode, or toggle simple settings. + * An example of how to attach {@link AmbientMode} to your {@link Activity} and use + * the {@link AmbientController} can be found below: + * <p> + * <pre class="prettyprint">{@code + * AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this); + * controller.setAutoResumeEnabled(true); + * }</pre> + */ +public final class AmbientMode extends Fragment { + + /** + * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate + * whether burn-in protection is required. When this property is set to true, views must be + * shifted around periodically in ambient mode. To ensure that content isn't shifted off + * the screen, avoid placing content within 10 pixels of the edge of the screen. Activities + * should also avoid solid white areas to prevent pixel burn-in. Both of these requirements + * only apply in ambient mode, and only when this property is set to true. + */ + public static final String EXTRA_BURN_IN_PROTECTION = + WearableActivityController.EXTRA_BURN_IN_PROTECTION; + + /** + * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate + * whether the device has low-bit ambient mode. When this property is set to true, the screen + * supports fewer bits for each color in ambient mode. In this case, activities should disable + * anti-aliasing in ambient mode. + */ + public static final String EXTRA_LOWBIT_AMBIENT = + WearableActivityController.EXTRA_LOWBIT_AMBIENT; + + /** + * Fragment tag used by default when adding {@link AmbientMode} to add ambient support to an + * {@link Activity}. + */ + public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode"; + + /** + * Interface for any {@link Activity} that wishes to implement Ambient Mode. Use the + * {@link #getAmbientCallback()} method to return and {@link AmbientCallback} which can be used + * to bind the {@link AmbientMode} to the instantiation of this interface. + * <p> + * <pre class="prettyprint">{@code + * return new AmbientMode.AmbientCallback() { + * public void onEnterAmbient(Bundle ambientDetails) {...} + * public void onExitAmbient(Bundle ambientDetails) {...} + * } + * }</pre> + */ + public interface AmbientCallbackProvider { + /** + * @return the {@link AmbientCallback} to be used by this class to communicate with the + * entity interested in ambient events. + */ + AmbientCallback getAmbientCallback(); + } + + /** + * Callback to receive ambient mode state changes. It must be used by all users of AmbientMode. + */ + public abstract static class AmbientCallback { + /** + * Called when an activity is entering ambient mode. This event is sent while an activity is + * running (after onResume, before onPause). All drawing should complete by the conclusion + * of this method. Note that {@code invalidate()} calls will be executed before resuming + * lower-power mode. + * <p> + * <p><em>Derived classes must call through to the super class's implementation of this + * method. If they do not, an exception will be thrown.</em> + * + * @param ambientDetails bundle containing information about the display being used. + * It includes information about low-bit color and burn-in protection. + */ + public void onEnterAmbient(Bundle ambientDetails) {} + + /** + * Called when the system is updating the display for ambient mode. Activities may use this + * opportunity to update or invalidate views. + */ + public void onUpdateAmbient() {}; + + /** + * Called when an activity should exit ambient mode. This event is sent while an activity is + * running (after onResume, before onPause). + * <p> + * <p><em>Derived classes must call through to the super class's implementation of this + * method. If they do not, an exception will be thrown.</em> + */ + public void onExitAmbient() {}; + } + + private final AmbientDelegate.AmbientCallback mCallback = + new AmbientDelegate.AmbientCallback() { + @Override + public void onEnterAmbient(Bundle ambientDetails) { + mSuppliedCallback.onEnterAmbient(ambientDetails); + } + + @Override + public void onExitAmbient() { + mSuppliedCallback.onExitAmbient(); + } + + @Override + public void onUpdateAmbient() { + mSuppliedCallback.onUpdateAmbient(); + } + }; + private AmbientDelegate mDelegate; + private AmbientCallback mSuppliedCallback; + private AmbientController mController; + + /** + * Constructor + */ + public AmbientMode() { + mController = new AmbientController(); + } + + @Override + @CallSuper + public void onAttach(Context context) { + super.onAttach(context); + mDelegate = new AmbientDelegate(getActivity(), new WearableControllerProvider(), mCallback); + + if (context instanceof AmbientCallbackProvider) { + mSuppliedCallback = ((AmbientCallbackProvider) context).getAmbientCallback(); + } else { + throw new IllegalArgumentException( + "fragment should attach to an activity that implements AmbientCallback"); + } + } + + @Override + @CallSuper + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mDelegate.onCreate(); + mDelegate.setAmbientEnabled(); + } + + @Override + @CallSuper + public void onResume() { + super.onResume(); + mDelegate.onResume(); + } + + @Override + @CallSuper + public void onPause() { + mDelegate.onPause(); + super.onPause(); + } + + @Override + @CallSuper + public void onStop() { + mDelegate.onStop(); + super.onStop(); + } + + @Override + @CallSuper + public void onDestroy() { + mDelegate.onDestroy(); + super.onDestroy(); + } + + @Override + @CallSuper + public void onDetach() { + mDelegate = null; + super.onDetach(); + } + + /** + * Attach ambient support to the given activity. + * + * @param activity the activity to attach ambient support to. This activity has to also + * implement {@link AmbientCallbackProvider} + * @return the associated {@link AmbientController} which can be used to query the state of + * ambient mode and toggle simple settings related to it. + */ + public static <T extends Activity & AmbientCallbackProvider> AmbientController + attachAmbientSupport(T activity) { + FragmentManager fragmentManager = activity.getFragmentManager(); + AmbientMode ambientFragment = (AmbientMode) fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (ambientFragment == null) { + AmbientMode fragment = new AmbientMode(); + fragmentManager + .beginTransaction() + .add(fragment, FRAGMENT_TAG) + .commit(); + ambientFragment = fragment; + } + return ambientFragment.mController; + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + if (mDelegate != null) { + mDelegate.dump(prefix, fd, writer, args); + } + } + + @VisibleForTesting + void setAmbientDelegate(AmbientDelegate delegate) { + mDelegate = delegate; + } + + /** + * A class for interacting with the ambient mode on a wearable device. This class can be used to + * query the current state of ambient mode and to enable or disable certain settings. + * An instance of this class is returned to the user when they attach their {@link Activity} + * to {@link AmbientMode}. + */ + public final class AmbientController { + private static final String TAG = "AmbientController"; + + // Do not initialize outside of this class. + 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() { + return mDelegate == null ? false : mDelegate.isAmbient(); + } + } +} diff --git a/android/support/wear/ambient/SharedLibraryVersion.java b/android/support/wear/ambient/SharedLibraryVersion.java new file mode 100644 index 00000000..cd90a3b7 --- /dev/null +++ b/android/support/wear/ambient/SharedLibraryVersion.java @@ -0,0 +1,97 @@ +/* + * 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.wear.ambient; + +import android.os.Build; +import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; + +import com.google.android.wearable.WearableSharedLib; + +/** + * Internal class which can be used to determine the version of the wearable shared library that is + * available on the current device. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +final class SharedLibraryVersion { + + private SharedLibraryVersion() { + } + + /** + * Returns the version of the wearable shared library available on the current device. + * <p> + * <p>Version 1 was introduced on 2016-09-26, so any previous shared library will return 0. In + * those cases, it may be necessary to check {@code Build.VERSION.SDK_INT}. + * + * @throws IllegalStateException if the Wearable Shared Library is not present, which means that + * the {@code <uses-library>} tag is missing. + */ + public static int version() { + verifySharedLibraryPresent(); + return VersionHolder.VERSION; + } + + /** + * Throws {@link IllegalStateException} if the Wearable Shared Library is not present and API + * level is at least LMP MR1. + * <p> + * <p>This validates that the developer hasn't forgotten to include a {@code <uses-library>} tag + * in their manifest. The method should be used in combination with API level checks for + * features added before {@link #version() version} 1. + */ + public static void verifySharedLibraryPresent() { + if (!PresenceHolder.PRESENT) { + throw new IllegalStateException("Could not find wearable shared library classes. " + + "Please add <uses-library android:name=\"com.google.android.wearable\" " + + "android:required=\"false\" /> to the application manifest"); + } + } + + // Lazy initialization holder class (see Effective Java item 71) + @VisibleForTesting + static final class VersionHolder { + static final int VERSION = getSharedLibVersion(Build.VERSION.SDK_INT); + + @VisibleForTesting + static int getSharedLibVersion(int sdkInt) { + if (sdkInt < Build.VERSION_CODES.N_MR1) { + // WearableSharedLib was introduced in N MR1 (Wear FDP 4) + return 0; + } + return WearableSharedLib.version(); + } + } + + // Lazy initialization holder class (see Effective Java item 71) + @VisibleForTesting + static final class PresenceHolder { + static final boolean PRESENT = isSharedLibPresent(Build.VERSION.SDK_INT); + + @VisibleForTesting + static boolean isSharedLibPresent(int sdkInt) { + try { + // A class which has been available on the shared library from the first version. + Class.forName("com.google.android.wearable.compat.WearableActivityController"); + } catch (ClassNotFoundException e) { + return false; + } + return true; + } + } +} diff --git a/android/support/wear/ambient/WearableControllerProvider.java b/android/support/wear/ambient/WearableControllerProvider.java new file mode 100644 index 00000000..1682dc0e --- /dev/null +++ b/android/support/wear/ambient/WearableControllerProvider.java @@ -0,0 +1,96 @@ +/* + * 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.wear.ambient; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.RestrictTo; + +import com.google.android.wearable.compat.WearableActivityController; + +import java.lang.reflect.Method; + +/** + * Provides a {@link WearableActivityController} for ambient mode control. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class WearableControllerProvider { + + private static final String TAG = "WearableControllerProvider"; + + private static volatile boolean sAmbientCallbacksVerifiedPresent; + + /** + * Retrieves a {@link WearableActivityController} to use for ambient mode. + * + * @param activity The {@link Activity} to be associated with the Controller. + * @param callback The {@link AmbientDelegate.AmbientCallback} for the Controller. + * @return the platform-appropriate version of the {@link WearableActivityController}. + */ + public WearableActivityController getWearableController(Activity activity, + final AmbientDelegate.AmbientCallback callback) { + SharedLibraryVersion.verifySharedLibraryPresent(); + + // The AmbientCallback is an abstract class instead of an interface. + WearableActivityController.AmbientCallback callbackBridge = + new WearableActivityController.AmbientCallback() { + @Override + public void onEnterAmbient(Bundle ambientDetails) { + callback.onEnterAmbient(ambientDetails); + } + + @Override + public void onUpdateAmbient() { + callback.onUpdateAmbient(); + } + + @Override + public void onExitAmbient() { + callback.onExitAmbient(); + } + }; + + verifyAmbientCallbacksPresent(); + + return new WearableActivityController(TAG, activity, callbackBridge); + } + + private static void verifyAmbientCallbacksPresent() { + if (sAmbientCallbacksVerifiedPresent) { + return; + } + try { + Method method = + WearableActivityController.AmbientCallback.class.getDeclaredMethod( + "onEnterAmbient", Bundle.class); + // Proguard is sneaky -- it will actually rewrite strings it finds in addition to + // function names. Therefore add a "." prefix to the method name check to ensure the + // function was not renamed by proguard. + if (!(".onEnterAmbient".equals("." + method.getName()))) { + throw new NoSuchMethodException(); + } + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + "Could not find a required method for " + + "ambient support, likely due to proguard optimization. Please add " + + "com.google.android.wearable:wearable jar to the list of library jars" + + " for your project"); + } + sAmbientCallbacksVerifiedPresent = true; + } +} diff --git a/android/system/Os.java b/android/system/Os.java index 5b9ff476..8e312ddc 100644 --- a/android/system/Os.java +++ b/android/system/Os.java @@ -516,7 +516,6 @@ public final class Os { /** @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 setsockoptGroupSourceReq(FileDescriptor fd, int level, int option, StructGroupSourceReq value) throws ErrnoException { Libcore.os.setsockoptGroupSourceReq(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); } diff --git a/android/system/StructGroupSourceReq.java b/android/system/StructGroupSourceReq.java deleted file mode 100644 index c300338f..00000000 --- a/android/system/StructGroupSourceReq.java +++ /dev/null @@ -1,41 +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.system; - -import java.net.InetAddress; -import libcore.util.Objects; - -/** - * Corresponds to C's {@code struct group_source_req}. - * - * @hide - */ -public final class StructGroupSourceReq { - public final int gsr_interface; - public final InetAddress gsr_group; - public final InetAddress gsr_source; - - public StructGroupSourceReq(int gsr_interface, InetAddress gsr_group, InetAddress gsr_source) { - this.gsr_interface = gsr_interface; - this.gsr_group = gsr_group; - this.gsr_source = gsr_source; - } - - @Override public String toString() { - return Objects.toString(this); - } -} diff --git a/android/telephony/MbmsDownloadSession.java b/android/telephony/MbmsDownloadSession.java index ebac0419..764b7b22 100644 --- a/android/telephony/MbmsDownloadSession.java +++ b/android/telephony/MbmsDownloadSession.java @@ -522,8 +522,7 @@ public class MbmsDownloadSession implements AutoCloseable { * @param handler The {@link Handler} on which calls to {@code callback} should be enqueued on. */ public void registerStateCallback(@NonNull DownloadRequest request, - @NonNull DownloadStateCallback callback, - @NonNull Handler handler) { + @NonNull DownloadStateCallback callback, @NonNull Handler handler) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); @@ -533,7 +532,8 @@ public class MbmsDownloadSession implements AutoCloseable { new InternalDownloadStateCallback(callback, handler); try { - int result = downloadService.registerStateCallback(request, internalCallback); + int result = downloadService.registerStateCallback(request, internalCallback, + callback.getCallbackFilterFlags()); if (result != MbmsErrors.SUCCESS) { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); diff --git a/android/telephony/PhoneNumberUtils.java b/android/telephony/PhoneNumberUtils.java index d5ff1adb..ff671163 100644 --- a/android/telephony/PhoneNumberUtils.java +++ b/android/telephony/PhoneNumberUtils.java @@ -77,9 +77,28 @@ public class PhoneNumberUtils public static final int TOA_International = 0x91; public static final int TOA_Unknown = 0x81; + /* + * The BCD extended type used to determine the extended char for the digit which is greater than + * 9. + * + * see TS 51.011 section 10.5.1 EF_ADN(Abbreviated dialling numbers) + */ + public static final int BCD_EXTENDED_TYPE_EF_ADN = 1; + + /* + * The BCD extended type used to determine the extended char for the digit which is greater than + * 9. + * + * see TS 24.008 section 10.5.4.7 Called party BCD number + */ + public static final int BCD_EXTENDED_TYPE_CALLED_PARTY = 2; + static final String LOG_TAG = "PhoneNumberUtils"; private static final boolean DBG = false; + private static final String BCD_EF_ADN_EXTENDED = "*#,N;"; + private static final String BCD_CALLED_PARTY_EXTENDED = "*#abc"; + /* * global-phone-number = ["+"] 1*( DIGIT / written-sep ) * written-sep = ("-"/".") @@ -799,11 +818,33 @@ public class PhoneNumberUtils * * @return partial string on invalid decode * - * FIXME(mkf) support alphanumeric address type - * currently implemented in SMSMessage.getAddress() + * @deprecated use {@link #calledPartyBCDToString(byte[], int, int, int)} instead. Calling this + * method is equivalent to calling {@link #calledPartyBCDToString(byte[], int, int)} with + * {@link #BCD_EXTENDED_TYPE_EF_ADN} as the extended type. */ - public static String - calledPartyBCDToString (byte[] bytes, int offset, int length) { + @Deprecated + public static String calledPartyBCDToString(byte[] bytes, int offset, int length) { + return calledPartyBCDToString(bytes, offset, length, BCD_EXTENDED_TYPE_EF_ADN); + } + + /** + * 3GPP TS 24.008 10.5.4.7 + * Called Party BCD Number + * + * See Also TS 51.011 10.5.1 "dialing number/ssc string" + * and TS 11.11 "10.3.1 EF adn (Abbreviated dialing numbers)" + * + * @param bytes the data buffer + * @param offset should point to the TOA (aka. TON/NPI) octet after the length byte + * @param length is the number of bytes including TOA byte + * and must be at least 2 + * @param bcdExtType used to determine the extended bcd coding + * @see #BCD_EXTENDED_TYPE_EF_ADN + * @see #BCD_EXTENDED_TYPE_CALLED_PARTY + * + */ + public static String calledPartyBCDToString( + byte[] bytes, int offset, int length, int bcdExtType) { boolean prependPlus = false; StringBuilder ret = new StringBuilder(1 + length * 2); @@ -817,7 +858,7 @@ public class PhoneNumberUtils } internalCalledPartyBCDFragmentToString( - ret, bytes, offset + 1, length - 1); + ret, bytes, offset + 1, length - 1, bcdExtType); if (prependPlus && ret.length() == 0) { // If the only thing there is a prepended plus, return "" @@ -902,14 +943,13 @@ public class PhoneNumberUtils return ret.toString(); } - private static void - internalCalledPartyBCDFragmentToString( - StringBuilder sb, byte [] bytes, int offset, int length) { + private static void internalCalledPartyBCDFragmentToString( + StringBuilder sb, byte [] bytes, int offset, int length, int bcdExtType) { for (int i = offset ; i < length + offset ; i++) { byte b; char c; - c = bcdToChar((byte)(bytes[i] & 0xf)); + c = bcdToChar((byte)(bytes[i] & 0xf), bcdExtType); if (c == 0) { return; @@ -930,7 +970,7 @@ public class PhoneNumberUtils break; } - c = bcdToChar(b); + c = bcdToChar(b, bcdExtType); if (c == 0) { return; } @@ -943,49 +983,65 @@ public class PhoneNumberUtils /** * Like calledPartyBCDToString, but field does not start with a * TOA byte. For example: SIM ADN extension fields + * + * @deprecated use {@link #calledPartyBCDFragmentToString(byte[], int, int, int)} instead. + * Calling this method is equivalent to calling + * {@link #calledPartyBCDFragmentToString(byte[], int, int, int)} with + * {@link #BCD_EXTENDED_TYPE_EF_ADN} as the extended type. */ + @Deprecated + public static String calledPartyBCDFragmentToString(byte[] bytes, int offset, int length) { + return calledPartyBCDFragmentToString(bytes, offset, length, BCD_EXTENDED_TYPE_EF_ADN); + } - public static String - calledPartyBCDFragmentToString(byte [] bytes, int offset, int length) { + /** + * Like calledPartyBCDToString, but field does not start with a + * TOA byte. For example: SIM ADN extension fields + */ + public static String calledPartyBCDFragmentToString( + byte[] bytes, int offset, int length, int bcdExtType) { StringBuilder ret = new StringBuilder(length * 2); - - internalCalledPartyBCDFragmentToString(ret, bytes, offset, length); - + internalCalledPartyBCDFragmentToString(ret, bytes, offset, length, bcdExtType); return ret.toString(); } - /** returns 0 on invalid value */ - private static char - bcdToChar(byte b) { + /** + * Returns the correspond character for given {@code b} based on {@code bcdExtType}, or 0 on + * invalid code. + */ + private static char bcdToChar(byte b, int bcdExtType) { if (b < 0xa) { - return (char)('0' + b); - } else switch (b) { - case 0xa: return '*'; - case 0xb: return '#'; - case 0xc: return PAUSE; - case 0xd: return WILD; + return (char) ('0' + b); + } - default: return 0; + String extended = null; + if (BCD_EXTENDED_TYPE_EF_ADN == bcdExtType) { + extended = BCD_EF_ADN_EXTENDED; + } else if (BCD_EXTENDED_TYPE_CALLED_PARTY == bcdExtType) { + extended = BCD_CALLED_PARTY_EXTENDED; } + if (extended == null || b - 0xa >= extended.length()) { + return 0; + } + + return extended.charAt(b - 0xa); } - private static int - charToBCD(char c) { - if (c >= '0' && c <= '9') { + private static int charToBCD(char c, int bcdExtType) { + if ('0' <= c && c <= '9') { return c - '0'; - } else if (c == '*') { - return 0xa; - } else if (c == '#') { - return 0xb; - } else if (c == PAUSE) { - return 0xc; - } else if (c == WILD) { - return 0xd; - } else if (c == WAIT) { - return 0xe; - } else { - throw new RuntimeException ("invalid char for BCD " + c); } + + String extended = null; + if (BCD_EXTENDED_TYPE_EF_ADN == bcdExtType) { + extended = BCD_EF_ADN_EXTENDED; + } else if (BCD_EXTENDED_TYPE_CALLED_PARTY == bcdExtType) { + extended = BCD_CALLED_PARTY_EXTENDED; + } + if (extended == null || extended.indexOf(c) == -1) { + throw new RuntimeException("invalid char for BCD " + c); + } + return 0xa + extended.indexOf(c); } /** @@ -1034,40 +1090,60 @@ public class PhoneNumberUtils * * Returns null if network portion is empty. */ - public static byte[] - networkPortionToCalledPartyBCD(String s) { + public static byte[] networkPortionToCalledPartyBCD(String s) { String networkPortion = extractNetworkPortion(s); - return numberToCalledPartyBCDHelper(networkPortion, false); + return numberToCalledPartyBCDHelper( + networkPortion, false, BCD_EXTENDED_TYPE_EF_ADN); } /** * Same as {@link #networkPortionToCalledPartyBCD}, but includes a * one-byte length prefix. */ - public static byte[] - networkPortionToCalledPartyBCDWithLength(String s) { + public static byte[] networkPortionToCalledPartyBCDWithLength(String s) { String networkPortion = extractNetworkPortion(s); - return numberToCalledPartyBCDHelper(networkPortion, true); + return numberToCalledPartyBCDHelper( + networkPortion, true, BCD_EXTENDED_TYPE_EF_ADN); + } + + /** + * Convert a dialing number to BCD byte array + * + * @param number dialing number string. If the dialing number starts with '+', set to + * international TOA + * + * @return BCD byte array + * + * @deprecated use {@link #numberToCalledPartyBCD(String, int)} instead. Calling this method + * is equivalent to calling {@link #numberToCalledPartyBCD(String, int)} with + * {@link #BCD_EXTENDED_TYPE_EF_ADN} as the extended type. + */ + @Deprecated + public static byte[] numberToCalledPartyBCD(String number) { + return numberToCalledPartyBCD(number, BCD_EXTENDED_TYPE_EF_ADN); } /** * Convert a dialing number to BCD byte array * - * @param number dialing number string - * if the dialing number starts with '+', set to international TOA + * @param number dialing number string. If the dialing number starts with '+', set to + * international TOA + * @param bcdExtType used to determine the extended bcd coding + * @see #BCD_EXTENDED_TYPE_EF_ADN + * @see #BCD_EXTENDED_TYPE_CALLED_PARTY + * * @return BCD byte array */ - public static byte[] - numberToCalledPartyBCD(String number) { - return numberToCalledPartyBCDHelper(number, false); + public static byte[] numberToCalledPartyBCD(String number, int bcdExtType) { + return numberToCalledPartyBCDHelper(number, false, bcdExtType); } /** * If includeLength is true, prepend a one-byte length value to * the return array. */ - private static byte[] - numberToCalledPartyBCDHelper(String number, boolean includeLength) { + private static byte[] numberToCalledPartyBCDHelper( + String number, boolean includeLength, int bcdExtType) { int numberLenReal = number.length(); int numberLenEffective = numberLenReal; boolean hasPlus = number.indexOf('+') != -1; @@ -1087,7 +1163,8 @@ public class PhoneNumberUtils char c = number.charAt(i); if (c == '+') continue; int shift = ((digitCount & 0x01) == 1) ? 4 : 0; - result[extraBytes + (digitCount >> 1)] |= (byte)((charToBCD(c) & 0x0F) << shift); + result[extraBytes + (digitCount >> 1)] |= + (byte)((charToBCD(c, bcdExtType) & 0x0F) << shift); digitCount++; } diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java index cde0bdfd..c0564c55 100644 --- a/android/telephony/TelephonyManager.java +++ b/android/telephony/TelephonyManager.java @@ -107,8 +107,6 @@ public class TelephonyManager { public static final String MODEM_ACTIVITY_RESULT_KEY = BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY; - private static ITelephonyRegistry sRegistry; - /** * The allowed states of Wi-Fi calling. * @@ -179,11 +177,6 @@ public class TelephonyManager { mContext = context; } mSubscriptionManager = SubscriptionManager.from(mContext); - - if (sRegistry == null) { - sRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService( - "telephony.registry")); - } } /** @hide */ @@ -3513,6 +3506,10 @@ public class TelephonyManager { return ITelecomService.Stub.asInterface(ServiceManager.getService(Context.TELECOM_SERVICE)); } + private ITelephonyRegistry getTelephonyRegistry() { + return ITelephonyRegistry.Stub.asInterface(ServiceManager.getService("telephony.registry")); + } + // // // PhoneStateListener @@ -3552,12 +3549,16 @@ public class TelephonyManager { if (listener.mSubId == null) { listener.mSubId = mSubId; } - sRegistry.listenForSubscriber(listener.mSubId, getOpPackageName(), - listener.callback, events, notifyNow); + + ITelephonyRegistry registry = getTelephonyRegistry(); + if (registry != null) { + registry.listenForSubscriber(listener.mSubId, getOpPackageName(), + listener.callback, events, notifyNow); + } else { + Rlog.w(TAG, "telephony registry not ready."); + } } catch (RemoteException ex) { // system process dead - } catch (NullPointerException ex) { - // system process dead } } diff --git a/android/telephony/mbms/DownloadStateCallback.java b/android/telephony/mbms/DownloadStateCallback.java index 86920bd3..892fbf07 100644 --- a/android/telephony/mbms/DownloadStateCallback.java +++ b/android/telephony/mbms/DownloadStateCallback.java @@ -16,8 +16,12 @@ package android.telephony.mbms; +import android.annotation.IntDef; import android.telephony.MbmsDownloadSession; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A optional listener class used by download clients to track progress. Apps should extend this * class and pass an instance into @@ -29,6 +33,71 @@ import android.telephony.MbmsDownloadSession; public class DownloadStateCallback { /** + * Bitmask flags used for filtering out callback methods. Used when constructing the + * DownloadStateCallback as an optional parameter. + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ALL_UPDATES, PROGRESS_UPDATES, STATE_UPDATES}) + public @interface FilterFlag {} + + /** + * Receive all callbacks. + * Default value. + */ + public static final int ALL_UPDATES = 0x00; + /** + * Receive callbacks for {@link #onProgressUpdated}. + */ + public static final int PROGRESS_UPDATES = 0x01; + /** + * Receive callbacks for {@link #onStateUpdated}. + */ + public static final int STATE_UPDATES = 0x02; + + private final int mCallbackFilterFlags; + + /** + * Creates a DownloadStateCallback that will receive all callbacks. + */ + public DownloadStateCallback() { + mCallbackFilterFlags = ALL_UPDATES; + } + + /** + * Creates a DownloadStateCallback that will only receive callbacks for the methods specified + * via the filterFlags parameter. + * @param filterFlags A bitmask of filter flags that will specify which callback this instance + * is interested in. + */ + public DownloadStateCallback(int filterFlags) { + mCallbackFilterFlags = filterFlags; + } + + /** + * Return the currently set filter flags. + * @return An integer containing the bitmask of flags that this instance is interested in. + * @hide + */ + public int getCallbackFilterFlags() { + return mCallbackFilterFlags; + } + + /** + * Returns true if a filter flag is set for a particular callback method. If the flag is set, + * the callback will be delivered to the listening process. + * @param flag A filter flag specifying whether or not a callback method is registered to + * receive callbacks. + * @return true if registered to receive callbacks in the listening process, false if not. + */ + public final boolean isFilterFlagSet(@FilterFlag int flag) { + if (mCallbackFilterFlags == ALL_UPDATES) { + return true; + } + return (mCallbackFilterFlags & flag) > 0; + } + + /** * Called when the middleware wants to report progress for a file in a {@link DownloadRequest}. * * @param request a {@link DownloadRequest}, indicating which download is being referenced. diff --git a/android/telephony/mbms/MbmsDownloadReceiver.java b/android/telephony/mbms/MbmsDownloadReceiver.java index 61415b50..fe275372 100644 --- a/android/telephony/mbms/MbmsDownloadReceiver.java +++ b/android/telephony/mbms/MbmsDownloadReceiver.java @@ -165,6 +165,12 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { Log.w(LOG_TAG, "Download result did not include a result code. Ignoring."); return false; } + // We do not need to verify below extras if the result is not success. + if (MbmsDownloadSession.RESULT_SUCCESSFUL != + intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, + MbmsDownloadSession.RESULT_CANCELLED)) { + return true; + } if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) { Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring."); return false; diff --git a/android/telephony/mbms/ServiceInfo.java b/android/telephony/mbms/ServiceInfo.java index 9a01ed00..8529f525 100644 --- a/android/telephony/mbms/ServiceInfo.java +++ b/android/telephony/mbms/ServiceInfo.java @@ -23,6 +23,7 @@ import android.os.Parcelable; import android.text.TextUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -62,12 +63,6 @@ public class ServiceInfo { throw new RuntimeException("bad locales length " + newLocales.size()); } - for (Locale l : newLocales) { - if (!newNames.containsKey(l)) { - throw new IllegalArgumentException("A name must be provided for each locale"); - } - } - names = new HashMap(newNames.size()); names.putAll(newNames); className = newClassName; @@ -127,7 +122,7 @@ public class ServiceInfo { * Get the user-displayable name for this cell-broadcast service corresponding to the * provided {@link Locale}. * @param locale The {@link Locale} in which you want the name of the service. This must be a - * value from the list returned by {@link #getLocales()} -- an + * value from the set returned by {@link #getNamedContentLocales()} -- an * {@link java.util.NoSuchElementException} may be thrown otherwise. * @return The {@link CharSequence} providing the name of the service in the given * {@link Locale} @@ -140,6 +135,17 @@ public class ServiceInfo { } /** + * Return an unmodifiable set of the current {@link Locale}s that have a user-displayable name + * associated with them. The user-displayable name associated with any {@link Locale} in this + * set can be retrieved with {@link #getNameForLocale(Locale)}. + * @return An unmodifiable set of {@link Locale} objects corresponding to a user-displayable + * content name in that locale. + */ + public @NonNull Set<Locale> getNamedContentLocales() { + return Collections.unmodifiableSet(names.keySet()); + } + + /** * The class name for this service - used to categorize and filter */ public String getServiceClassName() { diff --git a/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java b/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java index d845a57b..2f85a1df 100644 --- a/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java +++ b/android/telephony/mbms/vendor/MbmsDownloadServiceBase.java @@ -46,6 +46,47 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { private final Map<IBinder, DownloadStateCallback> mDownloadCallbackBinderMap = new HashMap<>(); private final Map<IBinder, DeathRecipient> mDownloadCallbackDeathRecipients = new HashMap<>(); + + // Filters the DownloadStateCallbacks by its configuration from the app. + private abstract static class FilteredDownloadStateCallback extends DownloadStateCallback { + + private final IDownloadStateCallback mCallback; + public FilteredDownloadStateCallback(IDownloadStateCallback callback, int callbackFlags) { + super(callbackFlags); + mCallback = callback; + } + + @Override + public void onProgressUpdated(DownloadRequest request, FileInfo fileInfo, + int currentDownloadSize, int fullDownloadSize, int currentDecodedSize, + int fullDecodedSize) { + if (!isFilterFlagSet(PROGRESS_UPDATES)) { + return; + } + try { + mCallback.onProgressUpdated(request, fileInfo, currentDownloadSize, + fullDownloadSize, currentDecodedSize, fullDecodedSize); + } catch (RemoteException e) { + onRemoteException(e); + } + } + + @Override + public void onStateUpdated(DownloadRequest request, FileInfo fileInfo, + @MbmsDownloadSession.DownloadStatus int state) { + if (!isFilterFlagSet(STATE_UPDATES)) { + return; + } + try { + mCallback.onStateUpdated(request, fileInfo, state); + } catch (RemoteException e) { + onRemoteException(e); + } + } + + protected abstract void onRemoteException(RemoteException e); + } + /** * Initialize the download service for this app and subId, registering the listener. * @@ -196,9 +237,8 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { * @hide */ @Override - public final int registerStateCallback( - final DownloadRequest downloadRequest, final IDownloadStateCallback callback) - throws RemoteException { + public final int registerStateCallback(final DownloadRequest downloadRequest, + final IDownloadStateCallback callback, int flags) throws RemoteException { final int uid = Binder.getCallingUid(); DeathRecipient deathRecipient = new DeathRecipient() { @Override @@ -211,28 +251,10 @@ public class MbmsDownloadServiceBase extends IMbmsDownloadService.Stub { mDownloadCallbackDeathRecipients.put(callback.asBinder(), deathRecipient); callback.asBinder().linkToDeath(deathRecipient, 0); - DownloadStateCallback exposedCallback = new DownloadStateCallback() { + DownloadStateCallback exposedCallback = new FilteredDownloadStateCallback(callback, flags) { @Override - public void onProgressUpdated(DownloadRequest request, FileInfo fileInfo, int - currentDownloadSize, int fullDownloadSize, int currentDecodedSize, int - fullDecodedSize) { - try { - callback.onProgressUpdated(request, fileInfo, currentDownloadSize, - fullDownloadSize, - currentDecodedSize, fullDecodedSize); - } catch (RemoteException e) { - onAppCallbackDied(uid, downloadRequest.getSubscriptionId()); - } - } - - @Override - public void onStateUpdated(DownloadRequest request, FileInfo fileInfo, - @MbmsDownloadSession.DownloadStatus int state) { - try { - callback.onStateUpdated(request, fileInfo, state); - } catch (RemoteException e) { - onAppCallbackDied(uid, downloadRequest.getSubscriptionId()); - } + protected void onRemoteException(RemoteException e) { + onAppCallbackDied(uid, downloadRequest.getSubscriptionId()); } }; diff --git a/android/telephony/mbms/vendor/VendorUtils.java b/android/telephony/mbms/vendor/VendorUtils.java index 8fb27b29..a43f1224 100644 --- a/android/telephony/mbms/vendor/VendorUtils.java +++ b/android/telephony/mbms/vendor/VendorUtils.java @@ -38,8 +38,9 @@ public class VendorUtils { /** * The MBMS middleware should send this when a download of single file has completed or - * failed. Mandatory extras are + * failed. The only mandatory extra is * {@link MbmsDownloadSession#EXTRA_MBMS_DOWNLOAD_RESULT} + * and the following are required when the download has completed: * {@link MbmsDownloadSession#EXTRA_MBMS_FILE_INFO} * {@link MbmsDownloadSession#EXTRA_MBMS_DOWNLOAD_REQUEST} * {@link #EXTRA_TEMP_LIST} diff --git a/android/text/BoringLayoutCreateDrawPerfTest.java b/android/text/BoringLayoutCreateDrawPerfTest.java new file mode 100644 index 00000000..47dd257b --- /dev/null +++ b/android/text/BoringLayoutCreateDrawPerfTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.text; + +import static android.text.Layout.Alignment.ALIGN_NORMAL; + +import android.graphics.Canvas; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.support.test.filters.LargeTest; +import android.text.NonEditableTextGenerator.TextType; +import android.view.DisplayListCanvas; +import android.view.RenderNode; + +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.Collection; +import java.util.List; +import java.util.Random; + +/** + * Performance test for {@link BoringLayout} create and draw. + */ +@LargeTest +@RunWith(Parameterized.class) +public class BoringLayoutCreateDrawPerfTest { + + private static final boolean[] BOOLEANS = new boolean[]{false, true}; + private static final float SPACING_ADD = 10f; + private static final float SPACING_MULT = 1.5f; + + @Parameterized.Parameters(name = "cached={3},{1} chars,{0}") + public static Collection cases() { + final List<Object[]> params = new ArrayList<>(); + for (int length : new int[]{128}) { + for (boolean cached : BOOLEANS) { + for (TextType textType : new TextType[]{TextType.STRING, + TextType.SPANNABLE_BUILDER}) { + params.add(new Object[]{textType.name(), length, textType, cached}); + } + } + } + return params; + } + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private final int mLength; + private final TextType mTextType; + private final boolean mCached; + private final TextPaint mTextPaint; + + public BoringLayoutCreateDrawPerfTest(String label, int length, TextType textType, + boolean cached) { + mLength = length; + mCached = cached; + mTextType = textType; + mTextPaint = new TextPaint(); + mTextPaint.setTextSize(10); + } + + /** + * Measures the creation time for {@link BoringLayout}. + */ + @Test + public void timeCreate() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final CharSequence text = createRandomText(); + // isBoring result is calculated in another test, we want to measure only the + // create time for Boring without isBoring check. Therefore it is calculated here. + final BoringLayout.Metrics metrics = BoringLayout.isBoring(text, mTextPaint); + if (mCached) createLayout(text, metrics); + state.resumeTiming(); + + while (state.keepRunning()) { + state.pauseTiming(); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + createLayout(text, metrics); + } + } + + /** + * Measures the draw time for {@link BoringLayout} or {@link StaticLayout}. + */ + @Test + public void timeDraw() throws Throwable { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final RenderNode node = RenderNode.create("benchmark", null); + final CharSequence text = createRandomText(); + final BoringLayout.Metrics metrics = BoringLayout.isBoring(text, mTextPaint); + final Layout layout = createLayout(text, metrics); + state.resumeTiming(); + + while (state.keepRunning()) { + + state.pauseTiming(); + final DisplayListCanvas canvas = node.start(1200, 200); + final int save = canvas.save(); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + layout.draw(canvas); + + state.pauseTiming(); + canvas.restoreToCount(save); + node.end(canvas); + state.resumeTiming(); + } + } + + private CharSequence createRandomText() { + return new NonEditableTextGenerator(new Random(0)) + .setSequenceLength(mLength) + .setCreateBoring(true) + .setTextType(mTextType) + .build(); + } + + private Layout createLayout(CharSequence text, + BoringLayout.Metrics metrics) { + return BoringLayout.make(text, mTextPaint, Integer.MAX_VALUE /*width*/, + ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, metrics, true /*includePad*/); + } +} diff --git a/android/text/BoringLayoutIsBoringPerfTest.java b/android/text/BoringLayoutIsBoringPerfTest.java new file mode 100644 index 00000000..34de65de --- /dev/null +++ b/android/text/BoringLayoutIsBoringPerfTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.text; + +import android.graphics.Canvas; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.support.test.filters.LargeTest; +import android.text.NonEditableTextGenerator.TextType; + +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.Collection; +import java.util.List; +import java.util.Random; + +/** + * Performance test for {@link BoringLayout#isBoring(CharSequence, TextPaint)}. + */ +@LargeTest +@RunWith(Parameterized.class) +public class BoringLayoutIsBoringPerfTest { + + private static final boolean[] BOOLEANS = new boolean[]{false, true}; + + @Parameterized.Parameters(name = "cached={4},{1} chars,{0}") + public static Collection cases() { + final List<Object[]> params = new ArrayList<>(); + for (int length : new int[]{128}) { + for (boolean boring : BOOLEANS) { + for (boolean cached : BOOLEANS) { + for (TextType textType : new TextType[]{TextType.STRING, + TextType.SPANNABLE_BUILDER}) { + params.add(new Object[]{ + (boring ? "Boring" : "NotBoring") + "," + textType.name(), + length, boring, textType, cached}); + } + } + } + } + return params; + } + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private final int mLength; + private final TextType mTextType; + private final boolean mCreateBoring; + private final boolean mCached; + private final TextPaint mTextPaint; + + public BoringLayoutIsBoringPerfTest(String label, int length, boolean boring, TextType textType, + boolean cached) { + mLength = length; + mCreateBoring = boring; + mCached = cached; + mTextType = textType; + mTextPaint = new TextPaint(); + mTextPaint.setTextSize(10); + } + + /** + * Measure the time for the {@link BoringLayout#isBoring(CharSequence, TextPaint)}. + */ + @Test + public void timeIsBoring() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final CharSequence text = createRandomText(); + if (mCached) BoringLayout.isBoring(text, mTextPaint); + state.resumeTiming(); + + while (state.keepRunning()) { + state.pauseTiming(); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + BoringLayout.isBoring(text, mTextPaint); + } + } + + private CharSequence createRandomText() { + return new NonEditableTextGenerator(new Random(0)) + .setSequenceLength(mLength) + .setCreateBoring(mCreateBoring) + .setTextType(mTextType) + .build(); + } +} diff --git a/android/text/DynamicLayout.java b/android/text/DynamicLayout.java index 5e40935c..24260c4f 100644 --- a/android/text/DynamicLayout.java +++ b/android/text/DynamicLayout.java @@ -384,7 +384,7 @@ public class DynamicLayout extends Layout private DynamicLayout(@NonNull Builder b) { super(createEllipsizer(b.mEllipsize, b.mDisplay), - b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd); + b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd); mDisplay = b.mDisplay; mIncludePad = b.mIncludePad; diff --git a/android/text/DynamicLayoutPerfTest.java b/android/text/DynamicLayoutPerfTest.java index e644a1f3..b4c7f543 100644 --- a/android/text/DynamicLayoutPerfTest.java +++ b/android/text/DynamicLayoutPerfTest.java @@ -16,35 +16,28 @@ package android.text; -import android.app.Activity; +import static android.text.Layout.Alignment.ALIGN_NORMAL; + import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.FontMetricsInt; -import android.os.Bundle; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.perftests.utils.StubActivity; - -import android.support.test.InstrumentationRegistry; import android.support.test.filters.LargeTest; import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; import android.text.style.ReplacementSpan; import android.util.ArraySet; -import static android.text.Layout.Alignment.ALIGN_NORMAL; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Locale; -import java.util.Random; - -import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized.Parameters; import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Random; @LargeTest @RunWith(Parameterized.class) diff --git a/android/text/Hyphenator.java b/android/text/Hyphenator.java index ea1100ea..ad26f23a 100644 --- a/android/text/Hyphenator.java +++ b/android/text/Hyphenator.java @@ -16,7 +16,12 @@ 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; @@ -24,9 +29,6 @@ import com.android.internal.annotations.GuardedBy; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; import java.util.HashMap; import java.util.Locale; @@ -37,39 +39,19 @@ import java.util.Locale; * @hide */ public class Hyphenator { - // This class has deliberately simple lifetime management (no finalizer) because in - // the common case a process will use a very small number of locales. - private static String TAG = "Hyphenator"; - // TODO: Confirm that these are the best values. Various sources suggest (1, 1), but - // that appears too small. - private static final int INDIC_MIN_PREFIX = 2; - private static final int INDIC_MIN_SUFFIX = 2; - private final static Object sLock = new Object(); @GuardedBy("sLock") final static HashMap<Locale, Hyphenator> sMap = new HashMap<Locale, Hyphenator>(); - // Reasonable enough values for cases where we have no hyphenation patterns but may be able to - // do some automatic hyphenation based on characters. These values would be used very rarely. - private static final int DEFAULT_MIN_PREFIX = 2; - private static final int DEFAULT_MIN_SUFFIX = 2; - final static Hyphenator sEmptyHyphenator = - new Hyphenator(StaticLayout.nLoadHyphenator( - null, 0, DEFAULT_MIN_PREFIX, DEFAULT_MIN_SUFFIX), - null); - - final private long mNativePtr; - - // We retain a reference to the buffer to keep the memory mapping valid - @SuppressWarnings("unused") - final private ByteBuffer mBuffer; + private final long mNativePtr; + private final HyphenationData mData; - private Hyphenator(long nativePtr, ByteBuffer b) { + private Hyphenator(long nativePtr, HyphenationData data) { mNativePtr = nativePtr; - mBuffer = b; + mData = data; } public long getNativePtr() { @@ -90,8 +72,7 @@ public class Hyphenator { new Locale(locale.getLanguage(), "", variant); result = sMap.get(languageAndVariantOnlyLocale); if (result != null) { - sMap.put(locale, result); - return result; + return putAlias(locale, result); } } @@ -99,8 +80,7 @@ public class Hyphenator { final Locale languageOnlyLocale = new Locale(locale.getLanguage()); result = sMap.get(languageOnlyLocale); if (result != null) { - sMap.put(locale, result); - return result; + return putAlias(locale, result); } // Fall back to script-only, if available @@ -112,135 +92,94 @@ public class Hyphenator { .build(); result = sMap.get(scriptOnlyLocale); if (result != null) { - sMap.put(locale, result); - return result; + return putAlias(locale, result); } } - sMap.put(locale, sEmptyHyphenator); // To remember we found nothing. + return putEmptyAlias(locale); } - return sEmptyHyphenator; } private static class HyphenationData { - final String mLanguageTag; - final int mMinPrefix, mMinSuffix; - HyphenationData(String languageTag, int minPrefix, int minSuffix) { - this.mLanguageTag = languageTag; - this.mMinPrefix = minPrefix; - this.mMinSuffix = minSuffix; - } - } + private static 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); - private static Hyphenator loadHyphenator(HyphenationData data) { - String patternFilename = "hyph-" + data.mLanguageTag.toLowerCase(Locale.US) + ".hyb"; - File patternFile = new File(getSystemHyphenatorLocation(), patternFilename); - if (!patternFile.canRead()) { - Log.e(TAG, "hyphenation patterns for " + patternFile + " not found or unreadable"); - return null; + // Create empty HyphenationData. + private HyphenationData(int minPrefix, int minSuffix) { + mMinPrefix = minPrefix; + mMinSuffix = minSuffix; + mDataAddress = 0; } - try { - RandomAccessFile f = new RandomAccessFile(patternFile, "r"); - try { - FileChannel fc = f.getChannel(); - MappedByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); - long nativePtr = StaticLayout.nLoadHyphenator( - buf, 0, data.mMinPrefix, data.mMinSuffix); - return new Hyphenator(nativePtr, buf); - } finally { - f.close(); + + 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; } - } catch (IOException e) { - Log.e(TAG, "error loading hyphenation " + patternFile, e); - return null; } } - private static File getSystemHyphenatorLocation() { - return new File("/system/usr/hyphen-data"); + // 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; } - // This array holds pairs of language tags that are used to prefill the map from locale to - // hyphenation data: The hyphenation data for the first field will be prefilled from the - // hyphenation data for the second field. - // - // The aliases that are computable by the get() method above are not included. - private static final String[][] LOCALE_FALLBACK_DATA = { - // English locales that fall back to en-US. The data is - // from CLDR. It's all English locales, minus the locales whose - // parent is en-001 (from supplementalData.xml, under <parentLocales>). - // TODO: Figure out how to get this from ICU. - {"en-AS", "en-US"}, // English (American Samoa) - {"en-GU", "en-US"}, // English (Guam) - {"en-MH", "en-US"}, // English (Marshall Islands) - {"en-MP", "en-US"}, // English (Northern Mariana Islands) - {"en-PR", "en-US"}, // English (Puerto Rico) - {"en-UM", "en-US"}, // English (United States Minor Outlying Islands) - {"en-VI", "en-US"}, // English (Virgin Islands) - - // All English locales other than those falling back to en-US are mapped to en-GB. - {"en", "en-GB"}, - - // For German, we're assuming the 1996 (and later) orthography by default. - {"de", "de-1996"}, - // Liechtenstein uses the Swiss hyphenation rules for the 1901 orthography. - {"de-LI-1901", "de-CH-1901"}, - - // Norwegian is very probably Norwegian Bokmål. - {"no", "nb"}, - - // Use mn-Cyrl. According to CLDR's likelySubtags.xml, mn is most likely to be mn-Cyrl. - {"mn", "mn-Cyrl"}, // Mongolian - - // Fall back to Ethiopic script for languages likely to be written in Ethiopic. - // Data is from CLDR's likelySubtags.xml. - // TODO: Convert this to a mechanism using ICU4J's ULocale#addLikelySubtags(). - {"am", "und-Ethi"}, // Amharic - {"byn", "und-Ethi"}, // Blin - {"gez", "und-Ethi"}, // Geʻez - {"ti", "und-Ethi"}, // Tigrinya - {"wal", "und-Ethi"}, // Wolaytta - }; + // 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); + } - private static final HyphenationData[] AVAILABLE_LANGUAGES = { - new HyphenationData("as", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Assamese - new HyphenationData("bg", 2, 2), // Bulgarian - new HyphenationData("bn", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Bengali - new HyphenationData("cu", 1, 2), // Church Slavonic - new HyphenationData("cy", 2, 3), // Welsh - new HyphenationData("da", 2, 2), // Danish - new HyphenationData("de-1901", 2, 2), // German 1901 orthography - new HyphenationData("de-1996", 2, 2), // German 1996 orthography - new HyphenationData("de-CH-1901", 2, 2), // Swiss High German 1901 orthography - new HyphenationData("en-GB", 2, 3), // British English - new HyphenationData("en-US", 2, 3), // American English - new HyphenationData("es", 2, 2), // Spanish - new HyphenationData("et", 2, 3), // Estonian - new HyphenationData("eu", 2, 2), // Basque - new HyphenationData("fr", 2, 3), // French - new HyphenationData("ga", 2, 3), // Irish - new HyphenationData("gu", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Gujarati - new HyphenationData("hi", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Hindi - new HyphenationData("hr", 2, 2), // Croatian - new HyphenationData("hu", 2, 2), // Hungarian - // texhyphen sources say Armenian may be (1, 2), but that it needs confirmation. - // Going with a more conservative value of (2, 2) for now. - new HyphenationData("hy", 2, 2), // Armenian - new HyphenationData("kn", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Kannada - new HyphenationData("ml", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Malayalam - new HyphenationData("mn-Cyrl", 2, 2), // Mongolian in Cyrillic script - new HyphenationData("mr", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Marathi - new HyphenationData("nb", 2, 2), // Norwegian Bokmål - new HyphenationData("nn", 2, 2), // Norwegian Nynorsk - new HyphenationData("or", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Oriya - new HyphenationData("pa", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Punjabi - new HyphenationData("pt", 2, 3), // Portuguese - new HyphenationData("sl", 2, 2), // Slovenian - new HyphenationData("ta", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Tamil - new HyphenationData("te", INDIC_MIN_PREFIX, INDIC_MIN_SUFFIX), // Telugu - new HyphenationData("tk", 2, 2), // Turkmen - new HyphenationData("und-Ethi", 1, 1), // Any language in Ethiopic script - }; + // 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 @@ -250,20 +189,85 @@ public class Hyphenator { * @hide */ public static void init() { - sMap.put(null, null); - - for (int i = 0; i < AVAILABLE_LANGUAGES.length; i++) { - HyphenationData data = AVAILABLE_LANGUAGES[i]; - Hyphenator h = loadHyphenator(data); - if (h != null) { - sMap.put(Locale.forLanguageTag(data.mLanguageTag), h); - } + 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 } + }; - for (int i = 0; i < LOCALE_FALLBACK_DATA.length; i++) { - String language = LOCALE_FALLBACK_DATA[i][0]; - String fallback = LOCALE_FALLBACK_DATA[i][1]; - sMap.put(Locale.forLanguageTag(language), sMap.get(Locale.forLanguageTag(fallback))); - } - } + private static native long nBuildHyphenator(/* non-zero */ long dataAddress, + @NonNull String langTag, @IntRange(from = 1) int minPrefix, + @IntRange(from = 1) int minSuffix); } diff --git a/android/text/Hyphenator_Delegate.java b/android/text/Hyphenator_Delegate.java deleted file mode 100644 index 499e58a5..00000000 --- a/android/text/Hyphenator_Delegate.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.text; - -import com.android.layoutlib.bridge.impl.DelegateManager; -import com.android.tools.layoutlib.annotations.LayoutlibDelegate; - -import java.io.File; -import java.nio.ByteBuffer; - -/** - * Delegate that overrides implementation for certain methods in {@link android.text.Hyphenator} - * <p/> - * Through the layoutlib_create tool, selected methods of Hyphenator have been replaced - * by calls to methods of the same name in this delegate class. - */ -public class Hyphenator_Delegate { - - private static final DelegateManager<Hyphenator_Delegate> sDelegateManager = new - DelegateManager<Hyphenator_Delegate>(Hyphenator_Delegate.class); - - @LayoutlibDelegate - /*package*/ static File getSystemHyphenatorLocation() { - // FIXME - return null; - } - - /*package*/ @SuppressWarnings("UnusedParameters") // TODO implement this. - static long loadHyphenator(ByteBuffer buffer, int offset, int minPrefix, int minSuffix) { - return sDelegateManager.addNewDelegate(new Hyphenator_Delegate()); - } -} diff --git a/android/text/Layout.java b/android/text/Layout.java index 25f791bc..60fff738 100644 --- a/android/text/Layout.java +++ b/android/text/Layout.java @@ -1915,8 +1915,7 @@ public abstract class Layout { return margin; } - /* package */ - static float measurePara(TextPaint paint, CharSequence text, int start, int end, + private static float measurePara(TextPaint paint, CharSequence text, int start, int end, TextDirectionHeuristic textDir) { MeasuredText mt = MeasuredText.obtain(); TextLine tl = TextLine.obtain(); @@ -2146,18 +2145,14 @@ public abstract class Layout { * text within the layout of a line. */ public static class Directions { - // Directions represents directional runs within a line of text. - // Runs are pairs of ints listed in visual order, starting from the - // leading margin. The first int of each pair is the offset from - // the first character of the line to the start of the run. The - // second int represents both the length and level of the run. - // The length is in the lower bits, accessed by masking with - // DIR_LENGTH_MASK. The level is in the higher bits, accessed - // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. - // To simply test for an RTL direction, test the bit using - // DIR_RTL_FLAG, if set then the direction is rtl. - /** + * Directions represents directional runs within a line of text. Runs are pairs of ints + * listed in visual order, starting from the leading margin. The first int of each pair is + * the offset from the first character of the line to the start of the run. The second int + * represents both the length and level of the run. The length is in the lower bits, + * accessed by masking with RUN_LENGTH_MASK. The level is in the higher bits, accessed by + * shifting by RUN_LEVEL_SHIFT and masking by RUN_LEVEL_MASK. To simply test for an RTL + * direction, test the bit using RUN_RTL_FLAG, if set then the direction is rtl. * @hide */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) diff --git a/android/text/MeasuredText.java b/android/text/MeasuredText.java index ce3e2822..b09ccc29 100644 --- a/android/text/MeasuredText.java +++ b/android/text/MeasuredText.java @@ -90,10 +90,6 @@ class MeasuredText { } } - void setPos(int pos) { - mPos = pos - mTextStart; - } - /** * Analyzes text for bidirectional runs. Allocates working buffers. */ @@ -160,47 +156,43 @@ class MeasuredText { } } + /** + * Apply the style. + * + * If StaticLyaout.Builder is not provided in setPara() method, this method measures the styled + * text width. + * If StaticLayout.Builder is provided in setPara() method, this method just passes the style + * information to native code by calling StaticLayout.Builder.addstyleRun() and returns 0. + */ float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) { if (fm != null) { paint.getFontMetricsInt(fm); } - int p = mPos; + final int p = mPos; mPos = p + len; - // try to do widths measurement in native code, but use Java if paint has been subclassed - // FIXME: may want to eliminate special case for subclass - float[] widths = null; - if (mBuilder == null || paint.getClass() != TextPaint.class) { - widths = mWidths; - } if (mEasy) { - boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT; - float width = 0; - if (widths != null) { - width = paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, widths, p); - if (mBuilder != null) { - mBuilder.addMeasuredRun(p, p + len, widths); - } + final boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT; + if (mBuilder == null) { + return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p); } else { - width = mBuilder.addStyleRun(paint, p, p + len, isRtl); + mBuilder.addStyleRun(paint, p, p + len, isRtl); + return 0.0f; // Builder.addStyleRun doesn't return the width. } - return width; } float totalAdvance = 0; int level = mLevels[p]; for (int q = p, i = p + 1, e = p + len;; ++i) { if (i == e || mLevels[i] != level) { - boolean isRtl = (level & 0x1) != 0; - if (widths != null) { + final boolean isRtl = (level & 0x1) != 0; + if (mBuilder == null) { totalAdvance += - paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, widths, q); - if (mBuilder != null) { - mBuilder.addMeasuredRun(q, i, widths); - } + paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q); } else { - totalAdvance += mBuilder.addStyleRun(paint, q, i, isRtl); + // Builder.addStyleRun doesn't return the width. + mBuilder.addStyleRun(paint, q, i, isRtl); } if (i == e) { break; @@ -209,7 +201,7 @@ class MeasuredText { level = mLevels[i]; } } - return totalAdvance; + return totalAdvance; // If mBuilder is null, the result is zero. } float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, @@ -243,7 +235,7 @@ class MeasuredText { for (int i = mPos + 1, e = mPos + len; i < e; i++) w[i] = 0; } else { - mBuilder.addReplacementRun(mPos, mPos + len, wid); + mBuilder.addReplacementRun(paint, mPos, mPos + len, wid); } mPos += len; } diff --git a/android/text/NonEditableTextGenerator.java b/android/text/NonEditableTextGenerator.java new file mode 100644 index 00000000..7c0cf0ee --- /dev/null +++ b/android/text/NonEditableTextGenerator.java @@ -0,0 +1,138 @@ +package android.text; + +import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE; + +import android.text.style.BulletSpan; + +import java.util.Random; + +/** + * + */ +public class NonEditableTextGenerator { + + enum TextType { + STRING, + SPANNED, + SPANNABLE_BUILDER + } + + private boolean mCreateBoring; + private TextType mTextType; + private int mSequenceLength; + private final Random mRandom; + + public NonEditableTextGenerator(Random random) { + mRandom = random; + } + + public NonEditableTextGenerator setCreateBoring(boolean createBoring) { + mCreateBoring = createBoring; + return this; + } + + public NonEditableTextGenerator setTextType(TextType textType) { + mTextType = textType; + return this; + } + + public NonEditableTextGenerator setSequenceLength(int sequenceLength) { + mSequenceLength = sequenceLength; + return this; + } + + /** + * Sample charSequence generated: + * NRjPzjvUadHmH ExoEoTqfx pCLw qtndsqfpk AqajVCbgjGZ igIeC dfnXRgA + */ + public CharSequence build() { + final RandomCharSequenceGenerator sequenceGenerator = new RandomCharSequenceGenerator( + mRandom); + if (mSequenceLength > 0) { + sequenceGenerator.setSequenceLength(mSequenceLength); + } + + final CharSequence charSequence = sequenceGenerator.buildLatinSequence(); + + switch (mTextType) { + case SPANNED: + case SPANNABLE_BUILDER: + return createSpannable(charSequence); + case STRING: + default: + return createString(charSequence); + } + } + + private Spannable createSpannable(CharSequence charSequence) { + final Spannable spannable = (mTextType == TextType.SPANNABLE_BUILDER) ? + new SpannableStringBuilder(charSequence) : new SpannableString(charSequence); + + if (!mCreateBoring) { + // add a paragraph style to make it non boring + spannable.setSpan(new BulletSpan(), 0, spannable.length(), SPAN_INCLUSIVE_INCLUSIVE); + } + + spannable.setSpan(new Object(), 0, spannable.length(), SPAN_INCLUSIVE_INCLUSIVE); + spannable.setSpan(new Object(), 0, 1, SPAN_INCLUSIVE_INCLUSIVE); + + return spannable; + } + + private String createString(CharSequence charSequence) { + if (mCreateBoring) { + return charSequence.toString(); + } else { + // BoringLayout checks to see if there is a surrogate pair and if so tells that + // the charSequence is not suitable for boring. Add an emoji to make it non boring. + // Emoji is added instead of RTL, since emoji stays in the same run and is a more + // common case. + return charSequence.toString() + "\uD83D\uDC68\uD83C\uDFFF"; + } + } + + public static class RandomCharSequenceGenerator { + + private static final int DEFAULT_MIN_WORD_LENGTH = 3; + private static final int DEFAULT_MAX_WORD_LENGTH = 15; + private static final int DEFAULT_SEQUENCE_LENGTH = 256; + + private int mMinWordLength = DEFAULT_MIN_WORD_LENGTH; + private int mMaxWordLength = DEFAULT_MAX_WORD_LENGTH; + private int mSequenceLength = DEFAULT_SEQUENCE_LENGTH; + private final Random mRandom; + + public RandomCharSequenceGenerator(Random random) { + mRandom = random; + } + + public RandomCharSequenceGenerator setSequenceLength(int sequenceLength) { + mSequenceLength = sequenceLength; + return this; + } + + public CharSequence buildLatinSequence() { + final StringBuilder result = new StringBuilder(); + while (result.length() < mSequenceLength) { + // add random word + result.append(buildLatinWord()); + result.append(' '); + } + return result.substring(0, mSequenceLength); + } + + public CharSequence buildLatinWord() { + final StringBuilder result = new StringBuilder(); + // create a random length that is (mMinWordLength + random amount of chars) where + // total size is less than mMaxWordLength + final int length = mRandom.nextInt(mMaxWordLength - mMinWordLength) + mMinWordLength; + while (result.length() < length) { + // add random letter + int base = mRandom.nextInt(2) == 0 ? 'A' : 'a'; + result.append(Character.toChars(mRandom.nextInt(26) + base)); + } + return result.toString(); + } + } + +} diff --git a/android/text/PaintMeasureDrawPerfTest.java b/android/text/PaintMeasureDrawPerfTest.java new file mode 100644 index 00000000..00b60add --- /dev/null +++ b/android/text/PaintMeasureDrawPerfTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.text; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.support.test.filters.LargeTest; +import android.view.DisplayListCanvas; +import android.view.RenderNode; + +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.Collection; +import java.util.List; +import java.util.Random; + +/** + * Performance test for single line measure and draw using {@link Paint} and {@link Canvas}. + */ +@LargeTest +@RunWith(Parameterized.class) +public class PaintMeasureDrawPerfTest { + + private static final boolean[] BOOLEANS = new boolean[]{false, true}; + + @Parameterized.Parameters(name = "cached={1},{0} chars") + public static Collection cases() { + final List<Object[]> params = new ArrayList<>(); + for (int length : new int[]{128}) { + for (boolean cached : BOOLEANS) { + params.add(new Object[]{length, cached}); + } + } + return params; + } + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private final int mLength; + private final boolean mCached; + private final TextPaint mTextPaint; + + + public PaintMeasureDrawPerfTest(int length, boolean cached) { + mLength = length; + mCached = cached; + mTextPaint = new TextPaint(); + mTextPaint.setTextSize(10); + } + + /** + * Measure the time for {@link Paint#measureText(String)} + */ + @Test + public void timeMeasure() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final String text = createRandomText(); + if (mCached) mTextPaint.measureText(text); + state.resumeTiming(); + + while (state.keepRunning()) { + state.pauseTiming(); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + mTextPaint.measureText(text); + } + } + + /** + * Measures the time for {@link Canvas#drawText(String, float, float, Paint)} + */ + @Test + public void timeDraw() throws Throwable { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final RenderNode node = RenderNode.create("benchmark", null); + final String text = createRandomText(); + if (mCached) mTextPaint.measureText(text); + state.resumeTiming(); + + while (state.keepRunning()) { + + state.pauseTiming(); + final DisplayListCanvas canvas = node.start(1200, 200); + final int save = canvas.save(); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + canvas.drawText(text, 0 /*x*/, 100 /*y*/, mTextPaint); + + state.pauseTiming(); + canvas.restoreToCount(save); + node.end(canvas); + state.resumeTiming(); + } + } + + private String createRandomText() { + return (String) new NonEditableTextGenerator(new Random(0)) + .setSequenceLength(mLength) + .setCreateBoring(true) + .setTextType(NonEditableTextGenerator.TextType.STRING) + .build(); + } +} diff --git a/android/text/StaticLayout.java b/android/text/StaticLayout.java index c124c7fd..961cd8ee 100644 --- a/android/text/StaticLayout.java +++ b/android/text/StaticLayout.java @@ -28,12 +28,12 @@ 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.nio.ByteBuffer; import java.util.Arrays; import java.util.Locale; @@ -101,6 +101,7 @@ 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; @@ -117,6 +118,9 @@ 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); sPool.release(b); } @@ -128,6 +132,8 @@ public class StaticLayout extends Layout { mPaint = null; mLeftIndents = null; mRightIndents = null; + mLeftPaddings = null; + mRightPaddings = null; mMeasuredText.finish(); } @@ -356,6 +362,28 @@ public class StaticLayout extends Layout { } /** + * Set available paddings to draw overhanging text on. Arguments are arrays holding the + * amount of padding available, one per line, measured in pixels. For lines past the last + * element in the array, the last element repeats. + * + * The individual padding amounts should be non-negative. The result of passing negative + * paddings is undefined. + * + * @param leftPaddings array of amounts of available padding for left margin, in pixels + * @param rightPaddings array of amounts of available padding for right margin, in pixels + * @return this builder, useful for chaining + * + * @hide + */ + @NonNull + public Builder setAvailablePaddings(@Nullable int[] leftPaddings, + @Nullable int[] rightPaddings) { + mLeftPaddings = leftPaddings; + mRightPaddings = rightPaddings; + return this; + } + + /** * Set paragraph justification mode. The default value is * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification, * the last line will be displayed with the alignment set by {@link #setAlignment}. @@ -401,10 +429,8 @@ public class StaticLayout extends Layout { * future). * * Then, for each run within the paragraph: - * - setLocales (this must be done at least for the first run, optional afterwards) * - one of the following, depending on the type of run: * + addStyleRun (a text run, to be measured in native code) - * + addMeasuredRun (a run already measured in Java, passed into native code) * + addReplacementRun (a replacement run, width is given) * * After measurement, nGetWidths() is valid if the widths are needed (eg for ellipsis). @@ -413,24 +439,29 @@ public class StaticLayout extends Layout { * After all paragraphs, call finish() to release expensive buffers. */ - private void setLocales(LocaleList locales) { + private Pair<String, long[]> getLocaleAndHyphenatorIfChanged(TextPaint paint) { + final LocaleList locales = paint.getTextLocales(); + final String languageTags; + long[] hyphenators; if (!locales.equals(mLocales)) { - nSetLocales(mNativePtr, locales.toLanguageTags(), getHyphenators(locales)); 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 */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) { - setLocales(paint.getTextLocales()); - return nAddStyleRun(mNativePtr, paint.getNativeInstance(), start, end, isRtl); - } - - /* package */ void addMeasuredRun(int start, int end, float[] widths) { - nAddMeasuredRun(mNativePtr, start, end, widths); + /* 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); } - /* package */ void addReplacementRun(int start, int end, float width) { - nAddReplacementRun(mNativePtr, start, end, width); + /* 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); } /** @@ -478,6 +509,8 @@ public class StaticLayout extends Layout { private int mHyphenationFrequency; @Nullable private int[] mLeftIndents; @Nullable private int[] mRightIndents; + @Nullable private int[] mLeftPaddings; + @Nullable private int[] mRightPaddings; private int mJustificationMode; private boolean mAddLastLineLineSpacing; @@ -616,7 +649,7 @@ public class StaticLayout extends Layout { : (b.mText instanceof Spanned) ? new SpannedEllipsizer(b.mText) : new Ellipsizer(b.mText), - b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd); + b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd); if (b.mEllipsize != null) { Ellipsizer e = (Ellipsizer) getText(); @@ -638,6 +671,8 @@ public class StaticLayout extends Layout { mLeftIndents = b.mLeftIndents; mRightIndents = b.mRightIndents; + mLeftPaddings = b.mLeftPaddings; + mRightPaddings = b.mRightPaddings; setJustificationMode(b.mJustificationMode); generate(b, b.mIncludePad, b.mIncludePad); @@ -662,7 +697,6 @@ public class StaticLayout extends Layout { // store fontMetrics per span range // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range) int[] fmCache = new int[4 * 4]; - b.setLocales(paint.getTextLocales()); mLineCount = 0; mEllipsized = false; @@ -776,11 +810,17 @@ 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, // TODO: Support more justification mode, e.g. letter spacing, stretching. - b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE, indents, mLineCount); + b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE, + // TODO: indents and paddings don't need to get passed to native code for every + // paragraph. Pass them to native code just once. + indents, mLeftPaddings, mRightPaddings, mLineCount); // measurement has to be done before performing line breaking // but we don't want to recompute fontmetrics or span ranges the @@ -1491,28 +1531,25 @@ public class StaticLayout extends Layout { private static native void nFreeBuilder(long nativePtr); private static native void nFinishBuilder(long nativePtr); - /* package */ static native long nLoadHyphenator(ByteBuffer buf, int offset, - int minPrefix, int minSuffix); - - private static native void nSetLocales(long nativePtr, String locales, - long[] nativeHyphenators); - // Set up paragraph text and settings; done as one big method to minimize jni crossings private static native void nSetupParagraph( - @NonNull long nativePtr, @NonNull char[] text, @IntRange(from = 0) int length, + /* non zero */ long nativePtr, @NonNull char[] text, @IntRange(from = 0) int length, @FloatRange(from = 0.0f) float firstWidth, @IntRange(from = 0) int firstWidthLineCount, @FloatRange(from = 0.0f) float restWidth, @Nullable int[] variableTabStops, int defaultTabStop, @BreakStrategy int breakStrategy, @HyphenationFrequency int hyphenationFrequency, boolean isJustified, - @Nullable int[] indents, @IntRange(from = 0) int indentsOffset); - - private static native float nAddStyleRun(long nativePtr, long nativePaint, int start, int end, - boolean isRtl); + @Nullable int[] indents, @Nullable int[] leftPaddings, @Nullable int[] rightPaddings, + @IntRange(from = 0) int indentsOffset); - private static native void nAddMeasuredRun(long nativePtr, - int start, int end, float[] widths); + 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); - private static native void nAddReplacementRun(long nativePtr, int start, int end, float width); + private static native void nAddReplacementRun(/* non-zero */ long nativePtr, + @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); @@ -1590,4 +1627,6 @@ public class StaticLayout extends Layout { @Nullable private int[] mLeftIndents; @Nullable private int[] mRightIndents; + @Nullable private int[] mLeftPaddings; + @Nullable private int[] mRightPaddings; } diff --git a/android/text/StaticLayoutCreateDrawPerfTest.java b/android/text/StaticLayoutCreateDrawPerfTest.java new file mode 100644 index 00000000..356e2e0d --- /dev/null +++ b/android/text/StaticLayoutCreateDrawPerfTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.text; + +import static android.text.Layout.Alignment.ALIGN_NORMAL; + +import android.graphics.Canvas; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.support.test.filters.LargeTest; +import android.text.NonEditableTextGenerator.TextType; +import android.view.DisplayListCanvas; +import android.view.RenderNode; + +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.Collection; +import java.util.List; +import java.util.Random; + +/** + * Performance test for multi line, single style {@link StaticLayout} creation/draw. + */ +@LargeTest +@RunWith(Parameterized.class) +public class StaticLayoutCreateDrawPerfTest { + + private static final boolean[] BOOLEANS = new boolean[]{false, true}; + + private static final float SPACING_ADD = 10f; + private static final float SPACING_MULT = 1.5f; + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Parameterized.Parameters(name = "cached={3},{1} chars,{0}") + public static Collection cases() { + final List<Object[]> params = new ArrayList<>(); + for (int length : new int[]{128}) { + for (boolean cached : BOOLEANS) { + for (TextType textType : new TextType[]{TextType.STRING, + TextType.SPANNABLE_BUILDER}) { + params.add(new Object[]{textType.name(), length, textType, cached}); + } + } + } + return params; + } + + private final int mLineWidth; + private final int mLength; + private final TextType mTextType; + private final boolean mCached; + private final TextPaint mTextPaint; + + public StaticLayoutCreateDrawPerfTest(String label, int length, TextType textType, + boolean cached) { + mLength = length; + mTextType = textType; + mCached = cached; + mTextPaint = new TextPaint(); + mTextPaint.setTextSize(10); + mLineWidth = Integer.MAX_VALUE; + } + + /** + * Measures the creation time for a multi line {@link StaticLayout}. + */ + @Test + public void timeCreate() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final CharSequence text = createRandomText(mLength); + createLayout(text); + state.resumeTiming(); + + while (state.keepRunning()) { + state.pauseTiming(); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + createLayout(text); + } + } + + /** + * Measures the draw time for a multi line {@link StaticLayout}. + */ + @Test + public void timeDraw() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final RenderNode node = RenderNode.create("benchmark", null); + final CharSequence text = createRandomText(mLength); + final Layout layout = createLayout(text); + state.resumeTiming(); + + while (state.keepRunning()) { + + state.pauseTiming(); + final DisplayListCanvas canvas = node.start(1200, 200); + int save = canvas.save(); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + layout.draw(canvas); + + state.pauseTiming(); + canvas.restoreToCount(save); + node.end(canvas); + state.resumeTiming(); + } + } + + private Layout createLayout(CharSequence text) { + return StaticLayout.Builder.obtain(text, 0 /*start*/, text.length() /*end*/, mTextPaint, + mLineWidth) + .setAlignment(ALIGN_NORMAL) + .setIncludePad(true) + .setLineSpacing(SPACING_ADD, SPACING_MULT) + .build(); + } + + private CharSequence createRandomText(int length) { + return new NonEditableTextGenerator(new Random(0)) + .setSequenceLength(length) + .setTextType(mTextType) + .build(); + } +} diff --git a/android/text/StaticLayout_Delegate.java b/android/text/StaticLayout_Delegate.java index 0d58bcc2..63337f08 100644 --- a/android/text/StaticLayout_Delegate.java +++ b/android/text/StaticLayout_Delegate.java @@ -53,26 +53,11 @@ public class StaticLayout_Delegate { } @LayoutlibDelegate - /*package*/ static long nLoadHyphenator(ByteBuffer buf, int offset, int minPrefix, - int minSuffix) { - return Hyphenator_Delegate.loadHyphenator(buf, offset, minPrefix, minSuffix); - } - - @LayoutlibDelegate - /*package*/ static void nSetLocales(long nativeBuilder, String locales, - long[] nativeHyphenators) { - Builder builder = sBuilderManager.getDelegate(nativeBuilder); - if (builder != null) { - builder.mLocales = locales; - builder.mNativeHyphenators = nativeHyphenators; - } - } - - @LayoutlibDelegate /*package*/ static void nSetupParagraph(long nativeBuilder, char[] text, int length, float firstWidth, int firstWidthLineCount, float restWidth, int[] variableTabStops, int defaultTabStop, int breakStrategy, - int hyphenationFrequency, boolean isJustified, int[] indents, int intentsOffset) { + int hyphenationFrequency, boolean isJustified, int[] indents, int[] leftPaddings, + int[] rightPaddings, int intentsOffset) { // TODO: implement justified alignment Builder builder = sBuilderManager.getDelegate(nativeBuilder); if (builder == null) { @@ -86,30 +71,29 @@ public class StaticLayout_Delegate { } @LayoutlibDelegate - /*package*/ static float nAddStyleRun(long nativeBuilder, long nativePaint, int start, - int end, boolean isRtl) { + /*package*/ static void nAddStyleRun(long nativeBuilder, long nativePaint, int start, + int end, boolean isRtl, String languageTags, long[] hyphenators) { 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; - return builder == null ? 0 : - measureText(nativePaint, builder.mText, start, end - start, builder.mWidths, - bidiFlags); - } - - @LayoutlibDelegate - /*package*/ static void nAddMeasuredRun(long nativeBuilder, int start, int end, float[] widths) { - Builder builder = sBuilderManager.getDelegate(nativeBuilder); - if (builder != null) { - System.arraycopy(widths, start, builder.mWidths, start, end - start); - } + measureText(nativePaint, builder.mText, start, end - start, builder.mWidths, + bidiFlags); } @LayoutlibDelegate - /*package*/ static void nAddReplacementRun(long nativeBuilder, int start, int end, float width) { + /*package*/ static void nAddReplacementRun(long nativeBuilder, int start, int end, float width, + String languageTags, long[] hyphenators) { 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); } diff --git a/android/text/TextViewSetTextMeasurePerfTest.java b/android/text/TextViewSetTextMeasurePerfTest.java new file mode 100644 index 00000000..a2bf33e1 --- /dev/null +++ b/android/text/TextViewSetTextMeasurePerfTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.text; + +import static android.view.View.MeasureSpec.AT_MOST; +import static android.view.View.MeasureSpec.UNSPECIFIED; + +import android.graphics.Canvas; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.text.NonEditableTextGenerator.TextType; +import android.view.DisplayListCanvas; +import android.view.RenderNode; +import android.widget.TextView; + +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.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * Performance test for multi line, single style {@link StaticLayout} creation/draw. + */ +@LargeTest +@RunWith(Parameterized.class) +public class TextViewSetTextMeasurePerfTest { + + private static final boolean[] BOOLEANS = new boolean[]{false, true}; + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Parameterized.Parameters(name = "cached={3},{1} chars,{0}") + public static Collection cases() { + final List<Object[]> params = new ArrayList<>(); + for (int length : new int[]{128}) { + for (boolean cached : BOOLEANS) { + for (TextType textType : new TextType[]{TextType.STRING, + TextType.SPANNABLE_BUILDER}) { + params.add(new Object[]{textType.name(), length, textType, cached}); + } + } + } + return params; + } + + private final int mLineWidth; + private final int mLength; + private final TextType mTextType; + private final boolean mCached; + private final TextPaint mTextPaint; + + public TextViewSetTextMeasurePerfTest(String label, int length, TextType textType, + boolean cached) { + mLength = length; + mTextType = textType; + mCached = cached; + mTextPaint = new TextPaint(); + mTextPaint.setTextSize(10); + mLineWidth = Integer.MAX_VALUE; + } + + /** + * Measures the time to setText and measure for a {@link TextView}. + */ + @Test + public void timeCreate() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final CharSequence text = createRandomText(mLength); + final TextView textView = new TextView(InstrumentationRegistry.getTargetContext()); + textView.setText(text); + state.resumeTiming(); + + while (state.keepRunning()) { + state.pauseTiming(); + textView.setTextLocale(Locale.UK); + textView.setTextLocale(Locale.US); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.setText(text); + textView.measure(AT_MOST | mLineWidth, UNSPECIFIED); + } + } + + /** + * Measures the time to draw for a {@link TextView}. + */ + @Test + public void timeDraw() throws Exception { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + state.pauseTiming(); + Canvas.freeTextLayoutCaches(); + final RenderNode node = RenderNode.create("benchmark", null); + final CharSequence text = createRandomText(mLength); + final TextView textView = new TextView(InstrumentationRegistry.getTargetContext()); + textView.setText(text); + state.resumeTiming(); + + while (state.keepRunning()) { + + state.pauseTiming(); + final DisplayListCanvas canvas = node.start(1200, 200); + int save = canvas.save(); + textView.setTextLocale(Locale.UK); + textView.setTextLocale(Locale.US); + if (!mCached) Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.draw(canvas); + + state.pauseTiming(); + canvas.restoreToCount(save); + node.end(canvas); + state.resumeTiming(); + } + } + + private CharSequence createRandomText(int length) { + return new NonEditableTextGenerator(new Random(0)) + .setSequenceLength(length) + .setCreateBoring(false) + .setTextType(mTextType) + .build(); + } +} diff --git a/android/text/format/Formatter.java b/android/text/format/Formatter.java index fc564552..2c83fc4d 100644 --- a/android/text/format/Formatter.java +++ b/android/text/format/Formatter.java @@ -32,6 +32,7 @@ import android.text.BidiFormatter; import android.text.TextUtils; import android.view.View; +import java.lang.reflect.Constructor; import java.math.BigDecimal; import java.util.Locale; @@ -194,13 +195,29 @@ public final class Formatter { /** * ICU doesn't support PETABYTE yet. Fake it so that we can treat all units the same way. - * {@hide} */ - public static final MeasureUnit PETABYTE = MeasureUnit.internalGetInstance( - "digital", "petabyte"); + private static final MeasureUnit PETABYTE = createPetaByte(); - /** {@hide} */ - public static class RoundedBytesResult { + /** + * Create a petabyte MeasureUnit without registering it with ICU. + * ICU doesn't support user-create MeasureUnit and the only public (but hidden) method to do so + * is {@link MeasureUnit#internalGetInstance(String, String)} which also registers the unit as + * an available type and thus leaks it to code that doesn't expect or support it. + * <p>This method uses reflection to create an instance of MeasureUnit to avoid leaking it. This + * instance is <b>only</b> to be used in this class. + */ + private static MeasureUnit createPetaByte() { + try { + Constructor<MeasureUnit> constructor = MeasureUnit.class + .getDeclaredConstructor(String.class, String.class); + constructor.setAccessible(true); + return constructor.newInstance("digital", "petabyte"); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to create petabyte MeasureUnit", e); + } + } + + private static class RoundedBytesResult { public final float value; public final MeasureUnit units; public final int fractionDigits; @@ -218,7 +235,7 @@ public final class Formatter { * Returns a RoundedBytesResult object based on the input size in bytes and the rounding * flags. The result can be used for formatting. */ - public static RoundedBytesResult roundBytes(long sizeBytes, int flags) { + static RoundedBytesResult roundBytes(long sizeBytes, int flags) { final boolean isNegative = (sizeBytes < 0); float result = isNegative ? -sizeBytes : sizeBytes; MeasureUnit units = MeasureUnit.BYTE; diff --git a/android/transition/TransitionUtils.java b/android/transition/TransitionUtils.java index 4951237e..084b79d5 100644 --- a/android/transition/TransitionUtils.java +++ b/android/transition/TransitionUtils.java @@ -101,7 +101,7 @@ public class TransitionUtils { ImageView copy = new ImageView(view.getContext()); copy.setScaleType(ImageView.ScaleType.CENTER_CROP); - Bitmap bitmap = createViewBitmap(view, matrix, bounds); + Bitmap bitmap = createViewBitmap(view, matrix, bounds, sceneRoot); if (bitmap != null) { copy.setImageBitmap(bitmap); } @@ -115,7 +115,7 @@ public class TransitionUtils { /** * Get a copy of bitmap of given drawable, return null if intrinsic size is zero */ - public static Bitmap createDrawableBitmap(Drawable drawable) { + public static Bitmap createDrawableBitmap(Drawable drawable, View hostView) { int width = drawable.getIntrinsicWidth(); int height = drawable.getIntrinsicHeight(); if (width <= 0 || height <= 0) { @@ -128,7 +128,7 @@ public class TransitionUtils { } int bitmapWidth = (int) (width * scale); int bitmapHeight = (int) (height * scale); - final RenderNode node = RenderNode.create("TransitionUtils", null); + final RenderNode node = RenderNode.create("TransitionUtils", hostView); node.setLeftTopRightBottom(0, 0, width, height); node.setClipToBounds(false); final DisplayListCanvas canvas = node.start(width, height); @@ -156,20 +156,30 @@ public class TransitionUtils { * returning. * @param bounds The bounds of the bitmap in the destination coordinate system (where the * view should be presented. Typically, this is matrix.mapRect(viewBounds); + * @param sceneRoot A ViewGroup that is attached to the window to temporarily contain the view + * if it isn't attached to the window. * @return A bitmap of the given view or null if bounds has no width or height. */ - public static Bitmap createViewBitmap(View view, Matrix matrix, RectF bounds) { + public static Bitmap createViewBitmap(View view, Matrix matrix, RectF bounds, + ViewGroup sceneRoot) { + final boolean addToOverlay = !view.isAttachedToWindow(); + if (addToOverlay) { + if (sceneRoot == null || !sceneRoot.isAttachedToWindow()) { + return null; + } + sceneRoot.getOverlay().add(view); + } Bitmap bitmap = null; int bitmapWidth = Math.round(bounds.width()); int bitmapHeight = Math.round(bounds.height()); if (bitmapWidth > 0 && bitmapHeight > 0) { - float scale = Math.min(1f, ((float)MAX_IMAGE_SIZE) / (bitmapWidth * bitmapHeight)); + float scale = Math.min(1f, ((float) MAX_IMAGE_SIZE) / (bitmapWidth * bitmapHeight)); bitmapWidth *= scale; bitmapHeight *= scale; matrix.postTranslate(-bounds.left, -bounds.top); matrix.postScale(scale, scale); - final RenderNode node = RenderNode.create("TransitionUtils", null); + final RenderNode node = RenderNode.create("TransitionUtils", view); node.setLeftTopRightBottom(0, 0, bitmapWidth, bitmapHeight); node.setClipToBounds(false); final DisplayListCanvas canvas = node.start(bitmapWidth, bitmapHeight); @@ -178,6 +188,9 @@ public class TransitionUtils { node.end(canvas); bitmap = ThreadedRenderer.createHardwareBitmap(node, bitmapWidth, bitmapHeight); } + if (addToOverlay) { + sceneRoot.getOverlay().remove(view); + } return bitmap; } diff --git a/android/util/FeatureFlagUtils.java b/android/util/FeatureFlagUtils.java index 5838f959..fc1d4873 100644 --- a/android/util/FeatureFlagUtils.java +++ b/android/util/FeatureFlagUtils.java @@ -50,6 +50,13 @@ public class FeatureFlagUtils { } /** + * Override feature flag to new state. + */ + public static void setEnabled(String feature, boolean enabled) { + SystemProperties.set(FFLAG_OVERRIDE_PREFIX + feature, enabled ? "true" : "false"); + } + + /** * Returns all feature flags in their raw form. */ public static Map<String, String> getAllFeatureFlags() { diff --git a/android/util/Log.java b/android/util/Log.java index 8691136d..02998653 100644 --- a/android/util/Log.java +++ b/android/util/Log.java @@ -392,7 +392,7 @@ public final class Log { // 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 = NoPreloadHolder.LOGGER_ENTRY_MAX_PAYLOAD // Base. + int bufferSize = PreloadHolder.LOGGER_ENTRY_MAX_PAYLOAD // Base. - 2 // Two terminators. - (tag != null ? tag.length() : 0) // Tag length. - 32; // Some slack. @@ -429,10 +429,10 @@ public final class Log { } /** - * NoPreloadHelper class. Caches the LOGGER_ENTRY_MAX_PAYLOAD value to avoid + * PreloadHelper class. Caches the LOGGER_ENTRY_MAX_PAYLOAD value to avoid * a JNI call during logging. */ - static class NoPreloadHolder { + static class PreloadHolder { public final static int LOGGER_ENTRY_MAX_PAYLOAD = logger_entry_max_payload_native(); } diff --git a/android/util/LruCache.java b/android/util/LruCache.java index 52086065..40154880 100644 --- a/android/util/LruCache.java +++ b/android/util/LruCache.java @@ -20,10 +20,6 @@ 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 @@ -91,9 +87,8 @@ public class LruCache<K, V> { /** * Sets the size of the cache. - * @param maxSize The new maximum size. * - * @hide + * @param maxSize The new maximum size. */ public void resize(int maxSize) { if (maxSize <= 0) { @@ -190,10 +185,13 @@ 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. */ - private void trimToSize(int maxSize) { + public void trimToSize(int maxSize) { while (true) { K key; V value; @@ -207,16 +205,7 @@ public class LruCache<K, V> { break; } - // 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 - + Map.Entry<K, V> toEvict = map.eldest(); if (toEvict == null) { break; } diff --git a/android/util/StatsLogKey.java b/android/util/StatsLogKey.java new file mode 100644 index 00000000..9ad0a23d --- /dev/null +++ b/android/util/StatsLogKey.java @@ -0,0 +1,48 @@ +/* + * 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 new file mode 100644 index 00000000..5e5a8287 --- /dev/null +++ b/android/util/StatsLogTag.java @@ -0,0 +1,32 @@ +/* + * 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 new file mode 100644 index 00000000..05b9d933 --- /dev/null +++ b/android/util/StatsLogValue.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// 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/proto/ProtoOutputStream.java b/android/util/proto/ProtoOutputStream.java index 9afa56dd..43a97897 100644 --- a/android/util/proto/ProtoOutputStream.java +++ b/android/util/proto/ProtoOutputStream.java @@ -29,8 +29,8 @@ import java.io.UnsupportedEncodingException; * Class to write to a protobuf stream. * * Each write method takes an ID code from the protoc generated classes - * and the value to write. To make a nested object, call startObject - * and then endObject when you are done. + * and the value to write. To make a nested object, call #start + * and then #end when you are done. * * The ID codes have type information embedded into them, so if you call * the incorrect function you will get an IllegalArgumentException. @@ -60,16 +60,16 @@ import java.io.UnsupportedEncodingException; * Message objects. We need to find another way. * * So what we do here is to let the calling code write the data into a - * byte[] (actually a collection of them wrapped in the EncodedBuffer) class, + * byte[] (actually a collection of them wrapped in the EncodedBuffer class), * but not do the varint encoding of the sub-message sizes. Then, we do a * recursive traversal of the buffer itself, calculating the sizes (which are * then knowable, although still not the actual sizes in the buffer because of * possible further nesting). Then we do a third pass, compacting the * buffer and varint encoding the sizes. * - * This gets us a relatively small number number of fixed-size allocations, + * This gets us a relatively small number of fixed-size allocations, * which is less likely to cause memory fragmentation or churn the GC, and - * the same number of data copies as would have gotten with setting it + * the same number of data copies as we would have gotten with setting it * field-by-field in generated code, and no code bloat from generated code. * The final data copy is also done with System.arraycopy, which will be * more efficient, in general, than doing the individual fields twice (as in @@ -77,26 +77,26 @@ import java.io.UnsupportedEncodingException; * * To accomplish the multiple passes, whenever we write a * WIRE_TYPE_LENGTH_DELIMITED field, we write the size occupied in our - * buffer as a fixed 32 bit int (called childRawSize), not variable length + * buffer as a fixed 32 bit int (called childRawSize), not a variable length * one. We reserve another 32 bit slot for the computed size (called * childEncodedSize). If we know the size up front, as we do for strings * and byte[], then we also put that into childEncodedSize, if we don't, we - * write the negative of childRawSize, as a sentiel that we need to + * write the negative of childRawSize, as a sentinel that we need to * compute it during the second pass and recursively compact it during the * third pass. * - * Unsgigned size varints can be up to five bytes long, but we reserve eight + * Unsigned size varints can be up to five bytes long, but we reserve eight * bytes for overhead, so we know that when we compact the buffer, there * will always be space for the encoded varint. * * When we can figure out the size ahead of time, we do, in order * to save overhead with recalculating it, and with the later arraycopy. * - * During the period between when the caller has called startObject, but - * not yet called endObject, we maintain a linked list of the tokens - * returned by startObject, stored in those 8 bytes of size storage space. + * During the period between when the caller has called #start, but + * not yet called #end, we maintain a linked list of the tokens + * returned by #start, stored in those 8 bytes of size storage space. * We use that linked list of tokens to ensure that the caller has - * correctly matched pairs of startObject and endObject calls, and issue + * correctly matched pairs of #start and #end calls, and issue * errors if they are not matched. */ @TestApi @@ -2375,6 +2375,9 @@ public final class ProtoOutputStream { if (countString == null) { countString = "fieldCount=" + fieldCount; } + if (countString.length() > 0) { + countString += " "; + } final long fieldType = fieldId & FIELD_TYPE_MASK; String typeString = getFieldTypeString(fieldType); @@ -2382,7 +2385,7 @@ public final class ProtoOutputStream { typeString = "fieldType=" + fieldType; } - return fieldCount + " " + typeString + " tag=" + ((int)fieldId) + return countString + typeString + " tag=" + ((int) fieldId) + " fieldId=0x" + Long.toHexString(fieldId); } diff --git a/android/util/proto/ProtoUtils.java b/android/util/proto/ProtoUtils.java new file mode 100644 index 00000000..449baca5 --- /dev/null +++ b/android/util/proto/ProtoUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.proto; + +import android.util.AggStats; + +/** + * This class contains a list of helper functions to write common proto in + * //frameworks/base/core/proto/android/base directory + */ +public class ProtoUtils { + + /** + * Dump AggStats to ProtoOutputStream + * @hide + */ + public static void toAggStatsProto(ProtoOutputStream proto, long fieldId, + long min, long average, long max) { + final long aggStatsToken = proto.start(fieldId); + proto.write(AggStats.MIN, min); + proto.write(AggStats.AVERAGE, average); + proto.write(AggStats.MAX, max); + proto.end(aggStatsToken); + } +} diff --git a/android/view/DragEvent.java b/android/view/DragEvent.java index 16f2d7d0..2c9f8712 100644 --- a/android/view/DragEvent.java +++ b/android/view/DragEvent.java @@ -103,7 +103,7 @@ import com.android.internal.view.IDragAndDropPermissions; * <tr> * <td>ACTION_DRAG_ENDED</td> * <td style="text-align: center;"> </td> - * <td style="text-align: center;"> </td> + * <td style="text-align: center;">X</td> * <td style="text-align: center;"> </td> * <td style="text-align: center;"> </td> * <td style="text-align: center;"> </td> @@ -112,6 +112,7 @@ import com.android.internal.view.IDragAndDropPermissions; * </table> * <p> * The {@link android.view.DragEvent#getAction()}, + * {@link android.view.DragEvent#getLocalState()} * {@link android.view.DragEvent#describeContents()}, * {@link android.view.DragEvent#writeToParcel(Parcel,int)}, and * {@link android.view.DragEvent#toString()} methods always return valid data. @@ -397,7 +398,7 @@ public class DragEvent implements Parcelable { * operation. In all other activities this method will return null * </p> * <p> - * This method returns valid data for all event actions except for {@link #ACTION_DRAG_ENDED}. + * This method returns valid data for all event actions. * </p> * @return The local state object sent to the system by startDragAndDrop(). */ diff --git a/android/view/Gravity.java b/android/view/Gravity.java index 324a1ae3..232ff255 100644 --- a/android/view/Gravity.java +++ b/android/view/Gravity.java @@ -440,4 +440,57 @@ public class Gravity } return result; } + + /** + * @hide + */ + public static String toString(int gravity) { + final StringBuilder result = new StringBuilder(); + if ((gravity & FILL) != 0) { + result.append("FILL").append(' '); + } else { + if ((gravity & FILL_VERTICAL) != 0) { + result.append("FILL_VERTICAL").append(' '); + } else { + if ((gravity & TOP) != 0) { + result.append("TOP").append(' '); + } + if ((gravity & BOTTOM) != 0) { + result.append("BOTTOM").append(' '); + } + } + if ((gravity & FILL_HORIZONTAL) != 0) { + result.append("FILL_HORIZONTAL").append(' '); + } else { + if ((gravity & START) != 0) { + result.append("START").append(' '); + } else if ((gravity & LEFT) != 0) { + result.append("LEFT").append(' '); + } + if ((gravity & END) != 0) { + result.append("END").append(' '); + } else if ((gravity & RIGHT) != 0) { + result.append("RIGHT").append(' '); + } + } + } + if ((gravity & CENTER) != 0) { + result.append("CENTER").append(' '); + } else { + if ((gravity & CENTER_VERTICAL) != 0) { + result.append("CENTER_VERTICAL").append(' '); + } + if ((gravity & CENTER_HORIZONTAL) != 0) { + result.append("CENTER_HORIZONTAL").append(' '); + } + } + if ((gravity & DISPLAY_CLIP_VERTICAL) != 0) { + result.append("DISPLAY_CLIP_VERTICAL").append(' '); + } + if ((gravity & DISPLAY_CLIP_VERTICAL) != 0) { + result.append("DISPLAY_CLIP_VERTICAL").append(' '); + } + result.deleteCharAt(result.length() - 1); + return result.toString(); + } } diff --git a/android/view/MenuInflater_Delegate.java b/android/view/MenuInflater_Delegate.java index 08a97d64..977a2a72 100644 --- a/android/view/MenuInflater_Delegate.java +++ b/android/view/MenuInflater_Delegate.java @@ -42,7 +42,6 @@ import android.util.AttributeSet; * ViewInfo}, we check the corresponding view key in the menu item for the view and add it */ public class MenuInflater_Delegate { - @LayoutlibDelegate /*package*/ static void registerMenu(MenuInflater thisInflater, MenuItem menuItem, AttributeSet attrs) { @@ -56,10 +55,15 @@ public class MenuInflater_Delegate { return; } } - // This means that Bridge did not take over the instantiation of some object properly. - // This is most likely a bug in the LayoutLib code. - Bridge.getLog().warning(LayoutLog.TAG_BROKEN, - "Action Bar Menu rendering may be incorrect.", null); + + if (menuItem == null || !menuItem.getClass().getName().startsWith("android.support.")) { + // This means that Bridge did not take over the instantiation of some object properly. + // This is most likely a bug in the LayoutLib code. + // We suppress this error for AppCompat menus since we do not support them in the menu + // editor yet. + Bridge.getLog().warning(LayoutLog.TAG_BROKEN, + "Action Bar Menu rendering may be incorrect.", null); + } } diff --git a/android/view/Surface.java b/android/view/Surface.java index 2c1f7346..ddced6cd 100644 --- a/android/view/Surface.java +++ b/android/view/Surface.java @@ -762,7 +762,7 @@ public class Surface implements Parcelable { return "ROTATION_270"; } default: { - throw new IllegalArgumentException("Invalid rotation: " + rotation); + return Integer.toString(rotation); } } } diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java index 91932dd4..31daefff 100644 --- a/android/view/SurfaceControl.java +++ b/android/view/SurfaceControl.java @@ -18,6 +18,7 @@ package android.view; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; +import android.annotation.Size; import android.graphics.Bitmap; import android.graphics.GraphicBuffer; import android.graphics.Rect; @@ -65,6 +66,7 @@ public class SurfaceControl { 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, float dtdy, float dsdy); private static native void nativeSetFlags(long nativeObject, int flags, int mask); @@ -105,8 +107,8 @@ public class SurfaceControl { long surfaceObject, long frame); private static native void nativeReparentChildren(long nativeObject, IBinder handle); - private static native void nativeReparentChild(long nativeObject, - IBinder parentHandle, IBinder childHandle); + private static native void nativeReparent(long nativeObject, + IBinder parentHandle); private static native void nativeSeverChildren(long nativeObject); private static native void nativeSetOverrideScalingMode(long nativeObject, int scalingMode); @@ -455,9 +457,9 @@ public class SurfaceControl { nativeReparentChildren(mNativeObject, newParentHandle); } - /** Re-parents a specific child layer to a new parent */ - public void reparentChild(IBinder newParentHandle, IBinder childHandle) { - nativeReparentChild(mNativeObject, newParentHandle, childHandle); + /** Re-parents this layer to a new parent. */ + public void reparent(IBinder newParentHandle) { + nativeReparent(mNativeObject, newParentHandle); } public void detachChildren() { @@ -552,6 +554,15 @@ public class SurfaceControl { nativeSetAlpha(mNativeObject, 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); + } + public void setMatrix(float dsdx, float dtdx, float dtdy, float dsdy) { checkNotReleased(); nativeSetMatrix(mNativeObject, dsdx, dtdx, dtdy, dsdy); diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java index ebb2af45..462dad3f 100644 --- a/android/view/SurfaceView.java +++ b/android/view/SurfaceView.java @@ -16,115 +16,1208 @@ package android.view; -import com.android.layoutlib.bridge.MockView; +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 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; /** - * 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. + * 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. * - * TODO: generate automatically. + * <p>Access to the underlying surface is provided via the SurfaceHolder interface, + * which can be retrieved by calling {@link #getHolder}. * + * <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 MockView { +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 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 defStyle) { - super(context, attrs, defStyle); + public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } 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) { - return false; + 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; } + @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; + } } - public SurfaceHolder getHolder() { - return mSurfaceHolder; + private void updateOpaqueFlag() { + if (!PixelFormat.formatHasAlpha(mRequestedFormat)) { + mSurfaceFlags |= SurfaceControl.OPAQUE; + } else { + mSurfaceFlags &= ~SurfaceControl.OPAQUE; + } } - private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + 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; + } + + private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + private static final String LOG_TAG = "SurfaceHolder"; @Override public boolean isCreating() { - return false; + return mIsCreating; } @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 - public void setType(int type) { - } + @Deprecated + 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 null; + 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 dirty) { + public Canvas lockCanvas(Rect inOutDirty) { + return internalLockCanvas(inOutDirty, false); + } + + @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(); + 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 null; + return mSurface; } @Override public Rect getSurfaceFrame() { - return null; + return mSurfaceFrame; } }; -} + 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 e5bd5ac0..b6be2961 100644 --- a/android/view/View.java +++ b/android/view/View.java @@ -127,6 +127,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -1078,6 +1079,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the * value should be <code>{@value #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE}</code>). * + * <p>When annotating a view with this hint, it's recommended to use a date autofill value to + * avoid ambiguity when the autofill service provides a value for it. To understand why a + * value can be ambiguous, consider "April of 2020", which could be represented as either of + * the following options: + * + * <ul> + * <li>{@code "04/2020"} + * <li>{@code "4/2020"} + * <li>{@code "2020/04"} + * <li>{@code "2020/4"} + * <li>{@code "April/2020"} + * <li>{@code "Apr/2020"} + * </ul> + * + * <p>You define a date autofill value for the view by overriding the following methods: + * + * <ol> + * <li>{@link #getAutofillType()} to return {@link #AUTOFILL_TYPE_DATE}. + * <li>{@link #getAutofillValue()} to return a + * {@link AutofillValue#forDate(long) date autofillvalue}. + * <li>{@link #autofill(AutofillValue)} to expect a data autofillvalue. + * </ol> + * * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints. */ public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE = @@ -1090,6 +1114,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * <a href="#attr_android:autofillHint"> {@code android:autofillHint}</a> (in which case the * value should be <code>{@value #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH}</code>). * + * <p>When annotating a view with this hint, it's recommended to use a text autofill value + * whose value is the numerical representation of the month, starting on {@code 1} to avoid + * ambiguity when the autofill service provides a value for it. To understand why a + * value can be ambiguous, consider "January", which could be represented as either of + * + * <ul> + * <li>{@code "1"}: recommended way. + * <li>{@code "0"}: if following the {@link Calendar#MONTH} convention. + * <li>{@code "January"}: full name, in English. + * <li>{@code "jan"}: abbreviated name, in English. + * <li>{@code "Janeiro"}: full name, in another language. + * </ul> + * + * <p>Another recommended approach is to use a date autofill value - see + * {@link #AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE} for more details. + * * <p>See {@link #setAutofillHints(String...)} for more info about autofill hints. */ public static final String AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH = @@ -3702,15 +3742,90 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ @ViewDebug.ExportedProperty(flagMapping = { - @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LOW_PROFILE, - equals = SYSTEM_UI_FLAG_LOW_PROFILE, - name = "SYSTEM_UI_FLAG_LOW_PROFILE", outputIf = true), - @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_HIDE_NAVIGATION, - equals = SYSTEM_UI_FLAG_HIDE_NAVIGATION, - name = "SYSTEM_UI_FLAG_HIDE_NAVIGATION", outputIf = true), - @ViewDebug.FlagToString(mask = PUBLIC_STATUS_BAR_VISIBILITY_MASK, - equals = SYSTEM_UI_FLAG_VISIBLE, - name = "SYSTEM_UI_FLAG_VISIBLE", outputIf = true) + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LOW_PROFILE, + equals = SYSTEM_UI_FLAG_LOW_PROFILE, + name = "LOW_PROFILE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_HIDE_NAVIGATION, + equals = SYSTEM_UI_FLAG_HIDE_NAVIGATION, + name = "HIDE_NAVIGATION"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_FULLSCREEN, + equals = SYSTEM_UI_FLAG_FULLSCREEN, + name = "FULLSCREEN"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_STABLE, + equals = SYSTEM_UI_FLAG_LAYOUT_STABLE, + name = "LAYOUT_STABLE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION, + equals = SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION, + name = "LAYOUT_HIDE_NAVIGATION"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, + equals = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, + name = "LAYOUT_FULLSCREEN"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_IMMERSIVE, + equals = SYSTEM_UI_FLAG_IMMERSIVE, + name = "IMMERSIVE"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_IMMERSIVE_STICKY, + equals = SYSTEM_UI_FLAG_IMMERSIVE_STICKY, + name = "IMMERSIVE_STICKY"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LIGHT_STATUS_BAR, + equals = SYSTEM_UI_FLAG_LIGHT_STATUS_BAR, + name = "LIGHT_STATUS_BAR"), + @ViewDebug.FlagToString(mask = SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, + equals = SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR, + name = "LIGHT_NAVIGATION_BAR"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_EXPAND, + equals = STATUS_BAR_DISABLE_EXPAND, + name = "STATUS_BAR_DISABLE_EXPAND"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_ICONS, + equals = STATUS_BAR_DISABLE_NOTIFICATION_ICONS, + name = "STATUS_BAR_DISABLE_NOTIFICATION_ICONS"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_ALERTS, + equals = STATUS_BAR_DISABLE_NOTIFICATION_ALERTS, + name = "STATUS_BAR_DISABLE_NOTIFICATION_ALERTS"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_NOTIFICATION_TICKER, + equals = STATUS_BAR_DISABLE_NOTIFICATION_TICKER, + name = "STATUS_BAR_DISABLE_NOTIFICATION_TICKER"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_SYSTEM_INFO, + equals = STATUS_BAR_DISABLE_SYSTEM_INFO, + name = "STATUS_BAR_DISABLE_SYSTEM_INFO"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_HOME, + equals = STATUS_BAR_DISABLE_HOME, + name = "STATUS_BAR_DISABLE_HOME"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_BACK, + equals = STATUS_BAR_DISABLE_BACK, + name = "STATUS_BAR_DISABLE_BACK"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_CLOCK, + equals = STATUS_BAR_DISABLE_CLOCK, + name = "STATUS_BAR_DISABLE_CLOCK"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_RECENT, + equals = STATUS_BAR_DISABLE_RECENT, + name = "STATUS_BAR_DISABLE_RECENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_DISABLE_SEARCH, + equals = STATUS_BAR_DISABLE_SEARCH, + name = "STATUS_BAR_DISABLE_SEARCH"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSIENT, + equals = STATUS_BAR_TRANSIENT, + name = "STATUS_BAR_TRANSIENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSIENT, + equals = NAVIGATION_BAR_TRANSIENT, + name = "NAVIGATION_BAR_TRANSIENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_UNHIDE, + equals = STATUS_BAR_UNHIDE, + name = "STATUS_BAR_UNHIDE"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_UNHIDE, + equals = NAVIGATION_BAR_UNHIDE, + name = "NAVIGATION_BAR_UNHIDE"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSLUCENT, + equals = STATUS_BAR_TRANSLUCENT, + name = "STATUS_BAR_TRANSLUCENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSLUCENT, + equals = NAVIGATION_BAR_TRANSLUCENT, + name = "NAVIGATION_BAR_TRANSLUCENT"), + @ViewDebug.FlagToString(mask = NAVIGATION_BAR_TRANSPARENT, + equals = NAVIGATION_BAR_TRANSPARENT, + name = "NAVIGATION_BAR_TRANSPARENT"), + @ViewDebug.FlagToString(mask = STATUS_BAR_TRANSPARENT, + equals = STATUS_BAR_TRANSPARENT, + name = "STATUS_BAR_TRANSPARENT") }, formatToHexString = true) int mSystemUiVisibility; @@ -15414,7 +15529,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * {@code dirty}. * * @param dirty the rectangle representing the bounds of the dirty region + * + * @deprecated The switch to hardware accelerated rendering in API 14 reduced + * the importance of the dirty rectangle. In API 21 the given rectangle is + * ignored entirely in favor of an internally-calculated area instead. + * Because of this, clients are encouraged to just call {@link #invalidate()}. */ + @Deprecated public void invalidate(Rect dirty) { final int scrollX = mScrollX; final int scrollY = mScrollY; @@ -15435,7 +15556,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param t the top position of the dirty region * @param r the right position of the dirty region * @param b the bottom position of the dirty region + * + * @deprecated The switch to hardware accelerated rendering in API 14 reduced + * the importance of the dirty rectangle. In API 21 the given rectangle is + * ignored entirely in favor of an internally-calculated area instead. + * Because of this, clients are encouraged to just call {@link #invalidate()}. */ + @Deprecated public void invalidate(int l, int t, int r, int b) { final int scrollX = mScrollX; final int scrollY = mScrollY; diff --git a/android/view/ViewDebug.java b/android/view/ViewDebug.java index 66c05785..3426485e 100644 --- a/android/view/ViewDebug.java +++ b/android/view/ViewDebug.java @@ -1375,6 +1375,81 @@ public class ViewDebug { } } + /** + * Converts an integer from a field that is mapped with {@link IntToString} to its string + * representation. + * + * @param clazz The class the field is defined on. + * @param field The field on which the {@link ExportedProperty} is defined on. + * @param integer The value to convert. + * @return The value converted into its string representation. + * @hide + */ + public static String intToString(Class<?> clazz, String field, int integer) { + final IntToString[] mapping = getMapping(clazz, field); + if (mapping == null) { + return Integer.toString(integer); + } + final int count = mapping.length; + for (int j = 0; j < count; j++) { + final IntToString map = mapping[j]; + if (map.from() == integer) { + return map.to(); + } + } + return Integer.toString(integer); + } + + /** + * Converts a set of flags from a field that is mapped with {@link FlagToString} to its string + * representation. + * + * @param clazz The class the field is defined on. + * @param field The field on which the {@link ExportedProperty} is defined on. + * @param flags The flags to convert. + * @return The flags converted into their string representations. + * @hide + */ + public static String flagsToString(Class<?> clazz, String field, int flags) { + final FlagToString[] mapping = getFlagMapping(clazz, field); + if (mapping == null) { + return Integer.toHexString(flags); + } + final StringBuilder result = new StringBuilder(); + final int count = mapping.length; + for (int j = 0; j < count; j++) { + final FlagToString flagMapping = mapping[j]; + final boolean ifTrue = flagMapping.outputIf(); + final int maskResult = flags & flagMapping.mask(); + final boolean test = maskResult == flagMapping.equals(); + if (test && ifTrue) { + final String name = flagMapping.name(); + result.append(name).append(' '); + } + } + if (result.length() > 0) { + result.deleteCharAt(result.length() - 1); + } + return result.toString(); + } + + private static FlagToString[] getFlagMapping(Class<?> clazz, String field) { + try { + return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class) + .flagMapping(); + } catch (NoSuchFieldException e) { + return null; + } + } + + private static IntToString[] getMapping(Class<?> clazz, String field) { + try { + return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class).mapping(); + } catch (NoSuchFieldException e) { + return null; + } + } + private static void exportUnrolledArray(Context context, BufferedWriter out, ExportedProperty property, int[] array, String prefix, String suffix) throws IOException { diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java index 415aad54..71106ada 100644 --- a/android/view/ViewRootImpl.java +++ b/android/view/ViewRootImpl.java @@ -366,7 +366,7 @@ public final class ViewRootImpl implements ViewParent, // These can be accessed by any thread, must be protected with a lock. // Surface can never be reassigned or cleared (use Surface.clear()). - final Surface mSurface = new Surface(); + public final Surface mSurface = new Surface(); boolean mAdded; boolean mAddedTouchMode; @@ -512,7 +512,7 @@ public final class ViewRootImpl implements ViewParent, mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); if (!sCompatibilityDone) { - sAlwaysAssignFocus = true; + sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P; sCompatibilityDone = true; } @@ -7714,7 +7714,7 @@ public final class ViewRootImpl implements ViewParent, public void onAccessibilityStateChanged(boolean enabled) { if (enabled) { ensureConnection(); - if (mAttachInfo.mHasWindowFocus) { + if (mAttachInfo.mHasWindowFocus && (mView != null)) { mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); View focusedView = mView.findFocus(); if (focusedView != null && focusedView != mView) { diff --git a/android/view/ViewStructure.java b/android/view/ViewStructure.java index 0ecd20da..f671c349 100644 --- a/android/view/ViewStructure.java +++ b/android/view/ViewStructure.java @@ -378,7 +378,7 @@ public abstract class ViewStructure { * * <p>Typically used when the view is a container for an HTML document. * - * @param domain URL representing the domain; only the host part will be used. + * @param domain RFC 2396-compliant URI representing the domain. */ public abstract void setWebDomain(@Nullable String domain); diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java index e56a82ff..c29a1daf 100644 --- a/android/view/WindowManager.java +++ b/android/view/WindowManager.java @@ -16,6 +16,8 @@ package android.view; +import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT; + import android.Manifest.permission; import android.annotation.IntDef; import android.annotation.NonNull; @@ -268,93 +270,93 @@ public interface WindowManager extends ViewManager { */ @ViewDebug.ExportedProperty(mapping = { @ViewDebug.IntToString(from = TYPE_BASE_APPLICATION, - to = "TYPE_BASE_APPLICATION"), + to = "BASE_APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION, - to = "TYPE_APPLICATION"), + to = "APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION_STARTING, - to = "TYPE_APPLICATION_STARTING"), + to = "APPLICATION_STARTING"), @ViewDebug.IntToString(from = TYPE_DRAWN_APPLICATION, - to = "TYPE_DRAWN_APPLICATION"), + to = "DRAWN_APPLICATION"), @ViewDebug.IntToString(from = TYPE_APPLICATION_PANEL, - to = "TYPE_APPLICATION_PANEL"), + to = "APPLICATION_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_MEDIA, - to = "TYPE_APPLICATION_MEDIA"), + to = "APPLICATION_MEDIA"), @ViewDebug.IntToString(from = TYPE_APPLICATION_SUB_PANEL, - to = "TYPE_APPLICATION_SUB_PANEL"), + to = "APPLICATION_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_ABOVE_SUB_PANEL, - to = "TYPE_APPLICATION_ABOVE_SUB_PANEL"), + to = "APPLICATION_ABOVE_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_APPLICATION_ATTACHED_DIALOG, - to = "TYPE_APPLICATION_ATTACHED_DIALOG"), + to = "APPLICATION_ATTACHED_DIALOG"), @ViewDebug.IntToString(from = TYPE_APPLICATION_MEDIA_OVERLAY, - to = "TYPE_APPLICATION_MEDIA_OVERLAY"), + to = "APPLICATION_MEDIA_OVERLAY"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR, - to = "TYPE_STATUS_BAR"), + to = "STATUS_BAR"), @ViewDebug.IntToString(from = TYPE_SEARCH_BAR, - to = "TYPE_SEARCH_BAR"), + to = "SEARCH_BAR"), @ViewDebug.IntToString(from = TYPE_PHONE, - to = "TYPE_PHONE"), + to = "PHONE"), @ViewDebug.IntToString(from = TYPE_SYSTEM_ALERT, - to = "TYPE_SYSTEM_ALERT"), + to = "SYSTEM_ALERT"), @ViewDebug.IntToString(from = TYPE_TOAST, - to = "TYPE_TOAST"), + to = "TOAST"), @ViewDebug.IntToString(from = TYPE_SYSTEM_OVERLAY, - to = "TYPE_SYSTEM_OVERLAY"), + to = "SYSTEM_OVERLAY"), @ViewDebug.IntToString(from = TYPE_PRIORITY_PHONE, - to = "TYPE_PRIORITY_PHONE"), + to = "PRIORITY_PHONE"), @ViewDebug.IntToString(from = TYPE_SYSTEM_DIALOG, - to = "TYPE_SYSTEM_DIALOG"), + to = "SYSTEM_DIALOG"), @ViewDebug.IntToString(from = TYPE_KEYGUARD_DIALOG, - to = "TYPE_KEYGUARD_DIALOG"), + to = "KEYGUARD_DIALOG"), @ViewDebug.IntToString(from = TYPE_SYSTEM_ERROR, - to = "TYPE_SYSTEM_ERROR"), + to = "SYSTEM_ERROR"), @ViewDebug.IntToString(from = TYPE_INPUT_METHOD, - to = "TYPE_INPUT_METHOD"), + to = "INPUT_METHOD"), @ViewDebug.IntToString(from = TYPE_INPUT_METHOD_DIALOG, - to = "TYPE_INPUT_METHOD_DIALOG"), + to = "INPUT_METHOD_DIALOG"), @ViewDebug.IntToString(from = TYPE_WALLPAPER, - to = "TYPE_WALLPAPER"), + to = "WALLPAPER"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR_PANEL, - to = "TYPE_STATUS_BAR_PANEL"), + to = "STATUS_BAR_PANEL"), @ViewDebug.IntToString(from = TYPE_SECURE_SYSTEM_OVERLAY, - to = "TYPE_SECURE_SYSTEM_OVERLAY"), + to = "SECURE_SYSTEM_OVERLAY"), @ViewDebug.IntToString(from = TYPE_DRAG, - to = "TYPE_DRAG"), + to = "DRAG"), @ViewDebug.IntToString(from = TYPE_STATUS_BAR_SUB_PANEL, - to = "TYPE_STATUS_BAR_SUB_PANEL"), + to = "STATUS_BAR_SUB_PANEL"), @ViewDebug.IntToString(from = TYPE_POINTER, - to = "TYPE_POINTER"), + to = "POINTER"), @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR, - to = "TYPE_NAVIGATION_BAR"), + to = "NAVIGATION_BAR"), @ViewDebug.IntToString(from = TYPE_VOLUME_OVERLAY, - to = "TYPE_VOLUME_OVERLAY"), + to = "VOLUME_OVERLAY"), @ViewDebug.IntToString(from = TYPE_BOOT_PROGRESS, - to = "TYPE_BOOT_PROGRESS"), + to = "BOOT_PROGRESS"), @ViewDebug.IntToString(from = TYPE_INPUT_CONSUMER, - to = "TYPE_INPUT_CONSUMER"), + to = "INPUT_CONSUMER"), @ViewDebug.IntToString(from = TYPE_DREAM, - to = "TYPE_DREAM"), + to = "DREAM"), @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR_PANEL, - to = "TYPE_NAVIGATION_BAR_PANEL"), + to = "NAVIGATION_BAR_PANEL"), @ViewDebug.IntToString(from = TYPE_DISPLAY_OVERLAY, - to = "TYPE_DISPLAY_OVERLAY"), + to = "DISPLAY_OVERLAY"), @ViewDebug.IntToString(from = TYPE_MAGNIFICATION_OVERLAY, - to = "TYPE_MAGNIFICATION_OVERLAY"), + to = "MAGNIFICATION_OVERLAY"), @ViewDebug.IntToString(from = TYPE_PRESENTATION, - to = "TYPE_PRESENTATION"), + to = "PRESENTATION"), @ViewDebug.IntToString(from = TYPE_PRIVATE_PRESENTATION, - to = "TYPE_PRIVATE_PRESENTATION"), + to = "PRIVATE_PRESENTATION"), @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION, - to = "TYPE_VOICE_INTERACTION"), + to = "VOICE_INTERACTION"), @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION_STARTING, - to = "TYPE_VOICE_INTERACTION_STARTING"), + to = "VOICE_INTERACTION_STARTING"), @ViewDebug.IntToString(from = TYPE_DOCK_DIVIDER, - to = "TYPE_DOCK_DIVIDER"), + to = "DOCK_DIVIDER"), @ViewDebug.IntToString(from = TYPE_QS_DIALOG, - to = "TYPE_QS_DIALOG"), + to = "QS_DIALOG"), @ViewDebug.IntToString(from = TYPE_SCREENSHOT, - to = "TYPE_SCREENSHOT"), + to = "SCREENSHOT"), @ViewDebug.IntToString(from = TYPE_APPLICATION_OVERLAY, - to = "TYPE_APPLICATION_OVERLAY") + to = "APPLICATION_OVERLAY") }) public int type; @@ -1198,63 +1200,69 @@ public interface WindowManager extends ViewManager { */ @ViewDebug.ExportedProperty(flagMapping = { @ViewDebug.FlagToString(mask = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, equals = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, - name = "FLAG_ALLOW_LOCK_WHILE_SCREEN_ON"), + name = "ALLOW_LOCK_WHILE_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_DIM_BEHIND, equals = FLAG_DIM_BEHIND, - name = "FLAG_DIM_BEHIND"), + name = "DIM_BEHIND"), @ViewDebug.FlagToString(mask = FLAG_BLUR_BEHIND, equals = FLAG_BLUR_BEHIND, - name = "FLAG_BLUR_BEHIND"), + name = "BLUR_BEHIND"), @ViewDebug.FlagToString(mask = FLAG_NOT_FOCUSABLE, equals = FLAG_NOT_FOCUSABLE, - name = "FLAG_NOT_FOCUSABLE"), + name = "NOT_FOCUSABLE"), @ViewDebug.FlagToString(mask = FLAG_NOT_TOUCHABLE, equals = FLAG_NOT_TOUCHABLE, - name = "FLAG_NOT_TOUCHABLE"), + name = "NOT_TOUCHABLE"), @ViewDebug.FlagToString(mask = FLAG_NOT_TOUCH_MODAL, equals = FLAG_NOT_TOUCH_MODAL, - name = "FLAG_NOT_TOUCH_MODAL"), + name = "NOT_TOUCH_MODAL"), @ViewDebug.FlagToString(mask = FLAG_TOUCHABLE_WHEN_WAKING, equals = FLAG_TOUCHABLE_WHEN_WAKING, - name = "FLAG_TOUCHABLE_WHEN_WAKING"), + name = "TOUCHABLE_WHEN_WAKING"), @ViewDebug.FlagToString(mask = FLAG_KEEP_SCREEN_ON, equals = FLAG_KEEP_SCREEN_ON, - name = "FLAG_KEEP_SCREEN_ON"), + name = "KEEP_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_IN_SCREEN, equals = FLAG_LAYOUT_IN_SCREEN, - name = "FLAG_LAYOUT_IN_SCREEN"), + name = "LAYOUT_IN_SCREEN"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_NO_LIMITS, equals = FLAG_LAYOUT_NO_LIMITS, - name = "FLAG_LAYOUT_NO_LIMITS"), + name = "LAYOUT_NO_LIMITS"), @ViewDebug.FlagToString(mask = FLAG_FULLSCREEN, equals = FLAG_FULLSCREEN, - name = "FLAG_FULLSCREEN"), + name = "FULLSCREEN"), @ViewDebug.FlagToString(mask = FLAG_FORCE_NOT_FULLSCREEN, equals = FLAG_FORCE_NOT_FULLSCREEN, - name = "FLAG_FORCE_NOT_FULLSCREEN"), + name = "FORCE_NOT_FULLSCREEN"), @ViewDebug.FlagToString(mask = FLAG_DITHER, equals = FLAG_DITHER, - name = "FLAG_DITHER"), + name = "DITHER"), @ViewDebug.FlagToString(mask = FLAG_SECURE, equals = FLAG_SECURE, - name = "FLAG_SECURE"), + name = "SECURE"), @ViewDebug.FlagToString(mask = FLAG_SCALED, equals = FLAG_SCALED, - name = "FLAG_SCALED"), + name = "SCALED"), @ViewDebug.FlagToString(mask = FLAG_IGNORE_CHEEK_PRESSES, equals = FLAG_IGNORE_CHEEK_PRESSES, - name = "FLAG_IGNORE_CHEEK_PRESSES"), + name = "IGNORE_CHEEK_PRESSES"), @ViewDebug.FlagToString(mask = FLAG_LAYOUT_INSET_DECOR, equals = FLAG_LAYOUT_INSET_DECOR, - name = "FLAG_LAYOUT_INSET_DECOR"), + name = "LAYOUT_INSET_DECOR"), @ViewDebug.FlagToString(mask = FLAG_ALT_FOCUSABLE_IM, equals = FLAG_ALT_FOCUSABLE_IM, - name = "FLAG_ALT_FOCUSABLE_IM"), + name = "ALT_FOCUSABLE_IM"), @ViewDebug.FlagToString(mask = FLAG_WATCH_OUTSIDE_TOUCH, equals = FLAG_WATCH_OUTSIDE_TOUCH, - name = "FLAG_WATCH_OUTSIDE_TOUCH"), + name = "WATCH_OUTSIDE_TOUCH"), @ViewDebug.FlagToString(mask = FLAG_SHOW_WHEN_LOCKED, equals = FLAG_SHOW_WHEN_LOCKED, - name = "FLAG_SHOW_WHEN_LOCKED"), + name = "SHOW_WHEN_LOCKED"), @ViewDebug.FlagToString(mask = FLAG_SHOW_WALLPAPER, equals = FLAG_SHOW_WALLPAPER, - name = "FLAG_SHOW_WALLPAPER"), + name = "SHOW_WALLPAPER"), @ViewDebug.FlagToString(mask = FLAG_TURN_SCREEN_ON, equals = FLAG_TURN_SCREEN_ON, - name = "FLAG_TURN_SCREEN_ON"), + name = "TURN_SCREEN_ON"), @ViewDebug.FlagToString(mask = FLAG_DISMISS_KEYGUARD, equals = FLAG_DISMISS_KEYGUARD, - name = "FLAG_DISMISS_KEYGUARD"), + name = "DISMISS_KEYGUARD"), @ViewDebug.FlagToString(mask = FLAG_SPLIT_TOUCH, equals = FLAG_SPLIT_TOUCH, - name = "FLAG_SPLIT_TOUCH"), + name = "SPLIT_TOUCH"), @ViewDebug.FlagToString(mask = FLAG_HARDWARE_ACCELERATED, equals = FLAG_HARDWARE_ACCELERATED, - name = "FLAG_HARDWARE_ACCELERATED"), - @ViewDebug.FlagToString(mask = FLAG_LOCAL_FOCUS_MODE, equals = FLAG_LOCAL_FOCUS_MODE, - name = "FLAG_LOCAL_FOCUS_MODE"), + name = "HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString(mask = FLAG_LAYOUT_IN_OVERSCAN, equals = FLAG_LAYOUT_IN_OVERSCAN, + name = "LOCAL_FOCUS_MODE"), @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_STATUS, equals = FLAG_TRANSLUCENT_STATUS, - name = "FLAG_TRANSLUCENT_STATUS"), + name = "TRANSLUCENT_STATUS"), @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_NAVIGATION, equals = FLAG_TRANSLUCENT_NAVIGATION, - name = "FLAG_TRANSLUCENT_NAVIGATION"), + name = "TRANSLUCENT_NAVIGATION"), + @ViewDebug.FlagToString(mask = FLAG_LOCAL_FOCUS_MODE, equals = FLAG_LOCAL_FOCUS_MODE, + name = "LOCAL_FOCUS_MODE"), + @ViewDebug.FlagToString(mask = FLAG_SLIPPERY, equals = FLAG_SLIPPERY, + name = "FLAG_SLIPPERY"), + @ViewDebug.FlagToString(mask = FLAG_LAYOUT_ATTACHED_IN_DECOR, equals = FLAG_LAYOUT_ATTACHED_IN_DECOR, + name = "FLAG_LAYOUT_ATTACHED_IN_DECOR"), @ViewDebug.FlagToString(mask = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, equals = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - name = "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS") + name = "DRAWS_SYSTEM_BAR_BACKGROUNDS") }, formatToHexString = true) public int flags; @@ -1438,6 +1446,88 @@ public interface WindowManager extends ViewManager { * Control flags that are private to the platform. * @hide */ + @ViewDebug.ExportedProperty(flagMapping = { + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED, + equals = PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED, + name = "FAKE_HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED, + equals = PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED, + name = "FORCE_HARDWARE_ACCELERATED"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS, + equals = PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS, + name = "WANTS_OFFSET_NOTIFICATIONS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SHOW_FOR_ALL_USERS, + equals = PRIVATE_FLAG_SHOW_FOR_ALL_USERS, + name = "SHOW_FOR_ALL_USERS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_NO_MOVE_ANIMATION, + equals = PRIVATE_FLAG_NO_MOVE_ANIMATION, + name = "NO_MOVE_ANIMATION"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_COMPATIBLE_WINDOW, + equals = PRIVATE_FLAG_COMPATIBLE_WINDOW, + name = "COMPATIBLE_WINDOW"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SYSTEM_ERROR, + equals = PRIVATE_FLAG_SYSTEM_ERROR, + name = "SYSTEM_ERROR"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR, + equals = PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR, + name = "INHERIT_TRANSLUCENT_DECOR"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_KEYGUARD, + equals = PRIVATE_FLAG_KEYGUARD, + name = "KEYGUARD"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, + equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS, + name = "DISABLE_WALLPAPER_TOUCH_EVENTS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT, + equals = PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT, + name = "FORCE_STATUS_BAR_VISIBLE_TRANSPARENT"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_PRESERVE_GEOMETRY, + equals = PRIVATE_FLAG_PRESERVE_GEOMETRY, + name = "PRESERVE_GEOMETRY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, + equals = PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY, + name = "FORCE_DECOR_VIEW_VISIBILITY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH, + equals = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH, + name = "WILL_NOT_REPLACE_ON_RELAUNCH"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME, + equals = PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME, + name = "LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND, + equals = PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND, + name = "FORCE_DRAW_STATUS_BAR_BACKGROUND"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE, + equals = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE, + name = "SUSTAINED_PERFORMANCE_MODE"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + equals = PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + name = "HIDE_NON_SYSTEM_OVERLAY_WINDOWS"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY, + equals = PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY, + name = "IS_ROUNDED_CORNERS_OVERLAY"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, + equals = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, + name = "ACQUIRES_SLEEP_TOKEN") + }) @TestApi public int privateFlags; @@ -1977,7 +2067,7 @@ public interface WindowManager extends ViewManager { * @hide */ @ActivityInfo.ColorMode - private int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT; + private int mColorMode = COLOR_MODE_DEFAULT; public LayoutParams() { super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -2442,9 +2532,15 @@ public interface WindowManager extends ViewManager { @Override public String toString() { + return toString(""); + } + + /** + * @hide + */ + public String toString(String prefix) { StringBuilder sb = new StringBuilder(256); - sb.append("WM.LayoutParams{"); - sb.append("("); + sb.append("{("); sb.append(x); sb.append(','); sb.append(y); @@ -2464,26 +2560,19 @@ public interface WindowManager extends ViewManager { sb.append(verticalMargin); } if (gravity != 0) { - sb.append(" gr=#"); - sb.append(Integer.toHexString(gravity)); + sb.append(" gr="); + sb.append(Gravity.toString(gravity)); } if (softInputMode != 0) { - sb.append(" sim=#"); - sb.append(Integer.toHexString(softInputMode)); + sb.append(" sim={"); + sb.append(softInputModeToString(softInputMode)); + sb.append('}'); } sb.append(" ty="); - sb.append(type); - sb.append(" fl=#"); - sb.append(Integer.toHexString(flags)); - if (privateFlags != 0) { - if ((privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) { - sb.append(" compatible=true"); - } - sb.append(" pfl=0x").append(Integer.toHexString(privateFlags)); - } + sb.append(ViewDebug.intToString(LayoutParams.class, "type", type)); if (format != PixelFormat.OPAQUE) { sb.append(" fmt="); - sb.append(format); + sb.append(PixelFormat.formatToString(format)); } if (windowAnimations != 0) { sb.append(" wanim=0x"); @@ -2491,7 +2580,7 @@ public interface WindowManager extends ViewManager { } if (screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { sb.append(" or="); - sb.append(screenOrientation); + sb.append(ActivityInfo.screenOrientationToString(screenOrientation)); } if (alpha != 1.0f) { sb.append(" alpha="); @@ -2507,7 +2596,7 @@ public interface WindowManager extends ViewManager { } if (rotationAnimation != ROTATION_ANIMATION_ROTATE) { sb.append(" rotAnim="); - sb.append(rotationAnimation); + sb.append(rotationAnimationToString(rotationAnimation)); } if (preferredRefreshRate != 0) { sb.append(" preferredRefreshRate="); @@ -2517,20 +2606,12 @@ public interface WindowManager extends ViewManager { sb.append(" preferredDisplayMode="); sb.append(preferredDisplayModeId); } - if (systemUiVisibility != 0) { - sb.append(" sysui=0x"); - sb.append(Integer.toHexString(systemUiVisibility)); - } - if (subtreeSystemUiVisibility != 0) { - sb.append(" vsysui=0x"); - sb.append(Integer.toHexString(subtreeSystemUiVisibility)); - } if (hasSystemUiListeners) { sb.append(" sysuil="); sb.append(hasSystemUiListeners); } if (inputFeatures != 0) { - sb.append(" if=0x").append(Integer.toHexString(inputFeatures)); + sb.append(" if=").append(inputFeatureToString(inputFeatures)); } if (userActivityTimeout >= 0) { sb.append(" userActivityTimeout=").append(userActivityTimeout); @@ -2546,11 +2627,30 @@ public interface WindowManager extends ViewManager { sb.append(" (!preservePreviousSurfaceInsets)"); } } - if (needsMenuKey != NEEDS_MENU_UNSET) { - sb.append(" needsMenuKey="); - sb.append(needsMenuKey); + if (needsMenuKey == NEEDS_MENU_SET_TRUE) { + sb.append(" needsMenuKey"); + } + if (mColorMode != COLOR_MODE_DEFAULT) { + sb.append(" colorMode=").append(ActivityInfo.colorModeToString(mColorMode)); + } + sb.append(System.lineSeparator()); + sb.append(prefix).append(" fl=").append( + ViewDebug.flagsToString(LayoutParams.class, "flags", flags)); + if (privateFlags != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" pfl=").append(ViewDebug.flagsToString( + LayoutParams.class, "privateFlags", privateFlags)); + } + if (systemUiVisibility != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" sysui=").append(ViewDebug.flagsToString( + View.class, "mSystemUiVisibility", systemUiVisibility)); + } + if (subtreeSystemUiVisibility != 0) { + sb.append(System.lineSeparator()); + sb.append(prefix).append(" vsysui=").append(ViewDebug.flagsToString( + View.class, "mSystemUiVisibility", subtreeSystemUiVisibility)); } - sb.append(" colorMode=").append(mColorMode); sb.append('}'); return sb.toString(); } @@ -2634,5 +2734,88 @@ public interface WindowManager extends ViewManager { && width == WindowManager.LayoutParams.MATCH_PARENT && height == WindowManager.LayoutParams.MATCH_PARENT; } + + private static String softInputModeToString(@SoftInputModeFlags int softInputMode) { + final StringBuilder result = new StringBuilder(); + final int state = softInputMode & SOFT_INPUT_MASK_STATE; + if (state != 0) { + result.append("state="); + switch (state) { + case SOFT_INPUT_STATE_UNCHANGED: + result.append("unchanged"); + break; + case SOFT_INPUT_STATE_HIDDEN: + result.append("hidden"); + break; + case SOFT_INPUT_STATE_ALWAYS_HIDDEN: + result.append("always_hidden"); + break; + case SOFT_INPUT_STATE_VISIBLE: + result.append("visible"); + break; + case SOFT_INPUT_STATE_ALWAYS_VISIBLE: + result.append("always_visible"); + break; + default: + result.append(state); + break; + } + result.append(' '); + } + final int adjust = softInputMode & SOFT_INPUT_MASK_ADJUST; + if (adjust != 0) { + result.append("adjust="); + switch (adjust) { + case SOFT_INPUT_ADJUST_RESIZE: + result.append("resize"); + break; + case SOFT_INPUT_ADJUST_PAN: + result.append("pan"); + break; + case SOFT_INPUT_ADJUST_NOTHING: + result.append("nothing"); + break; + default: + result.append(adjust); + break; + } + result.append(' '); + } + if ((softInputMode & SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { + result.append("forwardNavigation").append(' '); + } + result.deleteCharAt(result.length() - 1); + return result.toString(); + } + + private static String rotationAnimationToString(int rotationAnimation) { + switch (rotationAnimation) { + case ROTATION_ANIMATION_UNSPECIFIED: + return "UNSPECIFIED"; + case ROTATION_ANIMATION_ROTATE: + return "ROTATE"; + case ROTATION_ANIMATION_CROSSFADE: + return "CROSSFADE"; + case ROTATION_ANIMATION_JUMPCUT: + return "JUMPCUT"; + case ROTATION_ANIMATION_SEAMLESS: + return "SEAMLESS"; + default: + return Integer.toString(rotationAnimation); + } + } + + private static String inputFeatureToString(int inputFeature) { + switch (inputFeature) { + case INPUT_FEATURE_DISABLE_POINTER_GESTURES: + return "DISABLE_POINTER_GESTURES"; + case INPUT_FEATURE_NO_INPUT_CHANNEL: + return "NO_INPUT_CHANNEL"; + case INPUT_FEATURE_DISABLE_USER_ACTIVITY: + return "DISABLE_USER_ACTIVITY"; + default: + return Integer.toString(inputFeature); + } + } } } diff --git a/android/view/WindowManagerPolicy.java b/android/view/WindowManagerPolicy.java index 66506a18..da72535d 100644 --- a/android/view/WindowManagerPolicy.java +++ b/android/view/WindowManagerPolicy.java @@ -592,9 +592,10 @@ public interface WindowManagerPolicy { int getDockedDividerInsetsLw(); /** - * Retrieves the {@param outBounds} from the stack with id {@param stackId}. + * Retrieves the {@param outBounds} from the stack matching the {@param windowingMode} and + * {@param activityType}. */ - void getStackBounds(int stackId, Rect outBounds); + void getStackBounds(int windowingMode, int activityType, Rect outBounds); /** * Notifies window manager that {@link #isShowingDreamLw} has changed. @@ -617,6 +618,38 @@ public interface WindowManagerPolicy { * @param listener callback to call when display can be turned off */ void screenTurningOff(ScreenOffListener listener); + + /** + * Convert the lid state to a human readable format. + */ + static String lidStateToString(int lid) { + switch (lid) { + case LID_ABSENT: + return "LID_ABSENT"; + case LID_CLOSED: + return "LID_CLOSED"; + case LID_OPEN: + return "LID_OPEN"; + default: + return Integer.toString(lid); + } + } + + /** + * Convert the camera lens state to a human readable format. + */ + static String cameraLensStateToString(int lens) { + switch (lens) { + case CAMERA_LENS_COVER_ABSENT: + return "CAMERA_LENS_COVER_ABSENT"; + case CAMERA_LENS_UNCOVERED: + return "CAMERA_LENS_UNCOVERED"; + case CAMERA_LENS_COVERED: + return "CAMERA_LENS_COVERED"; + default: + return Integer.toString(lens); + } + } } public interface PointerEventListener { @@ -1750,4 +1783,34 @@ public interface WindowManagerPolicy { * @return true if ready; false otherwise. */ boolean canDismissBootAnimation(); + + /** + * Convert the user rotation mode to a human readable format. + */ + static String userRotationModeToString(int mode) { + switch(mode) { + case USER_ROTATION_FREE: + return "USER_ROTATION_FREE"; + case USER_ROTATION_LOCKED: + return "USER_ROTATION_LOCKED"; + default: + return Integer.toString(mode); + } + } + + /** + * Convert the off reason to a human readable format. + */ + static String offReasonToString(int why) { + switch (why) { + case OFF_BECAUSE_OF_ADMIN: + return "OFF_BECAUSE_OF_ADMIN"; + case OFF_BECAUSE_OF_USER: + return "OFF_BECAUSE_OF_USER"; + case OFF_BECAUSE_OF_TIMEOUT: + return "OFF_BECAUSE_OF_TIMEOUT"; + default: + return Integer.toString(why); + } + } } diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 11cb046a..0b9bc576 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -16,46 +16,152 @@ 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. - * Such events are generated when something notable happens in the user interface, + * 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, * 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 - * {@code android.accessibilityservice.AccessibilityService}. + * {@link android.accessibilityservice.AccessibilityService}. * * @see AccessibilityEvent - * @see android.content.Context#getSystemService + * @see AccessibilityNodeInfo + * @see android.accessibilityservice.AccessibilityService + * @see Context#getSystemService + * @see Context#ACCESSIBILITY_SERVICE */ -@SuppressWarnings("UnusedDeclaration") +@SystemService(Context.ACCESSIBILITY_SERVICE) 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; - private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0); + int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK; + 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 accessibility state. + * 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}. */ public interface AccessibilityStateChangeListener { /** - * Called back on change in the accessibility state. + * Called when the accessibility enabled state changes. * * @param enabled Whether accessibility is enabled. */ - public void onAccessibilityStateChanged(boolean enabled); + void onAccessibilityStateChanged(boolean enabled); } /** @@ -71,7 +177,24 @@ public final class AccessibilityManager { * * @param enabled Whether touch exploration is enabled. */ - public void onTouchExplorationStateChanged(boolean 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); } /** @@ -79,6 +202,8 @@ 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 { @@ -87,26 +212,72 @@ public final class AccessibilityManager { * * @param enabled Whether high text contrast is enabled. */ - public void onHighTextContrastStateChanged(boolean enabled); + void onHighTextContrastStateChanged(boolean enabled); } private final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { - public void setState(int state) { - } + @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(); + } - public void notifyServicesStateChanged() { + @Override + public void notifyServicesStateChanged() { + final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mServicesStateChangeListeners.isEmpty()) { + return; } + listeners = new ArrayMap<>(mServicesStateChangeListeners); + } - public void setRelevantEventTypes(int eventTypes) { - } - }; + 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)); + } + } + + @Override + public void setRelevantEventTypes(int eventTypes) { + mRelevantEventTypes = 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; } @@ -114,21 +285,68 @@ 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; } /** - * Returns if the {@link AccessibilityManager} is enabled. + * @hide + */ + @VisibleForTesting + public Handler.Callback getCallback() { + return mCallback; + } + + /** + * Returns if the accessibility in the system is enabled. * - * @return True if this {@link AccessibilityManager} is enabled, false otherwise. + * @return True if accessibility is enabled, false otherwise. */ public boolean isEnabled() { - return false; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsEnabled; + } } /** @@ -137,7 +355,13 @@ public final class AccessibilityManager { * @return True if touch exploration is enabled, false otherwise. */ public boolean isTouchExplorationEnabled() { - return true; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsTouchExplorationEnabled; + } } /** @@ -147,35 +371,169 @@ 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() { - return false; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsHighTextContrastEnabled; + } } /** * 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 interruption of the accessibility feedback from all accessibility services. + * Requests feedback interruption 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() { - return Collections.emptyList(); + 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); } + /** + * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. + * + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + */ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { - return Collections.emptyList(); + 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(); + } } /** @@ -190,21 +548,48 @@ 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) { - return Collections.emptyList(); + 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(); + } } /** * Registers an {@link AccessibilityStateChangeListener} for changes in - * the global accessibility state of the system. + * the global accessibility state of the system. Equivalent to calling + * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)} + * with a null handler. * * @param listener The listener. - * @return True if successfully registered. + * @return Always returns {@code true}. */ public boolean addAccessibilityStateChangeListener( - AccessibilityStateChangeListener listener) { + @NonNull AccessibilityStateChangeListener listener) { + addAccessibilityStateChangeListener(listener, null); return true; } @@ -218,22 +603,40 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {} + @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mAccessibilityStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } + /** + * Unregisters an {@link AccessibilityStateChangeListener}. + * + * @param listener The listener. + * @return True if the listener was previously registered. + */ public boolean removeAccessibilityStateChangeListener( - AccessibilityStateChangeListener listener) { - return true; + @NonNull AccessibilityStateChangeListener listener) { + synchronized (mLock) { + int index = mAccessibilityStateChangeListeners.indexOfKey(listener); + mAccessibilityStateChangeListeners.remove(listener); + return (index >= 0); + } } /** * Registers a {@link TouchExplorationStateChangeListener} for changes in - * the global touch exploration state of the system. + * the global touch exploration state of the system. Equivalent to calling + * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)} + * with a null handler. * * @param listener The listener. - * @return True if successfully registered. + * @return Always returns {@code true}. */ public boolean addTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { + addTouchExplorationStateChangeListener(listener, null); return true; } @@ -247,17 +650,103 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addTouchExplorationStateChangeListener( - @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {} + @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mTouchExplorationStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } /** * Unregisters a {@link TouchExplorationStateChangeListener}. * * @param listener The listener. - * @return True if successfully unregistered. + * @return True if listener was previously registered. */ public boolean removeTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - return true; + 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); } /** @@ -269,7 +758,12 @@ public final class AccessibilityManager { * @hide */ public void addHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {} + @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mHighTextContrastStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } /** * Unregisters a {@link HighTextContrastChangeListener}. @@ -279,7 +773,51 @@ public final class AccessibilityManager { * @hide */ public void removeHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener) {} + @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; + } + } /** * Sets the current state and notifies listeners, if necessary. @@ -287,14 +825,314 @@ 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 61cbce97..4fb2a99a 100644 --- a/android/view/autofill/AutofillManager.java +++ b/android/view/autofill/AutofillManager.java @@ -37,14 +37,13 @@ import android.service.autofill.AutofillService; import android.service.autofill.FillEventHistory; import android.util.ArrayMap; import android.util.ArraySet; -import android.util.DebugUtils; import android.util.Log; import android.util.SparseArray; import android.view.View; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -202,9 +201,12 @@ public final class AutofillManager { * Initial state of the autofill context, set when there is no session (i.e., when * {@link #mSessionId} is {@link #NO_SESSION}). * + * <p>In this state, app callbacks (such as {@link #notifyViewEntered(View)}) are notified to + * the server. + * * @hide */ - public static final int STATE_UNKNOWN = 1; + public static final int STATE_UNKNOWN = 0; /** * State where the autofill context hasn't been {@link #commit() finished} nor @@ -212,7 +214,18 @@ public final class AutofillManager { * * @hide */ - public static final int STATE_ACTIVE = 2; + public static final int STATE_ACTIVE = 1; + + /** + * State where the autofill context was finished by the server because the autofill + * service could not autofill the page. + * + * <p>In this state, most apps callback (such as {@link #notifyViewEntered(View)}) are ignored, + * exception {@link #requestAutofill(View)} (and {@link #requestAutofill(View, int, Rect)}). + * + * @hide + */ + public static final int STATE_FINISHED = 2; /** * State where the autofill context has been {@link #commit() finished} but the server still has @@ -220,7 +233,7 @@ public final class AutofillManager { * * @hide */ - public static final int STATE_SHOWING_SAVE_UI = 4; + public static final int STATE_SHOWING_SAVE_UI = 3; /** * Makes an authentication id from a request id and a dataset id. @@ -559,6 +572,14 @@ public final class AutofillManager { } AutofillCallback callback = null; synchronized (mLock) { + if (isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { + if (sVerbose) { + Log.v(TAG, "notifyViewEntered(flags=" + flags + ", view=" + view + + "): ignored on state " + getStateAsStringLocked()); + } + return; + } + ensureServiceClientAddedIfNeededLocked(); if (!mEnabled) { @@ -682,6 +703,14 @@ public final class AutofillManager { } AutofillCallback callback = null; synchronized (mLock) { + if (isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { + if (sVerbose) { + Log.v(TAG, "notifyViewEntered(flags=" + flags + ", view=" + view + + ", virtualId=" + virtualId + + "): ignored on state " + getStateAsStringLocked()); + } + return; + } ensureServiceClientAddedIfNeededLocked(); if (!mEnabled) { @@ -765,6 +794,10 @@ public final class AutofillManager { } if (!mEnabled || !isActiveLocked()) { + if (sVerbose && mEnabled) { + Log.v(TAG, "notifyValueChanged(" + view + "): ignoring on state " + + getStateAsStringLocked()); + } return; } @@ -904,10 +937,7 @@ public final class AutofillManager { } private AutofillClient getClientLocked() { - if (mContext instanceof AutofillClient) { - return (AutofillClient) mContext; - } - return null; + return mContext.getAutofillClient(); } /** @hide */ @@ -950,10 +980,13 @@ public final class AutofillManager { @NonNull AutofillValue value, int flags) { if (sVerbose) { Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value - + ", flags=" + flags + ", state=" + mState); + + ", flags=" + flags + ", state=" + getStateAsStringLocked()); } - if (mState != STATE_UNKNOWN) { - if (sDebug) Log.d(TAG, "not starting session for " + id + " on state " + mState); + if (mState != STATE_UNKNOWN && (flags & FLAG_MANUAL_REQUEST) == 0) { + if (sVerbose) { + Log.v(TAG, "not automatically starting session for " + id + + " on state " + getStateAsStringLocked()); + } return; } try { @@ -973,7 +1006,7 @@ public final class AutofillManager { } private void finishSessionLocked() { - if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + mState); + if (sVerbose) Log.v(TAG, "finishSessionLocked(): " + getStateAsStringLocked()); if (!isActiveLocked()) return; @@ -987,7 +1020,7 @@ public final class AutofillManager { } private void cancelSessionLocked() { - if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + mState); + if (sVerbose) Log.v(TAG, "cancelSessionLocked(): " + getStateAsStringLocked()); if (!isActiveLocked()) return; @@ -1245,10 +1278,10 @@ public final class AutofillManager { } } - final LogMaker log = new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_DATASET_APPLIED); - log.addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount); - log.addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, - numApplied); + final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_DATASET_APPLIED) + .setPackageName(mContext.getPackageName()) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VALUES, itemCount) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_VIEWS_FILLED, numApplied); mMetricsLogger.write(log); } } @@ -1306,6 +1339,20 @@ public final class AutofillManager { } } + /** + * Marks the state of the session as finished. + * + * @param newState {@link #STATE_FINISHED} (because the autofill service returned a {@code null} + * FillResponse) or {@link #STATE_UNKNOWN} (because the session was removed). + */ + private void setSessionFinished(int newState) { + synchronized (mLock) { + if (sVerbose) Log.v(TAG, "setSessionFinished(): from " + mState + " to " + newState); + resetSessionLocked(); + mState = newState; + } + } + private void requestHideFillUi(AutofillId id) { final View anchor = findView(id); if (sVerbose) Log.v(TAG, "requestHideFillUi(" + id + "): anchor = " + anchor); @@ -1341,7 +1388,11 @@ public final class AutofillManager { } } - private void notifyNoFillUi(int sessionId, AutofillId id) { + private void notifyNoFillUi(int sessionId, AutofillId id, boolean sessionFinished) { + if (sVerbose) { + Log.v(TAG, "notifyNoFillUi(): sessionId=" + sessionId + ", autofillId=" + id + + ", finished=" + sessionFinished); + } final View anchor = findView(id); if (anchor == null) { return; @@ -1361,7 +1412,11 @@ public final class AutofillManager { } else { callback.onAutofillEvent(anchor, AutofillCallback.EVENT_INPUT_UNAVAILABLE); } + } + if (sessionFinished) { + // Callback call was "hijacked" to also update the session state. + setSessionFinished(STATE_FINISHED); } } @@ -1434,8 +1489,7 @@ public final class AutofillManager { pw.print(outerPrefix); pw.println("AutofillManager:"); final String pfx = outerPrefix + " "; pw.print(pfx); pw.print("sessionId: "); pw.println(mSessionId); - pw.print(pfx); pw.print("state: "); pw.println( - DebugUtils.flagsToString(AutofillManager.class, "STATE_", mState)); + pw.print(pfx); pw.print("state: "); pw.println(getStateAsStringLocked()); 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); @@ -1452,10 +1506,29 @@ public final class AutofillManager { pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds); } + private String getStateAsStringLocked() { + switch (mState) { + case STATE_UNKNOWN: + return "STATE_UNKNOWN"; + case STATE_ACTIVE: + return "STATE_ACTIVE"; + case STATE_FINISHED: + return "STATE_FINISHED"; + case STATE_SHOWING_SAVE_UI: + return "STATE_SHOWING_SAVE_UI"; + default: + return "INVALID:" + mState; + } + } + private boolean isActiveLocked() { return mState == STATE_ACTIVE; } + private boolean isFinishedLocked() { + return mState == STATE_FINISHED; + } + private void post(Runnable runnable) { final AutofillClient client = getClientLocked(); if (client == null) { @@ -1787,10 +1860,10 @@ public final class AutofillManager { } @Override - public void notifyNoFillUi(int sessionId, AutofillId id) { + public void notifyNoFillUi(int sessionId, AutofillId id, boolean sessionFinished) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() -> afm.notifyNoFillUi(sessionId, id)); + afm.post(() -> afm.notifyNoFillUi(sessionId, id, sessionFinished)); } } @@ -1823,7 +1896,15 @@ public final class AutofillManager { public void setSaveUiState(int sessionId, boolean shown) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() ->afm.setSaveUiState(sessionId, shown)); + afm.post(() -> afm.setSaveUiState(sessionId, shown)); + } + } + + @Override + public void setSessionFinished(int newState) { + final AutofillManager afm = mAfm.get(); + if (afm != null) { + afm.post(() -> afm.setSessionFinished(newState)); } } } diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java index 5f476380..b4688bb1 100644 --- a/android/view/autofill/AutofillPopupWindow.java +++ b/android/view/autofill/AutofillPopupWindow.java @@ -47,6 +47,19 @@ public class AutofillPopupWindow extends PopupWindow { private final WindowPresenter mWindowPresenter; private WindowManager.LayoutParams mWindowLayoutParams; + private final View.OnAttachStateChangeListener mOnAttachStateChangeListener = + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + /* ignore - handled by the super class */ + } + + @Override + public void onViewDetachedFromWindow(View v) { + dismiss(); + } + }; + /** * Creates a popup window with a presenter owning the window and responsible for * showing/hiding/updating the backing window. This can be useful of the window is @@ -208,7 +221,21 @@ public class AutofillPopupWindow extends PopupWindow { p.packageName = anchor.getContext().getPackageName(); mWindowPresenter.show(p, getTransitionEpicenter(), isLayoutInsetDecor(), anchor.getLayoutDirection()); - return; + } + + @Override + protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { + super.attachToAnchor(anchor, xoff, yoff, gravity); + anchor.addOnAttachStateChangeListener(mOnAttachStateChangeListener); + } + + @Override + protected void detachFromAnchor() { + final View anchor = getAnchor(); + if (anchor != null) { + anchor.removeOnAttachStateChangeListener(mOnAttachStateChangeListener); + } + super.detachFromAnchor(); } @Override diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java index 1849368f..8c3b8a2e 100644 --- a/android/view/textclassifier/TextClassification.java +++ b/android/view/textclassifier/TextClassification.java @@ -28,6 +28,7 @@ import android.view.textclassifier.TextClassifier.EntityType; import com.android.internal.util.Preconditions; +import java.util.ArrayList; import java.util.List; /** @@ -41,10 +42,10 @@ public final class TextClassification { static final TextClassification EMPTY = new TextClassification.Builder().build(); @NonNull private final String mText; - @Nullable private final Drawable mIcon; - @Nullable private final String mLabel; - @Nullable private final Intent mIntent; - @Nullable private final OnClickListener mOnClickListener; + @NonNull private final List<Drawable> mIcons; + @NonNull private final List<String> mLabels; + @NonNull private final List<Intent> mIntents; + @NonNull private final List<OnClickListener> mOnClickListeners; @NonNull private final EntityConfidence<String> mEntityConfidence; @NonNull private final List<String> mEntities; private int mLogType; @@ -52,18 +53,21 @@ public final class TextClassification { private TextClassification( @Nullable String text, - @Nullable Drawable icon, - @Nullable String label, - @Nullable Intent intent, - @Nullable OnClickListener onClickListener, + @NonNull List<Drawable> icons, + @NonNull List<String> labels, + @NonNull List<Intent> intents, + @NonNull List<OnClickListener> onClickListeners, @NonNull EntityConfidence<String> entityConfidence, int logType, @NonNull String versionInfo) { + Preconditions.checkArgument(labels.size() == intents.size()); + Preconditions.checkArgument(icons.size() == intents.size()); + Preconditions.checkArgument(onClickListeners.size() == intents.size()); mText = text; - mIcon = icon; - mLabel = label; - mIntent = intent; - mOnClickListener = onClickListener; + mIcons = icons; + mLabels = labels; + mIntents = intents; + mOnClickListeners = onClickListeners; mEntityConfidence = new EntityConfidence<>(entityConfidence); mEntities = mEntityConfidence.getEntities(); mLogType = logType; @@ -109,35 +113,106 @@ public final class TextClassification { } /** - * Returns an icon that may be rendered on a widget used to act on the classified text. + * Returns the number of actions that are available to act on the classified text. + * @see #getIntent(int) + * @see #getLabel(int) + * @see #getIcon(int) + * @see #getOnClickListener(int) + */ + @IntRange(from = 0) + public int getActionCount() { + return mIntents.size(); + } + + /** + * Returns one of the icons that maybe rendered on a widget used to act on the classified text. + * @param index Index of the action to get the icon for. + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getActionCount() for the number of entities available. + * @see #getIntent(int) + * @see #getLabel(int) + * @see #getOnClickListener(int) + */ + @Nullable + public Drawable getIcon(int index) { + return mIcons.get(index); + } + + /** + * Returns an icon for the default intent that may be rendered on a widget used to act on the + * classified text. */ @Nullable public Drawable getIcon() { - return mIcon; + return mIcons.isEmpty() ? null : mIcons.get(0); } /** - * Returns a label that may be rendered on a widget used to act on the classified text. + * Returns one of the labels that may be rendered on a widget used to act on the classified + * text. + * @param index Index of the action to get the label for. + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getActionCount() + * @see #getIntent(int) + * @see #getIcon(int) + * @see #getOnClickListener(int) + */ + @Nullable + public CharSequence getLabel(int index) { + return mLabels.get(index); + } + + /** + * Returns a label for the default intent that may be rendered on a widget used to act on the + * classified text. */ @Nullable public CharSequence getLabel() { - return mLabel; + return mLabels.isEmpty() ? null : mLabels.get(0); } /** - * Returns an intent that may be fired to act on the classified text. + * Returns one of the intents that may be fired to act on the classified text. + * @param index Index of the action to get the intent for. + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getActionCount() + * @see #getLabel(int) + * @see #getIcon(int) + * @see #getOnClickListener(int) + */ + @Nullable + public Intent getIntent(int index) { + return mIntents.get(index); + } + + /** + * Returns the default intent that may be fired to act on the classified text. */ @Nullable public Intent getIntent() { - return mIntent; + return mIntents.isEmpty() ? null : mIntents.get(0); } /** - * Returns an OnClickListener that may be triggered to act on the classified text. + * Returns one of the OnClickListeners that may be triggered to act on the classified text. + * @param index Index of the action to get the click listener for. + * @throws IndexOutOfBoundsException if the specified index is out of range. + * @see #getActionCount() + * @see #getIntent(int) + * @see #getLabel(int) + * @see #getIcon(int) + */ + @Nullable + public OnClickListener getOnClickListener(int index) { + return mOnClickListeners.get(index); + } + + /** + * Returns the default OnClickListener that may be triggered to act on the classified text. */ @Nullable public OnClickListener getOnClickListener() { - return mOnClickListener; + return mOnClickListeners.isEmpty() ? null : mOnClickListeners.get(0); } /** @@ -160,8 +235,8 @@ public final class TextClassification { @Override public String toString() { return String.format("TextClassification {" - + "text=%s, entities=%s, label=%s, intent=%s}", - mText, mEntityConfidence, mLabel, mIntent); + + "text=%s, entities=%s, labels=%s, intents=%s}", + mText, mEntityConfidence, mLabels, mIntents); } /** @@ -184,10 +259,10 @@ public final class TextClassification { public static final class Builder { @NonNull private String mText; - @Nullable private Drawable mIcon; - @Nullable private String mLabel; - @Nullable private Intent mIntent; - @Nullable private OnClickListener mOnClickListener; + @NonNull private final List<Drawable> mIcons = new ArrayList<>(); + @NonNull private final List<String> mLabels = new ArrayList<>(); + @NonNull private final List<Intent> mIntents = new ArrayList<>(); + @NonNull private final List<OnClickListener> mOnClickListeners = new ArrayList<>(); @NonNull private final EntityConfidence<String> mEntityConfidence = new EntityConfidence<>(); private int mLogType; @@ -216,26 +291,57 @@ public final class TextClassification { } /** - * Sets an icon that may be rendered on a widget used to act on the classified text. + * Adds an action that may be performed on the classified text. The label and icon are used + * for rendering of widgets that offer the intent. Actions should be added in order of + * priority and the first one will be treated as the default. + */ + public Builder addAction( + Intent intent, @Nullable String label, @Nullable Drawable icon, + @Nullable OnClickListener onClickListener) { + mIntents.add(intent); + mLabels.add(label); + mIcons.add(icon); + mOnClickListeners.add(onClickListener); + return this; + } + + /** + * Removes all actions. + */ + public Builder clearActions() { + mIntents.clear(); + mOnClickListeners.clear(); + mLabels.clear(); + mIcons.clear(); + return this; + } + + /** + * Sets the icon for the default action that may be rendered on a widget used to act on the + * classified text. */ public Builder setIcon(@Nullable Drawable icon) { - mIcon = icon; + ensureDefaultActionAvailable(); + mIcons.set(0, icon); return this; } /** - * Sets a label that may be rendered on a widget used to act on the classified text. + * Sets the label for the default action that may be rendered on a widget used to act on the + * classified text. */ public Builder setLabel(@Nullable String label) { - mLabel = label; + ensureDefaultActionAvailable(); + mLabels.set(0, label); return this; } /** - * Sets an intent that may be fired to act on the classified text. + * Sets the intent for the default action that may be fired to act on the classified text. */ public Builder setIntent(@Nullable Intent intent) { - mIntent = intent; + ensureDefaultActionAvailable(); + mIntents.set(0, intent); return this; } @@ -249,10 +355,12 @@ public final class TextClassification { } /** - * Sets an OnClickListener that may be triggered to act on the classified text. + * Sets the OnClickListener for the default action that may be triggered to act on the + * classified text. */ public Builder setOnClickListener(@Nullable OnClickListener onClickListener) { - mOnClickListener = onClickListener; + ensureDefaultActionAvailable(); + mOnClickListeners.set(0, onClickListener); return this; } @@ -266,11 +374,21 @@ public final class TextClassification { } /** + * Ensures that we have at we have storage for the default action. + */ + private void ensureDefaultActionAvailable() { + if (mIntents.isEmpty()) mIntents.add(null); + if (mLabels.isEmpty()) mLabels.add(null); + if (mIcons.isEmpty()) mIcons.add(null); + if (mOnClickListeners.isEmpty()) mOnClickListeners.add(null); + } + + /** * Builds and returns a {@link TextClassification} object. */ public TextClassification build() { return new TextClassification( - mText, mIcon, mLabel, mIntent, mOnClickListener, mEntityConfidence, + mText, mIcons, mLabels, mIntents, mOnClickListeners, mEntityConfidence, mLogType, mVersionInfo); } } diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java index 7e93b78c..2aa81a2c 100644 --- a/android/view/textclassifier/TextClassifierImpl.java +++ b/android/view/textclassifier/TextClassifierImpl.java @@ -29,6 +29,7 @@ import android.net.Uri; import android.os.LocaleList; import android.os.ParcelFileDescriptor; import android.provider.Browser; +import android.provider.ContactsContract; import android.text.Spannable; import android.text.TextUtils; import android.text.method.WordIterator; @@ -356,7 +357,16 @@ final class TextClassifierImpl implements TextClassifier { final String type = getHighestScoringType(classifications); builder.setLogType(IntentFactory.getLogType(type)); - final Intent intent = IntentFactory.create(mContext, type, text.toString()); + final List<Intent> intents = IntentFactory.create(mContext, type, text.toString()); + for (Intent intent : intents) { + extendClassificationWithIntent(intent, builder); + } + + return builder.setVersionInfo(getVersionInfo()).build(); + } + + /** Extends the classification with the intent if it can be resolved. */ + private void extendClassificationWithIntent(Intent intent, TextClassification.Builder builder) { final PackageManager pm; final ResolveInfo resolveInfo; if (intent != null) { @@ -367,30 +377,29 @@ final class TextClassifierImpl implements TextClassifier { resolveInfo = null; } if (resolveInfo != null && resolveInfo.activityInfo != null) { - builder.setIntent(intent) - .setOnClickListener(TextClassification.createStartActivityOnClickListener( - mContext, intent)); - final String packageName = resolveInfo.activityInfo.packageName; + CharSequence label; + Drawable icon; if ("android".equals(packageName)) { // Requires the chooser to find an activity to handle the intent. - builder.setLabel(IntentFactory.getLabel(mContext, type)); + label = IntentFactory.getLabel(mContext, intent); + icon = null; } else { // A default activity will handle the intent. intent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name)); - Drawable icon = resolveInfo.activityInfo.loadIcon(pm); + icon = resolveInfo.activityInfo.loadIcon(pm); if (icon == null) { icon = resolveInfo.loadIcon(pm); } - builder.setIcon(icon); - CharSequence label = resolveInfo.activityInfo.loadLabel(pm); + label = resolveInfo.activityInfo.loadLabel(pm); if (label == null) { label = resolveInfo.loadLabel(pm); } - builder.setLabel(label != null ? label.toString() : null); } + builder.addAction( + intent, label != null ? label.toString() : null, icon, + TextClassification.createStartActivityOnClickListener(mContext, intent)); } - return builder.setVersionInfo(getVersionInfo()).build(); } private static int getHintFlags(CharSequence text, int start, int end) { @@ -477,10 +486,11 @@ final class TextClassifierImpl implements TextClassifier { if (results.length > 0) { final String type = getHighestScoringType(results); if (matches(type, linkMask)) { - final Intent intent = IntentFactory.create( + // For links without disambiguation, we simply use the default intent. + final List<Intent> intents = IntentFactory.create( context, type, text.substring(selectionStart, selectionEnd)); - if (hasActivityHandler(context, intent)) { - final ClickableSpan span = createSpan(context, intent); + if (!intents.isEmpty() && hasActivityHandler(context, intents.get(0))) { + final ClickableSpan span = createSpan(context, intents.get(0)); spans.add(new SpanSpec(selectionStart, selectionEnd, span)); } } @@ -564,7 +574,7 @@ final class TextClassifierImpl implements TextClassifier { }; } - private static boolean hasActivityHandler(Context context, @Nullable Intent intent) { + private static boolean hasActivityHandler(Context context, Intent intent) { if (intent == null) { return false; } @@ -625,20 +635,32 @@ final class TextClassifierImpl implements TextClassifier { private IntentFactory() {} - @Nullable - public static Intent create(Context context, String type, String text) { + @NonNull + public static List<Intent> create(Context context, String type, String text) { + final List<Intent> intents = new ArrayList<>(); type = type.trim().toLowerCase(Locale.ENGLISH); text = text.trim(); switch (type) { case TextClassifier.TYPE_EMAIL: - return new Intent(Intent.ACTION_SENDTO) - .setData(Uri.parse(String.format("mailto:%s", text))); + intents.add(new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse(String.format("mailto:%s", text)))); + intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) + .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) + .putExtra(ContactsContract.Intents.Insert.EMAIL, text)); + break; case TextClassifier.TYPE_PHONE: - return new Intent(Intent.ACTION_DIAL) - .setData(Uri.parse(String.format("tel:%s", text))); + intents.add(new Intent(Intent.ACTION_DIAL) + .setData(Uri.parse(String.format("tel:%s", text)))); + intents.add(new Intent(Intent.ACTION_INSERT_OR_EDIT) + .setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE) + .putExtra(ContactsContract.Intents.Insert.PHONE, text)); + intents.add(new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse(String.format("smsto:%s", text)))); + break; case TextClassifier.TYPE_ADDRESS: - return new Intent(Intent.ACTION_VIEW) - .setData(Uri.parse(String.format("geo:0,0?q=%s", text))); + intents.add(new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(String.format("geo:0,0?q=%s", text)))); + break; case TextClassifier.TYPE_URL: final String httpPrefix = "http://"; final String httpsPrefix = "https://"; @@ -649,25 +671,47 @@ final class TextClassifierImpl implements TextClassifier { } else { text = httpPrefix + text; } - return new Intent(Intent.ACTION_VIEW, Uri.parse(text)) - .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); - default: - return null; + intents.add(new Intent(Intent.ACTION_VIEW, Uri.parse(text)) + .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName())); + break; } + return intents; } @Nullable - public static String getLabel(Context context, String type) { - type = type.trim().toLowerCase(Locale.ENGLISH); - switch (type) { - case TextClassifier.TYPE_EMAIL: - return context.getString(com.android.internal.R.string.email); - case TextClassifier.TYPE_PHONE: + public static String getLabel(Context context, @Nullable Intent intent) { + if (intent == null || intent.getAction() == null) { + return null; + } + switch (intent.getAction()) { + case Intent.ACTION_DIAL: return context.getString(com.android.internal.R.string.dial); - case TextClassifier.TYPE_ADDRESS: - return context.getString(com.android.internal.R.string.map); - case TextClassifier.TYPE_URL: - return context.getString(com.android.internal.R.string.browse); + case Intent.ACTION_SENDTO: + switch (intent.getScheme()) { + case "mailto": + return context.getString(com.android.internal.R.string.email); + case "smsto": + return context.getString(com.android.internal.R.string.sms); + default: + return null; + } + case Intent.ACTION_INSERT_OR_EDIT: + switch (intent.getDataString()) { + case ContactsContract.Contacts.CONTENT_ITEM_TYPE: + return context.getString(com.android.internal.R.string.add_contact); + default: + return null; + } + case Intent.ACTION_VIEW: + switch (intent.getScheme()) { + case "geo": + return context.getString(com.android.internal.R.string.map); + case "http": // fall through + case "https": + return context.getString(com.android.internal.R.string.browse); + default: + return null; + } default: return null; } diff --git a/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/android/view/textclassifier/logging/SmartSelectionEventTracker.java index 8d88ba60..83af19bb 100644 --- a/android/view/textclassifier/logging/SmartSelectionEventTracker.java +++ b/android/view/textclassifier/logging/SmartSelectionEventTracker.java @@ -581,6 +581,7 @@ public final class SmartSelectionEventTracker { case ActionType.SMART_SHARE: // fall through case ActionType.DRAG: // fall through case ActionType.ABANDON: // fall through + case ActionType.OTHER: // fall through return true; default: return false; diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java index 8e1f2183..f368c74a 100644 --- a/android/view/textservice/TextServicesManager.java +++ b/android/view/textservice/TextServicesManager.java @@ -1,58 +1,213 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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 + * 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; /** - * A stub class of TextServicesManager for Layout-Lib. + * 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> + * */ +@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) public final class TextServicesManager { - private static final TextServicesManager sInstance = new TextServicesManager(); - private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0]; + 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)); + } /** * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist. * @hide */ public static TextServicesManager getInstance() { - return sInstance; + 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); + } } + /** + * 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) { - return null; + 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; } /** * @hide */ public SpellCheckerInfo[] getEnabledSpellCheckers() { - return EMPTY_SPELL_CHECKER_INFO; + 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(); + } } /** * @hide */ public SpellCheckerInfo getCurrentSpellChecker() { - return null; + try { + // Passing null as a locale for ICS + return mService.getCurrentSpellChecker(null); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -60,13 +215,22 @@ public final class TextServicesManager { */ public SpellCheckerSubtype getCurrentSpellCheckerSubtype( boolean allowImplicitlySelectedSubtype) { - return null; + 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(); + } } /** * @hide */ public boolean isSpellCheckerEnabled() { - return false; + try { + return mService.isSpellCheckerEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } diff --git a/android/webkit/CacheManager.java b/android/webkit/CacheManager.java index b8394203..fc76029a 100644 --- a/android/webkit/CacheManager.java +++ b/android/webkit/CacheManager.java @@ -16,13 +16,14 @@ package android.webkit; +import android.annotation.Nullable; + import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Map; - /** * Manages the HTTP cache used by an application's {@link WebView} instances. * @deprecated Access to the HTTP cache will be removed in a future release. @@ -233,6 +234,7 @@ public final class CacheManager { * @deprecated This method no longer has any effect and always returns {@code null}. */ @Deprecated + @Nullable public static File getCacheFileBaseDir() { return null; } @@ -287,6 +289,7 @@ public final class CacheManager { * @deprecated This method no longer has any effect and always returns {@code null}. */ @Deprecated + @Nullable public static CacheResult getCacheFile(String url, Map<String, String> headers) { return null; diff --git a/android/webkit/ClientCertRequest.java b/android/webkit/ClientCertRequest.java index de17534b..0fc47f1e 100644 --- a/android/webkit/ClientCertRequest.java +++ b/android/webkit/ClientCertRequest.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.Nullable; + import java.security.Principal; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -42,14 +44,16 @@ public abstract class ClientCertRequest { public ClientCertRequest() { } /** - * Returns the acceptable types of asymmetric keys (can be {@code null}). + * Returns the acceptable types of asymmetric keys. */ + @Nullable public abstract String[] getKeyTypes(); /** * Returns the acceptable certificate issuers for the certificate - * matching the private key (can be {@code null}). + * matching the private key. */ + @Nullable public abstract Principal[] getPrincipals(); /** diff --git a/android/webkit/CookieManager.java b/android/webkit/CookieManager.java index 89892930..ae6a2fd7 100644 --- a/android/webkit/CookieManager.java +++ b/android/webkit/CookieManager.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.net.WebAddress; @@ -116,7 +117,8 @@ public abstract class CookieManager { * HTTP response header * @param callback a callback to be executed when the cookie has been set */ - public abstract void setCookie(String url, String value, ValueCallback<Boolean> callback); + public abstract void setCookie(String url, String value, @Nullable ValueCallback<Boolean> + callback); /** * Gets the cookies for the given URL. @@ -175,7 +177,7 @@ public abstract class CookieManager { * method from a thread without a Looper. * @param callback a callback which is executed when the session cookies have been removed */ - public abstract void removeSessionCookies(ValueCallback<Boolean> callback); + public abstract void removeSessionCookies(@Nullable ValueCallback<Boolean> callback); /** * Removes all cookies. @@ -197,7 +199,7 @@ public abstract class CookieManager { * method from a thread without a Looper. * @param callback a callback which is executed when the cookies have been removed */ - public abstract void removeAllCookies(ValueCallback<Boolean> callback); + public abstract void removeAllCookies(@Nullable ValueCallback<Boolean> callback); /** * Gets whether there are stored cookies. diff --git a/android/webkit/FindActionModeCallback.java b/android/webkit/FindActionModeCallback.java index 71f85d70..e011d51c 100644 --- a/android/webkit/FindActionModeCallback.java +++ b/android/webkit/FindActionModeCallback.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.NonNull; import android.annotation.SystemApi; import android.content.Context; import android.content.res.Resources; @@ -69,7 +70,7 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, mActionMode.finish(); } - /* + /** * Place text in the text field so it can be searched for. Need to press * the find next or find previous button to find all of the matches. */ @@ -87,10 +88,12 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, mMatchesFound = false; } - /* - * Set the WebView to search. Must be non null. + /** + * Set the WebView to search. + * + * @param webView an implementation of WebView */ - public void setWebView(WebView webView) { + public void setWebView(@NonNull WebView webView) { if (null == webView) { throw new AssertionError("WebView supplied to " + "FindActionModeCallback cannot be null"); @@ -107,7 +110,7 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, } } - /* + /** * Move the highlight to the next match. * @param next If {@code true}, find the next match further down in the document. * If {@code false}, find the previous match, up in the document. @@ -130,7 +133,7 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, updateMatchesString(); } - /* + /** * Highlight all the instances of the string from mEditText in mWebView. */ public void findAll() { @@ -169,7 +172,7 @@ public class FindActionModeCallback implements ActionMode.Callback, TextWatcher, } } - /* + /** * Update the string which tells the user how many matches were found, and * which match is currently highlighted. */ diff --git a/android/webkit/MimeTypeMap.java b/android/webkit/MimeTypeMap.java index e172c02e..39874e82 100644 --- a/android/webkit/MimeTypeMap.java +++ b/android/webkit/MimeTypeMap.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.text.TextUtils; import libcore.net.MimeUtils; @@ -86,6 +87,7 @@ public class MimeTypeMap { * @param extension A file extension without the leading '.' * @return The MIME type for the given extension or {@code null} iff there is none. */ + @Nullable public String getMimeTypeFromExtension(String extension) { return MimeUtils.guessMimeTypeFromExtension(extension); } @@ -111,6 +113,7 @@ public class MimeTypeMap { * @param mimeType A MIME type (i.e. text/plain) * @return The extension for the given MIME type or {@code null} iff there is none. */ + @Nullable public String getExtensionFromMimeType(String mimeType) { return MimeUtils.guessExtensionFromMimeType(mimeType); } @@ -125,7 +128,7 @@ public class MimeTypeMap { * @param contentDisposition Content-disposition header given by the server. * @return The MIME type that should be used for this data. */ - /* package */ String remapGenericMimeType(String mimeType, String url, + /* package */ String remapGenericMimeType(@Nullable String mimeType, String url, String contentDisposition) { // If we have one of "generic" MIME types, try to deduce // the right MIME type from the file extension (if any): diff --git a/android/webkit/Plugin.java b/android/webkit/Plugin.java index 072e02aa..29a5edc3 100644 --- a/android/webkit/Plugin.java +++ b/android/webkit/Plugin.java @@ -16,12 +16,12 @@ package android.webkit; -import com.android.internal.R; - import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import com.android.internal.R; + /** * Represents a plugin (Java equivalent of the PluginPackageAndroid * C++ class in libs/WebKitLib/WebKit/WebCore/plugins/android/) @@ -32,13 +32,13 @@ import android.content.DialogInterface; */ @Deprecated public class Plugin { - /* + /** * @hide * @deprecated This interface was intended to be used by Gears. Since Gears was * deprecated, so is this class. */ public interface PreferencesClickHandler { - /* + /** * @hide * @deprecated This interface was intended to be used by Gears. Since Gears was * deprecated, so is this class. diff --git a/android/webkit/ServiceWorkerClient.java b/android/webkit/ServiceWorkerClient.java index b4964fd2..d6e8f36c 100644 --- a/android/webkit/ServiceWorkerClient.java +++ b/android/webkit/ServiceWorkerClient.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.Nullable; + /** * Base class for clients to capture Service Worker related callbacks, * see {@link ServiceWorkerController} for usage example. @@ -37,6 +39,7 @@ public class ServiceWorkerClient { * resource itself. * @see WebViewClient#shouldInterceptRequest(WebView, WebResourceRequest) */ + @Nullable public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { return null; } diff --git a/android/webkit/TokenBindingService.java b/android/webkit/TokenBindingService.java index 43565c23..b37e1b89 100644 --- a/android/webkit/TokenBindingService.java +++ b/android/webkit/TokenBindingService.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.net.Uri; @@ -84,31 +86,30 @@ public abstract class TokenBindingService { * The user can pass {@code null} if any algorithm is acceptable. * * @param origin The origin for the server. - * @param algorithm The list of algorithms. Can be {@code null}. An - * IllegalArgumentException is thrown if array is empty. + * @param algorithm The list of algorithms. An IllegalArgumentException is thrown if array is + * empty. * @param callback The callback that will be called when key is available. - * Cannot be {@code null}. */ public abstract void getKey(Uri origin, - String[] algorithm, - ValueCallback<TokenBindingKey> callback); + @Nullable String[] algorithm, + @NonNull ValueCallback<TokenBindingKey> callback); /** * Deletes specified key (for use when associated cookie is cleared). * * @param origin The origin of the server. * @param callback The callback that will be called when key is deleted. The * callback parameter (Boolean) will indicate if operation is - * successful or if failed. The callback can be {@code null}. + * successful or if failed. */ public abstract void deleteKey(Uri origin, - ValueCallback<Boolean> callback); + @Nullable ValueCallback<Boolean> callback); /** * Deletes all the keys (for use when cookies are cleared). * * @param callback The callback that will be called when keys are deleted. * The callback parameter (Boolean) will indicate if operation is - * successful or if failed. The callback can be {@code null}. + * successful or if failed. */ - public abstract void deleteAllKeys(ValueCallback<Boolean> callback); + public abstract void deleteAllKeys(@Nullable ValueCallback<Boolean> callback); } diff --git a/android/webkit/URLUtil.java b/android/webkit/URLUtil.java index c8bfb779..c2f121a1 100644 --- a/android/webkit/URLUtil.java +++ b/android/webkit/URLUtil.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.net.ParseException; import android.net.Uri; import android.net.WebAddress; @@ -300,8 +301,8 @@ public final class URLUtil { */ public static final String guessFileName( String url, - String contentDisposition, - String mimeType) { + @Nullable String contentDisposition, + @Nullable String mimeType) { String filename = null; String extension = null; @@ -388,7 +389,7 @@ public final class URLUtil { Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$", Pattern.CASE_INSENSITIVE); - /* + /** * Parse the Content-Disposition HTTP Header. The format of the header * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html * This header provides a filename for content that is going to be diff --git a/android/webkit/UrlInterceptHandler.java b/android/webkit/UrlInterceptHandler.java index aa5c6dc7..0a6e51f7 100644 --- a/android/webkit/UrlInterceptHandler.java +++ b/android/webkit/UrlInterceptHandler.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.webkit.CacheManager.CacheResult; import android.webkit.PluginData; @@ -35,14 +36,15 @@ public interface UrlInterceptHandler { * not interested. * * @param url URL string. - * @param headers The headers associated with the request. May be {@code null}. + * @param headers The headers associated with the request. * @return The CacheResult containing the surrogate response. * * @hide * @deprecated Do not use, this interface is deprecated. */ @Deprecated - public CacheResult service(String url, Map<String, String> headers); + @Nullable + CacheResult service(String url, @Nullable Map<String, String> headers); /** * Given an URL, returns the PluginData which contains the @@ -50,12 +52,13 @@ public interface UrlInterceptHandler { * not interested. * * @param url URL string. - * @param headers The headers associated with the request. May be {@code null}. + * @param headers The headers associated with the request. * @return The PluginData containing the surrogate response. * * @hide * @deprecated Do not use, this interface is deprecated. */ @Deprecated - public PluginData getPluginData(String url, Map<String, String> headers); + @Nullable + PluginData getPluginData(String url, @Nullable Map<String, String> headers); } diff --git a/android/webkit/UrlInterceptRegistry.java b/android/webkit/UrlInterceptRegistry.java index 67af2ad0..700d6d93 100644 --- a/android/webkit/UrlInterceptRegistry.java +++ b/android/webkit/UrlInterceptRegistry.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.webkit.CacheManager.CacheResult; import android.webkit.PluginData; import android.webkit.UrlInterceptHandler; @@ -121,6 +122,7 @@ public final class UrlInterceptRegistry { * deprecated, so is this class. */ @Deprecated + @Nullable public static synchronized CacheResult getSurrogate( String url, Map<String, String> headers) { if (urlInterceptDisabled()) { @@ -149,6 +151,7 @@ public final class UrlInterceptRegistry { * deprecated, so is this class. */ @Deprecated + @Nullable public static synchronized PluginData getPluginData( String url, Map<String, String> headers) { if (urlInterceptDisabled()) { diff --git a/android/webkit/WebBackForwardList.java b/android/webkit/WebBackForwardList.java index 3349b065..0c34e3c1 100644 --- a/android/webkit/WebBackForwardList.java +++ b/android/webkit/WebBackForwardList.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.Nullable; + import java.io.Serializable; /** @@ -29,6 +31,7 @@ public abstract class WebBackForwardList implements Cloneable, Serializable { * empty. * @return The current history item. */ + @Nullable public abstract WebHistoryItem getCurrentItem(); /** diff --git a/android/webkit/WebChromeClient.java b/android/webkit/WebChromeClient.java index 742daa91..4aa1c4a6 100644 --- a/android/webkit/WebChromeClient.java +++ b/android/webkit/WebChromeClient.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -383,6 +384,7 @@ public class WebChromeClient { * @return Bitmap The image to use as a default poster, or {@code null} if no such image is * available. */ + @Nullable public Bitmap getDefaultVideoPoster() { return null; } @@ -394,6 +396,7 @@ public class WebChromeClient { * * @return View The View to be displayed whilst the video is loading. */ + @Nullable public View getVideoLoadingProgressView() { return null; } @@ -452,6 +455,7 @@ public class WebChromeClient { * @return the Uris of selected file(s) or {@code null} if the resultCode indicates * activity canceled or any other error. */ + @Nullable public static Uri[] parseResult(int resultCode, Intent data) { return WebViewFactory.getProvider().getStatics().parseFileChooserResult(resultCode, data); } @@ -477,14 +481,16 @@ public class WebChromeClient { public abstract boolean isCaptureEnabled(); /** - * Returns the title to use for this file selector, or null. If {@code null} a default - * title should be used. + * Returns the title to use for this file selector. If {@code null} a default title should + * be used. */ + @Nullable public abstract CharSequence getTitle(); /** * The file name of a default selection if specified, or {@code null}. */ + @Nullable public abstract String getFilenameHint(); /** diff --git a/android/webkit/WebHistoryItem.java b/android/webkit/WebHistoryItem.java index 1591833e..74db039e 100644 --- a/android/webkit/WebHistoryItem.java +++ b/android/webkit/WebHistoryItem.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.graphics.Bitmap; @@ -70,6 +71,7 @@ public abstract class WebHistoryItem implements Cloneable { * Note: The VM ensures 32-bit atomic read/write operations so we don't have * to synchronize this method. */ + @Nullable public abstract Bitmap getFavicon(); /** diff --git a/android/webkit/WebMessage.java b/android/webkit/WebMessage.java index 7fe66dc8..bfc00e7a 100644 --- a/android/webkit/WebMessage.java +++ b/android/webkit/WebMessage.java @@ -16,6 +16,8 @@ package android.webkit; +import android.annotation.Nullable; + /** * The Java representation of the HTML5 PostMessage event. See * https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces @@ -56,6 +58,7 @@ public class WebMessage { * Returns the ports that are sent with the message, or {@code null} if no port * is sent. */ + @Nullable public WebMessagePort[] getPorts() { return mPorts; } diff --git a/android/webkit/WebResourceResponse.java b/android/webkit/WebResourceResponse.java index 80c43c14..7bc7b07d 100644 --- a/android/webkit/WebResourceResponse.java +++ b/android/webkit/WebResourceResponse.java @@ -16,12 +16,13 @@ package android.webkit; +import android.annotation.NonNull; +import android.annotation.SystemApi; + import java.io.InputStream; import java.io.StringBufferInputStream; import java.util.Map; -import android.annotation.SystemApi; - /** * Encapsulates a resource response. Applications can return an instance of this * class from {@link WebViewClient#shouldInterceptRequest} to provide a custom @@ -63,15 +64,15 @@ public class WebResourceResponse { * @param encoding the resource response's encoding * @param statusCode the status code needs to be in the ranges [100, 299], [400, 599]. * Causing a redirect by specifying a 3xx code is not supported. - * @param reasonPhrase the phrase describing the status code, for example "OK". Must be non-null - * and not empty. + * @param reasonPhrase the phrase describing the status code, for example "OK". Must be + * non-empty. * @param responseHeaders the resource response's headers represented as a mapping of header * name -> header value. * @param data the input stream that provides the resource response's data. Must not be a * StringBufferInputStream. */ public WebResourceResponse(String mimeType, String encoding, int statusCode, - String reasonPhrase, Map<String, String> responseHeaders, InputStream data) { + @NonNull String reasonPhrase, Map<String, String> responseHeaders, InputStream data) { this(mimeType, encoding, data); setStatusCodeAndReasonPhrase(statusCode, reasonPhrase); setResponseHeaders(responseHeaders); @@ -121,10 +122,10 @@ public class WebResourceResponse { * * @param statusCode the status code needs to be in the ranges [100, 299], [400, 599]. * Causing a redirect by specifying a 3xx code is not supported. - * @param reasonPhrase the phrase describing the status code, for example "OK". Must be non-null - * and not empty. + * @param reasonPhrase the phrase describing the status code, for example "OK". Must be + * non-empty. */ - public void setStatusCodeAndReasonPhrase(int statusCode, String reasonPhrase) { + public void setStatusCodeAndReasonPhrase(int statusCode, @NonNull String reasonPhrase) { checkImmutable(); if (statusCode < 100) throw new IllegalArgumentException("statusCode can't be less than 100."); diff --git a/android/webkit/WebSettings.java b/android/webkit/WebSettings.java index 22d8561d..203de9c2 100644 --- a/android/webkit/WebSettings.java +++ b/android/webkit/WebSettings.java @@ -17,6 +17,7 @@ package android.webkit; import android.annotation.IntDef; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.content.Context; @@ -1238,7 +1239,7 @@ public abstract class WebSettings { * * @param ua new user-agent string */ - public abstract void setUserAgentString(String ua); + public abstract void setUserAgentString(@Nullable String ua); /** * Gets the WebView's user-agent string. diff --git a/android/webkit/WebSyncManager.java b/android/webkit/WebSyncManager.java index 801be128..03b94e71 100644 --- a/android/webkit/WebSyncManager.java +++ b/android/webkit/WebSyncManager.java @@ -18,7 +18,7 @@ package android.webkit; import android.content.Context; -/* +/** * @deprecated The WebSyncManager no longer does anything. */ @Deprecated diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java index 202f2046..dfc81b2b 100644 --- a/android/webkit/WebView.java +++ b/android/webkit/WebView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,223 +16,3001 @@ package android.webkit; -import com.android.layoutlib.bridge.MockView; - +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.Widget; 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; /** - * 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. + * <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> * */ -public class WebView extends MockView { +// 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; + /** + * 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:"; /** - * Construct a new WebView with a Context object. - * @param context A Context object used to access application assets. + * 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 */ public WebView(Context context) { this(context, null); } /** - * 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. + * 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 */ public WebView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.webViewStyle); } /** - * 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. + * 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. */ - public WebView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public WebView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - - // START FAKE PUBLIC METHODS - + + /** + * 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 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() { - return false; + // The old implementation defaulted to true, so return true for consistency + return true; } + /** + * 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) { - return null; + checkThread(); + return mProvider.getHttpAuthUsernamePassword(host, realm); } + /** + * 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); + } } - public void loadData(String data, String mimeType, String encoding) { + /** + * 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 loadDataWithBaseURL(String baseUrl, String data, - String mimeType, String encoding, String failUrl) { + /** + * 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); } + /** + * 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() { - return false; + checkThread(); + return mProvider.canGoBack(); } + /** + * 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() { - return false; + checkThread(); + return mProvider.canGoForward(); } + /** + * 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) { - return false; + checkThread(); + return mProvider.canGoBackOrForward(steps); } + /** + * 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) { - return false; + checkThread(); + return mProvider.pageUp(top); } - + + /** + * 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) { - return false; + 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); + } + + /** + * 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() { - return null; + 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"); } + /** + * 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() { - return 0; + checkThread(); + return mProvider.getScale(); } + /** + * 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(); } - public void requestFocusNodeHref(Message hrefMsg) { + /** + * 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); } + /** + * 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() { - return null; + checkThread(); + return mProvider.getUrl(); } + /** + * 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() { - return null; + checkThread(); + return mProvider.getTitle(); } + /** + * 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() { - return null; + checkThread(); + return mProvider.getFavicon(); } + /** + * 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() { - return 0; + checkThread(); + return mProvider.getProgress(); } - + + /** + * Gets the height of the HTML content. + * + * @return the height of the HTML content + */ + @ViewDebug.ExportedProperty(category = "webview") public int getContentHeight() { - return 0; + 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(); } + /** + * 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(); } - public void clearCache() { + /** + * 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); } + /** + * 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) { - return null; + // 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(); } + /** + * 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); } - public void addJavascriptInterface(Object obj, String interfaceName) { + /** + * 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 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() { - return null; + checkThread(); + return mProvider.getZoomControls(); } + /** + * 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() { - return false; + checkThread(); + return mProvider.zoomIn(); } + /** + * Performs zoom out in this WebView. + * + * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes + */ public boolean zoomOut() { - return false; + 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()); } } diff --git a/android/webkit/WebViewClient.java b/android/webkit/WebViewClient.java index af7026d9..c5b64eb8 100644 --- a/android/webkit/WebViewClient.java +++ b/android/webkit/WebViewClient.java @@ -17,6 +17,7 @@ package android.webkit; import android.annotation.IntDef; +import android.annotation.Nullable; import android.graphics.Bitmap; import android.net.http.SslError; import android.os.Message; @@ -167,6 +168,7 @@ public class WebViewClient { * shouldInterceptRequest(WebView, WebResourceRequest)} instead. */ @Deprecated + @Nullable public WebResourceResponse shouldInterceptRequest(WebView view, String url) { return null; @@ -191,6 +193,7 @@ public class WebViewClient { * response information or {@code null} if the WebView should load the * resource itself. */ + @Nullable public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { return shouldInterceptRequest(view, request.getUrl().toString()); @@ -496,7 +499,7 @@ public class WebViewClient { * @param args Authenticator specific arguments used to log in the user. */ public void onReceivedLoginRequest(WebView view, String realm, - String account, String args) { + @Nullable String account, String args) { } /** diff --git a/android/webkit/WebViewDatabase.java b/android/webkit/WebViewDatabase.java index de75d5d0..f6166c58 100644 --- a/android/webkit/WebViewDatabase.java +++ b/android/webkit/WebViewDatabase.java @@ -16,6 +16,7 @@ package android.webkit; +import android.annotation.Nullable; import android.content.Context; /** @@ -135,6 +136,7 @@ public abstract class WebViewDatabase { * @see #hasHttpAuthUsernamePassword * @see #clearHttpAuthUsernamePassword */ + @Nullable public abstract String[] getHttpAuthUsernamePassword(String host, String realm); /** diff --git a/android/webkit/WebViewFactory.java b/android/webkit/WebViewFactory.java index 7c4154f5..95cb4549 100644 --- a/android/webkit/WebViewFactory.java +++ b/android/webkit/WebViewFactory.java @@ -51,9 +51,6 @@ public final class WebViewFactory { private static final String CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"; - private static final String NULL_WEBVIEW_FACTORY = - "com.android.webview.nullwebview.NullWebViewFactoryProvider"; - public static final String CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY = "persist.sys.webview.vmsize"; @@ -66,6 +63,7 @@ public final class WebViewFactory { private static WebViewFactoryProvider sProviderInstance; private static final Object sProviderLock = new Object(); private static PackageInfo sPackageInfo; + private static Boolean sWebViewSupported; // Error codes for loadWebViewNativeLibraryFromPackage public static final int LIBLOAD_SUCCESS = 0; @@ -105,6 +103,16 @@ public final class WebViewFactory { public MissingWebViewPackageException(Exception e) { super(e); } } + private static boolean isWebViewSupported() { + // No lock; this is a benign race as Boolean's state is final and the PackageManager call + // will always return the same value. + if (sWebViewSupported == null) { + sWebViewSupported = AppGlobals.getInitialApplication().getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_WEBVIEW); + } + return sWebViewSupported; + } + /** * @hide */ @@ -135,6 +143,10 @@ public final class WebViewFactory { */ public static int loadWebViewNativeLibraryFromPackage(String packageName, ClassLoader clazzLoader) { + if (!isWebViewSupported()) { + return LIBLOAD_WRONG_PACKAGE_NAME; + } + WebViewProviderResponse response = null; try { response = getUpdateService().waitForAndGetProvider(); @@ -188,6 +200,11 @@ public final class WebViewFactory { "For security reasons, WebView is not allowed in privileged processes"); } + if (!isWebViewSupported()) { + // Device doesn't support WebView; don't try to load it, just throw. + throw new UnsupportedOperationException(); + } + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()"); try { @@ -410,15 +427,6 @@ public final class WebViewFactory { Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); } } catch (MissingWebViewPackageException e) { - // If the package doesn't exist, then try loading the null WebView instead. - // If that succeeds, then this is a device without WebView support; if it fails then - // swallow the failure, complain that the real WebView is missing and rethrow the - // original exception. - try { - return (Class<WebViewFactoryProvider>) Class.forName(NULL_WEBVIEW_FACTORY); - } catch (ClassNotFoundException e2) { - // Ignore. - } Log.e(LOGTAG, "Chromium WebView package does not exist", e); throw new AndroidRuntimeException(e); } @@ -446,13 +454,13 @@ public final class WebViewFactory { // waiting on relro creation. if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { if (DEBUG) Log.v(LOGTAG, "Create 32 bit relro"); - WebViewLibraryLoader.createRelroFile(false /* is64Bit */, nativeLibraryPaths); + WebViewLibraryLoader.createRelroFile(false /* is64Bit */, nativeLibraryPaths[0]); numRelros++; } if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { if (DEBUG) Log.v(LOGTAG, "Create 64 bit relro"); - WebViewLibraryLoader.createRelroFile(true /* is64Bit */, nativeLibraryPaths); + WebViewLibraryLoader.createRelroFile(true /* is64Bit */, nativeLibraryPaths[1]); numRelros++; } return numRelros; @@ -463,7 +471,7 @@ public final class WebViewFactory { */ public static int onWebViewProviderChanged(PackageInfo packageInfo) { String[] nativeLibs = null; - String originalSourceDir = packageInfo.applicationInfo.sourceDir; + ApplicationInfo originalAppInfo = new ApplicationInfo(packageInfo.applicationInfo); try { fixupStubApplicationInfo(packageInfo.applicationInfo, AppGlobals.getInitialApplication().getPackageManager()); @@ -474,7 +482,7 @@ public final class WebViewFactory { Log.e(LOGTAG, "error preparing webview native library", t); } - WebViewZygote.onWebViewProviderChanged(packageInfo, originalSourceDir); + WebViewZygote.onWebViewProviderChanged(packageInfo, originalAppInfo); return prepareWebViewInSystemServer(nativeLibs); } @@ -483,6 +491,15 @@ public final class WebViewFactory { /** @hide */ public static IWebViewUpdateService getUpdateService() { + if (isWebViewSupported()) { + return getUpdateServiceUnchecked(); + } else { + return null; + } + } + + /** @hide */ + static IWebViewUpdateService getUpdateServiceUnchecked() { return IWebViewUpdateService.Stub.asInterface( ServiceManager.getService(WEBVIEW_UPDATE_SERVICE_NAME)); } diff --git a/android/webkit/WebViewLibraryLoader.java b/android/webkit/WebViewLibraryLoader.java index 6f9e8ece..fa1a3907 100644 --- a/android/webkit/WebViewLibraryLoader.java +++ b/android/webkit/WebViewLibraryLoader.java @@ -62,25 +62,23 @@ class WebViewLibraryLoader { boolean result = false; boolean is64Bit = VMRuntime.getRuntime().is64Bit(); try { - if (args.length != 2 || args[0] == null || args[1] == null) { + if (args.length != 1 || args[0] == null) { Log.e(LOGTAG, "Invalid RelroFileCreator args: " + Arrays.toString(args)); return; } - Log.v(LOGTAG, "RelroFileCreator (64bit = " + is64Bit + "), " - + " 32-bit lib: " + args[0] + ", 64-bit lib: " + args[1]); + Log.v(LOGTAG, "RelroFileCreator (64bit = " + is64Bit + "), lib: " + args[0]); if (!sAddressSpaceReserved) { Log.e(LOGTAG, "can't create relro file; address space not reserved"); return; } - result = nativeCreateRelroFile(args[0] /* path32 */, - args[1] /* path64 */, - CHROMIUM_WEBVIEW_NATIVE_RELRO_32, - CHROMIUM_WEBVIEW_NATIVE_RELRO_64); + result = nativeCreateRelroFile(args[0] /* path */, + is64Bit ? CHROMIUM_WEBVIEW_NATIVE_RELRO_64 : + CHROMIUM_WEBVIEW_NATIVE_RELRO_32); if (result && DEBUG) Log.v(LOGTAG, "created relro file"); } finally { // We must do our best to always notify the update service, even if something fails. try { - WebViewFactory.getUpdateService().notifyRelroCreationCompleted(); + WebViewFactory.getUpdateServiceUnchecked().notifyRelroCreationCompleted(); } catch (RemoteException e) { Log.e(LOGTAG, "error notifying update service", e); } @@ -96,7 +94,7 @@ class WebViewLibraryLoader { /** * Create a single relro file by invoking an isolated process that to do the actual work. */ - static void createRelroFile(final boolean is64Bit, String[] nativeLibraryPaths) { + static void createRelroFile(final boolean is64Bit, String nativeLibraryPath) { final String abi = is64Bit ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0]; @@ -114,13 +112,12 @@ class WebViewLibraryLoader { }; try { - if (nativeLibraryPaths == null - || nativeLibraryPaths[0] == null || nativeLibraryPaths[1] == null) { + if (nativeLibraryPath == null) { throw new IllegalArgumentException( "Native library paths to the WebView RelRo process must not be null!"); } int pid = LocalServices.getService(ActivityManagerInternal.class).startIsolatedProcess( - RelroFileCreator.class.getName(), nativeLibraryPaths, + RelroFileCreator.class.getName(), new String[] { nativeLibraryPath }, "WebViewLoader-" + abi, abi, Process.SHARED_RELRO_UID, crashHandler); if (pid <= 0) throw new Exception("Failed to start the relro file creator process"); } catch (Throwable t) { @@ -217,8 +214,9 @@ class WebViewLibraryLoader { final String libraryFileName = WebViewFactory.getWebViewLibrary(packageInfo.applicationInfo); - int result = nativeLoadWithRelroFile(libraryFileName, CHROMIUM_WEBVIEW_NATIVE_RELRO_32, - CHROMIUM_WEBVIEW_NATIVE_RELRO_64, clazzLoader); + String relroPath = VMRuntime.getRuntime().is64Bit() ? CHROMIUM_WEBVIEW_NATIVE_RELRO_64 : + CHROMIUM_WEBVIEW_NATIVE_RELRO_32; + int result = nativeLoadWithRelroFile(libraryFileName, relroPath, clazzLoader); if (result != WebViewFactory.LIBLOAD_SUCCESS) { Log.w(LOGTAG, "failed to load with relro file, proceeding without"); } else if (DEBUG) { @@ -313,8 +311,6 @@ class WebViewLibraryLoader { } static native boolean nativeReserveAddressSpace(long addressSpaceToReserve); - static native boolean nativeCreateRelroFile(String lib32, String lib64, - String relro32, String relro64); - static native int nativeLoadWithRelroFile(String lib, String relro32, String relro64, - ClassLoader clazzLoader); + static native boolean nativeCreateRelroFile(String lib, String relro); + static native int nativeLoadWithRelroFile(String lib, String relro, ClassLoader clazzLoader); } diff --git a/android/webkit/WebViewUpdateService.java b/android/webkit/WebViewUpdateService.java index 2f7d6854..629891cc 100644 --- a/android/webkit/WebViewUpdateService.java +++ b/android/webkit/WebViewUpdateService.java @@ -31,8 +31,12 @@ public final class WebViewUpdateService { * Fetch all packages that could potentially implement WebView. */ public static WebViewProviderInfo[] getAllWebViewPackages() { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return new WebViewProviderInfo[0]; + } try { - return getUpdateService().getAllWebViewPackages(); + return service.getAllWebViewPackages(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -42,8 +46,12 @@ public final class WebViewUpdateService { * Fetch all packages that could potentially implement WebView and are currently valid. */ public static WebViewProviderInfo[] getValidWebViewPackages() { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return new WebViewProviderInfo[0]; + } try { - return getUpdateService().getValidWebViewPackages(); + return service.getValidWebViewPackages(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -53,8 +61,12 @@ public final class WebViewUpdateService { * Used by DevelopmentSetting to get the name of the WebView provider currently in use. */ public static String getCurrentWebViewPackageName() { + IWebViewUpdateService service = getUpdateService(); + if (service == null) { + return null; + } try { - return getUpdateService().getCurrentWebViewPackageName(); + return service.getCurrentWebViewPackageName(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/android/webkit/WebViewZygote.java b/android/webkit/WebViewZygote.java index 6e65c7a1..db60ad8d 100644 --- a/android/webkit/WebViewZygote.java +++ b/android/webkit/WebViewZygote.java @@ -17,6 +17,7 @@ package android.webkit; import android.app.LoadedApk; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.os.Build; import android.os.SystemService; @@ -67,11 +68,11 @@ public class WebViewZygote { private static PackageInfo sPackage; /** - * Cache key for the selected WebView package's classloader. This is set from + * Original ApplicationInfo for the selected WebView package before stub fixup. This is set from * #onWebViewProviderChanged(). */ @GuardedBy("sLock") - private static String sPackageCacheKey; + private static ApplicationInfo sPackageOriginalAppInfo; /** * Flag for whether multi-process WebView is enabled. If this is {@code false}, the zygote @@ -125,10 +126,11 @@ public class WebViewZygote { } } - public static void onWebViewProviderChanged(PackageInfo packageInfo, String cacheKey) { + public static void onWebViewProviderChanged(PackageInfo packageInfo, + ApplicationInfo originalAppInfo) { synchronized (sLock) { sPackage = packageInfo; - sPackageCacheKey = cacheKey; + sPackageOriginalAppInfo = originalAppInfo; // If multi-process is not enabled, then do not start the zygote service. if (!sMultiprocessEnabled) { @@ -217,10 +219,17 @@ public class WebViewZygote { final String zip = (zipPaths.size() == 1) ? zipPaths.get(0) : TextUtils.join(File.pathSeparator, zipPaths); + // In the case where the ApplicationInfo has been modified by the stub WebView, + // we need to use the original ApplicationInfo to determine what the original classpath + // would have been to use as a cache key. + LoadedApk.makePaths(null, false, sPackageOriginalAppInfo, zipPaths, null); + final String cacheKey = (zipPaths.size() == 1) ? zipPaths.get(0) : + TextUtils.join(File.pathSeparator, zipPaths); + ZygoteProcess.waitForConnectionToZygote(WEBVIEW_ZYGOTE_SOCKET); Log.d(LOGTAG, "Preloading package " + zip + " " + librarySearchPath); - sZygote.preloadPackageForAbi(zip, librarySearchPath, sPackageCacheKey, + sZygote.preloadPackageForAbi(zip, librarySearchPath, cacheKey, Build.SUPPORTED_ABIS[0]); } catch (Exception e) { Log.e(LOGTAG, "Error connecting to " + serviceName, e); diff --git a/android/widget/EditTextBackspacePerfTest.java b/android/widget/EditTextBackspacePerfTest.java index 40b56f4a..d219d3a2 100644 --- a/android/widget/EditTextBackspacePerfTest.java +++ b/android/widget/EditTextBackspacePerfTest.java @@ -16,31 +16,26 @@ package android.widget; -import android.app.Activity; -import android.os.Bundle; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.perftests.utils.StubActivity; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; import android.text.Selection; import android.view.KeyEvent; import android.view.View.MeasureSpec; import android.view.ViewGroup; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.LargeTest; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Locale; - import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized.Parameters; import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; @LargeTest @RunWith(Parameterized.class) diff --git a/android/widget/EditTextCursorMovementPerfTest.java b/android/widget/EditTextCursorMovementPerfTest.java index b100acba..b6cf7d3f 100644 --- a/android/widget/EditTextCursorMovementPerfTest.java +++ b/android/widget/EditTextCursorMovementPerfTest.java @@ -16,31 +16,26 @@ package android.widget; -import android.app.Activity; -import android.os.Bundle; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.perftests.utils.StubActivity; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; import android.text.Selection; import android.view.KeyEvent; import android.view.View.MeasureSpec; import android.view.ViewGroup; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.LargeTest; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Locale; - import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized.Parameters; import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; @LargeTest @RunWith(Parameterized.class) diff --git a/android/widget/Editor.java b/android/widget/Editor.java index 0f617242..afd11881 100644 --- a/android/widget/Editor.java +++ b/android/widget/Editor.java @@ -119,6 +119,7 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.Preconditions; import com.android.internal.widget.EditableInputConnection; +import com.android.internal.widget.Magnifier; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -138,6 +139,9 @@ import java.util.List; public class Editor { private static final String TAG = "Editor"; private static final boolean DEBUG_UNDO = false; + // Specifies whether to use or not the magnifier when pressing the insertion or selection + // handles. + private static final boolean FLAG_USE_MAGNIFIER = true; static final int BLINK = 500; private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; @@ -161,6 +165,17 @@ 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; + @IntDef({MagnifierHandleTrigger.SELECTION_START, + MagnifierHandleTrigger.SELECTION_END, + MagnifierHandleTrigger.INSERTION}) + @Retention(RetentionPolicy.SOURCE) + private @interface MagnifierHandleTrigger { + int INSERTION = 0; + int SELECTION_START = 1; + int SELECTION_END = 2; + } + // Each Editor manages its own undo stack. private final UndoManager mUndoManager = new UndoManager(); private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this); @@ -179,6 +194,8 @@ public class Editor { private final boolean mHapticTextHandleEnabled; + private final Magnifier mMagnifier; + // Used to highlight a word when it is corrected by the IME private CorrectionHighlighter mCorrectionHighlighter; @@ -250,7 +267,7 @@ public class Editor { SuggestionRangeSpan mSuggestionRangeSpan; private Runnable mShowSuggestionRunnable; - Drawable mCursorDrawable = null; + Drawable mDrawableForCursor = null; private Drawable mSelectHandleLeft; private Drawable mSelectHandleRight; @@ -325,6 +342,8 @@ public class Editor { mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this); mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean( com.android.internal.R.bool.config_enableHapticTextHandle); + + mMagnifier = FLAG_USE_MAGNIFIER ? new Magnifier(mTextView) : null; } ParcelableParcel saveInstanceState() { @@ -1678,7 +1697,7 @@ public class Editor { mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); } - if (highlight != null && selectionStart == selectionEnd && mCursorDrawable != null) { + if (highlight != null && selectionStart == selectionEnd && mDrawableForCursor != null) { drawCursor(canvas, cursorOffsetVertical); // Rely on the drawable entirely, do not draw the cursor line. // Has to be done after the IMM related code above which relies on the highlight. @@ -1873,8 +1892,8 @@ public class Editor { private void drawCursor(Canvas canvas, int cursorOffsetVertical) { final boolean translate = cursorOffsetVertical != 0; if (translate) canvas.translate(0, cursorOffsetVertical); - if (mCursorDrawable != null) { - mCursorDrawable.draw(canvas); + if (mDrawableForCursor != null) { + mDrawableForCursor.draw(canvas); } if (translate) canvas.translate(0, -cursorOffsetVertical); } @@ -1933,7 +1952,7 @@ public class Editor { void updateCursorPosition() { if (mTextView.mCursorDrawableRes == 0) { - mCursorDrawable = null; + mDrawableForCursor = null; return; } @@ -2314,17 +2333,17 @@ public class Editor { @VisibleForTesting @Nullable public Drawable getCursorDrawable() { - return mCursorDrawable; + return mDrawableForCursor; } private void updateCursorPosition(int top, int bottom, float horizontal) { - if (mCursorDrawable == null) { - mCursorDrawable = mTextView.getContext().getDrawable( + if (mDrawableForCursor == null) { + mDrawableForCursor = mTextView.getContext().getDrawable( mTextView.mCursorDrawableRes); } - final int left = clampHorizontalPosition(mCursorDrawable, horizontal); - final int width = mCursorDrawable.getIntrinsicWidth(); - mCursorDrawable.setBounds(left, top - mTempRect.top, left + width, + final int left = clampHorizontalPosition(mDrawableForCursor, horizontal); + final int width = mDrawableForCursor.getIntrinsicWidth(); + mDrawableForCursor.setBounds(left, top - mTempRect.top, left + width, bottom + mTempRect.bottom); } @@ -4353,6 +4372,9 @@ public class Editor { protected abstract void updatePosition(float x, float y, boolean fromTouchScreen); + @MagnifierHandleTrigger + protected abstract int getMagnifierHandleTrigger(); + protected boolean isAtRtlRun(@NonNull Layout layout, int offset) { return layout.isRtlCharAt(offset); } @@ -4490,6 +4512,53 @@ public class Editor { return 0; } + protected final void showMagnifier() { + if (mMagnifier == null) { + return; + } + + final int trigger = getMagnifierHandleTrigger(); + final int offset; + switch (trigger) { + case MagnifierHandleTrigger.INSERTION: // Fall through. + case MagnifierHandleTrigger.SELECTION_START: + offset = mTextView.getSelectionStart(); + break; + case MagnifierHandleTrigger.SELECTION_END: + offset = mTextView.getSelectionEnd(); + break; + default: + offset = -1; + break; + } + + if (offset == -1) { + dismissMagnifier(); + } + + final Layout layout = mTextView.getLayout(); + final int lineNumber = layout.getLineForOffset(offset); + // Horizontally snap to character offset. + final float xPosInView = getHorizontal(mTextView.getLayout(), offset); + // 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]; + + mMagnifier.show(centerXOnScreen, centerYOnScreen, MAGNIFIER_ZOOM); + } + + protected final void dismissMagnifier() { + if (mMagnifier != null) { + mMagnifier.dismiss(); + } + } + @Override public boolean onTouchEvent(MotionEvent ev) { updateFloatingToolbarVisibility(ev); @@ -4542,10 +4611,7 @@ public class Editor { case MotionEvent.ACTION_UP: filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)); - mIsDragging = false; - updateDrawable(); - break; - + // Fall through. case MotionEvent.ACTION_CANCEL: mIsDragging = false; updateDrawable(); @@ -4646,9 +4712,9 @@ public class Editor { @Override protected int getCursorOffset() { int offset = super.getCursorOffset(); - if (mCursorDrawable != null) { - mCursorDrawable.getPadding(mTempRect); - offset += (mCursorDrawable.getIntrinsicWidth() + if (mDrawableForCursor != null) { + mDrawableForCursor.getPadding(mTempRect); + offset += (mDrawableForCursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2; } return offset; @@ -4656,9 +4722,9 @@ public class Editor { @Override int getCursorHorizontalPosition(Layout layout, int offset) { - if (mCursorDrawable != null) { + if (mDrawableForCursor != null) { final float horizontal = getHorizontal(layout, offset); - return clampHorizontalPosition(mCursorDrawable, horizontal) + mTempRect.left; + return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left; } return super.getCursorHorizontalPosition(layout, offset); } @@ -4671,6 +4737,11 @@ public class Editor { case MotionEvent.ACTION_DOWN: mDownPositionX = ev.getRawX(); mDownPositionY = ev.getRawY(); + showMagnifier(); + break; + + case MotionEvent.ACTION_MOVE: + showMagnifier(); break; case MotionEvent.ACTION_UP: @@ -4696,11 +4767,10 @@ public class Editor { mTextActionMode.invalidateContentRect(); } } - hideAfterDelay(); - break; - + // Fall through. case MotionEvent.ACTION_CANCEL: hideAfterDelay(); + dismissMagnifier(); break; default: @@ -4751,6 +4821,12 @@ public class Editor { super.onDetached(); removeHiderCallback(); } + + @Override + @MagnifierHandleTrigger + protected int getMagnifierHandleTrigger() { + return MagnifierHandleTrigger.INSERTION; + } } @Retention(RetentionPolicy.SOURCE) @@ -5009,12 +5085,26 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent event) { boolean superResult = super.onTouchEvent(event); - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - // Reset the touch word offset and x value when the user - // re-engages the handle. - mTouchWordDelta = 0.0f; - mPrevX = UNSET_X_VALUE; + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + // Reset the touch word offset and x value when the user + // re-engages the handle. + mTouchWordDelta = 0.0f; + mPrevX = UNSET_X_VALUE; + showMagnifier(); + break; + + case MotionEvent.ACTION_MOVE: + showMagnifier(); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + dismissMagnifier(); + break; } + return superResult; } @@ -5110,6 +5200,13 @@ public class Editor { return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset; } } + + @MagnifierHandleTrigger + protected int getMagnifierHandleTrigger() { + return isStartHandle() + ? MagnifierHandleTrigger.SELECTION_START + : MagnifierHandleTrigger.SELECTION_END; + } } private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) { diff --git a/android/widget/PopupWindow.java b/android/widget/PopupWindow.java index bf25915d..23ebadb3 100644 --- a/android/widget/PopupWindow.java +++ b/android/widget/PopupWindow.java @@ -2296,8 +2296,8 @@ public class PopupWindow { } /** @hide */ - protected final void detachFromAnchor() { - final View anchor = mAnchor != null ? mAnchor.get() : null; + protected void detachFromAnchor() { + final View anchor = getAnchor(); if (anchor != null) { final ViewTreeObserver vto = anchor.getViewTreeObserver(); vto.removeOnScrollChangedListener(mOnScrollChangedListener); @@ -2316,7 +2316,7 @@ public class PopupWindow { } /** @hide */ - protected final void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { + protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { detachFromAnchor(); final ViewTreeObserver vto = anchor.getViewTreeObserver(); @@ -2339,6 +2339,11 @@ public class PopupWindow { mAnchoredGravity = gravity; } + /** @hide */ + protected @Nullable View getAnchor() { + return mAnchor != null ? mAnchor.get() : null; + } + private void alignToAnchor() { final View anchor = mAnchor != null ? mAnchor.get() : null; if (anchor != null && anchor.isAttachedToWindow() && hasDecorView()) { diff --git a/android/widget/RemoteViews.java b/android/widget/RemoteViews.java index bc85fadb..1b26f8e2 100644 --- a/android/widget/RemoteViews.java +++ b/android/widget/RemoteViews.java @@ -16,9 +16,11 @@ package android.widget; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + import android.annotation.ColorInt; import android.annotation.DimenRes; -import android.app.ActivityManager.StackId; +import android.annotation.NonNull; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Application; @@ -107,9 +109,9 @@ public class RemoteViews implements Parcelable, Filter { // The unique identifiers for each custom {@link Action}. private static final int SET_ON_CLICK_PENDING_INTENT_TAG = 1; private static final int REFLECTION_ACTION_TAG = 2; - private static final int SET_DRAWABLE_PARAMETERS_TAG = 3; + private static final int SET_DRAWABLE_TINT_TAG = 3; private static final int VIEW_GROUP_ACTION_ADD_TAG = 4; - private static final int SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG = 5; + private static final int VIEW_CONTENT_NAVIGATION_TAG = 5; private static final int SET_EMPTY_VIEW_ACTION_TAG = 6; private static final int VIEW_GROUP_ACTION_REMOVE_TAG = 7; private static final int SET_PENDING_INTENT_TEMPLATE_TAG = 8; @@ -120,7 +122,6 @@ public class RemoteViews implements Parcelable, Filter { private static final int TEXT_VIEW_SIZE_ACTION_TAG = 13; private static final int VIEW_PADDING_ACTION_TAG = 14; private static final int SET_REMOTE_VIEW_ADAPTER_LIST_TAG = 15; - private static final int TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG = 17; private static final int SET_REMOTE_INPUTS_ACTION_TAG = 18; private static final int LAYOUT_PARAM_ACTION_TAG = 19; private static final int OVERRIDE_TEXT_COLORS_TAG = 20; @@ -324,11 +325,11 @@ public class RemoteViews implements Parcelable, Filter { public boolean onClickHandler(View view, PendingIntent pendingIntent, Intent fillInIntent) { - return onClickHandler(view, pendingIntent, fillInIntent, StackId.INVALID_STACK_ID); + return onClickHandler(view, pendingIntent, fillInIntent, WINDOWING_MODE_UNDEFINED); } public boolean onClickHandler(View view, PendingIntent pendingIntent, - Intent fillInIntent, int launchStackId) { + Intent fillInIntent, int windowingMode) { try { // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT? Context context = view.getContext(); @@ -339,8 +340,8 @@ public class RemoteViews implements Parcelable, Filter { opts = ActivityOptions.makeBasic(); } - if (launchStackId != StackId.INVALID_STACK_ID) { - opts.setLaunchStackId(launchStackId); + if (windowingMode != WINDOWING_MODE_UNDEFINED) { + opts.setLaunchWindowingMode(windowingMode); } context.startIntentSender( pendingIntent.getIntentSender(), fillInIntent, @@ -388,10 +389,10 @@ public class RemoteViews implements Parcelable, Filter { return MERGE_REPLACE; } - public abstract String getActionName(); + public abstract int getActionTag(); public String getUniqueKey() { - return (getActionName() + viewId); + return (getActionTag() + "_" + viewId); } /** @@ -423,8 +424,8 @@ public class RemoteViews implements Parcelable, Filter { */ private static abstract class RuntimeAction extends Action { @Override - public final String getActionName() { - return "RuntimeAction"; + public final int getActionTag() { + return 0; } @Override @@ -513,7 +514,6 @@ public class RemoteViews implements Parcelable, Filter { } private class SetEmptyView extends Action { - int viewId; int emptyViewId; SetEmptyView(int viewId, int emptyViewId) { @@ -527,7 +527,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel out, int flags) { - out.writeInt(SET_EMPTY_VIEW_ACTION_TAG); out.writeInt(this.viewId); out.writeInt(this.emptyViewId); } @@ -545,8 +544,9 @@ public class RemoteViews implements Parcelable, Filter { adapterView.setEmptyView(emptyView); } - public String getActionName() { - return "SetEmptyView"; + @Override + public int getActionTag() { + return SET_EMPTY_VIEW_ACTION_TAG; } } @@ -558,13 +558,12 @@ public class RemoteViews implements Parcelable, Filter { public SetOnClickFillInIntent(Parcel parcel) { viewId = parcel.readInt(); - fillInIntent = Intent.CREATOR.createFromParcel(parcel); + fillInIntent = parcel.readTypedObject(Intent.CREATOR); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_ON_CLICK_FILL_IN_INTENT_TAG); dest.writeInt(viewId); - fillInIntent.writeToParcel(dest, 0 /* no flags */); + dest.writeTypedObject(fillInIntent, 0 /* no flags */); } @Override @@ -623,8 +622,9 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { - return "SetOnClickFillInIntent"; + @Override + public int getActionTag() { + return SET_ON_CLICK_FILL_IN_INTENT_TAG; } Intent fillInIntent; @@ -642,9 +642,8 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_PENDING_INTENT_TEMPLATE_TAG); dest.writeInt(viewId); - pendingIntentTemplate.writeToParcel(dest, 0 /* no flags */); + PendingIntent.writePendingIntentOrNullToParcel(pendingIntentTemplate, dest); } @Override @@ -698,8 +697,9 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { - return "SetPendingIntentTemplate"; + @Override + public int getActionTag() { + return SET_PENDING_INTENT_TEMPLATE_TAG; } PendingIntent pendingIntentTemplate; @@ -715,30 +715,13 @@ public class RemoteViews implements Parcelable, Filter { public SetRemoteViewsAdapterList(Parcel parcel) { viewId = parcel.readInt(); viewTypeCount = parcel.readInt(); - int count = parcel.readInt(); - list = new ArrayList<RemoteViews>(); - - for (int i = 0; i < count; i++) { - RemoteViews rv = RemoteViews.CREATOR.createFromParcel(parcel); - list.add(rv); - } + list = parcel.createTypedArrayList(RemoteViews.CREATOR); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_REMOTE_VIEW_ADAPTER_LIST_TAG); dest.writeInt(viewId); dest.writeInt(viewTypeCount); - - if (list == null || list.size() == 0) { - dest.writeInt(0); - } else { - int count = list.size(); - dest.writeInt(count); - for (int i = 0; i < count; i++) { - RemoteViews rv = list.get(i); - rv.writeToParcel(dest, flags); - } - } + dest.writeTypedList(list, flags); } @Override @@ -778,8 +761,9 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { - return "SetRemoteViewsAdapterList"; + @Override + public int getActionTag() { + return SET_REMOTE_VIEW_ADAPTER_LIST_TAG; } int viewTypeCount; @@ -794,13 +778,12 @@ public class RemoteViews implements Parcelable, Filter { public SetRemoteViewsAdapterIntent(Parcel parcel) { viewId = parcel.readInt(); - intent = Intent.CREATOR.createFromParcel(parcel); + intent = parcel.readTypedObject(Intent.CREATOR); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_REMOTE_VIEW_ADAPTER_INTENT_TAG); dest.writeInt(viewId); - intent.writeToParcel(dest, flags); + dest.writeTypedObject(intent, flags); } @Override @@ -844,8 +827,9 @@ public class RemoteViews implements Parcelable, Filter { return copy; } - public String getActionName() { - return "SetRemoteViewsAdapterIntent"; + @Override + public int getActionTag() { + return SET_REMOTE_VIEW_ADAPTER_INTENT_TAG; } Intent intent; @@ -865,22 +849,12 @@ public class RemoteViews implements Parcelable, Filter { public SetOnClickPendingIntent(Parcel parcel) { viewId = parcel.readInt(); - - // We check a flag to determine if the parcel contains a PendingIntent. - if (parcel.readInt() != 0) { - pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); - } + pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_ON_CLICK_PENDING_INTENT_TAG); dest.writeInt(viewId); - - // We use a flag to indicate whether the parcel contains a valid object. - dest.writeInt(pendingIntent != null ? 1 : 0); - if (pendingIntent != null) { - pendingIntent.writeToParcel(dest, 0 /* no flags */); - } + PendingIntent.writePendingIntentOrNullToParcel(pendingIntent, dest); } @Override @@ -921,8 +895,9 @@ public class RemoteViews implements Parcelable, Filter { target.setOnClickListener(listener); } - public String getActionName() { - return "SetOnClickPendingIntent"; + @Override + public int getActionTag() { + return SET_ON_CLICK_PENDING_INTENT_TAG; } PendingIntent pendingIntent; @@ -1011,55 +986,37 @@ public class RemoteViews implements Parcelable, Filter { } /** - * Equivalent to calling a combination of {@link Drawable#setAlpha(int)}, + * Equivalent to calling * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}, - * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given view. + * on the {@link Drawable} of a given view. * <p> - * These operations will be performed on the {@link Drawable} returned by the + * The operation will be performed on the {@link Drawable} returned by the * target {@link View#getBackground()} by default. If targetBackground is false, * we assume the target is an {@link ImageView} and try applying the operations * to {@link ImageView#getDrawable()}. * <p> - * You can omit specific calls by marking their values with null or -1. */ - private class SetDrawableParameters extends Action { - public SetDrawableParameters(int id, boolean targetBackground, int alpha, - int colorFilter, PorterDuff.Mode mode, int level) { + private class SetDrawableTint extends Action { + SetDrawableTint(int id, boolean targetBackground, + int colorFilter, @NonNull PorterDuff.Mode mode) { this.viewId = id; this.targetBackground = targetBackground; - this.alpha = alpha; this.colorFilter = colorFilter; this.filterMode = mode; - this.level = level; } - public SetDrawableParameters(Parcel parcel) { + SetDrawableTint(Parcel parcel) { viewId = parcel.readInt(); targetBackground = parcel.readInt() != 0; - alpha = parcel.readInt(); colorFilter = parcel.readInt(); - boolean hasMode = parcel.readInt() != 0; - if (hasMode) { - filterMode = PorterDuff.Mode.valueOf(parcel.readString()); - } else { - filterMode = null; - } - level = parcel.readInt(); + filterMode = PorterDuff.intToMode(parcel.readInt()); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_DRAWABLE_PARAMETERS_TAG); dest.writeInt(viewId); dest.writeInt(targetBackground ? 1 : 0); - dest.writeInt(alpha); dest.writeInt(colorFilter); - if (filterMode != null) { - dest.writeInt(1); - dest.writeString(filterMode.toString()); - } else { - dest.writeInt(0); - } - dest.writeInt(level); + dest.writeInt(PorterDuff.modeToInt(filterMode)); } @Override @@ -1077,47 +1034,36 @@ public class RemoteViews implements Parcelable, Filter { } if (targetDrawable != null) { - // Perform modifications only if values are set correctly - if (alpha != -1) { - targetDrawable.mutate().setAlpha(alpha); - } - if (filterMode != null) { - targetDrawable.mutate().setColorFilter(colorFilter, filterMode); - } - if (level != -1) { - targetDrawable.mutate().setLevel(level); - } + targetDrawable.mutate().setColorFilter(colorFilter, filterMode); } } - public String getActionName() { - return "SetDrawableParameters"; + @Override + public int getActionTag() { + return SET_DRAWABLE_TINT_TAG; } boolean targetBackground; - int alpha; int colorFilter; PorterDuff.Mode filterMode; - int level; } - private final class ReflectionActionWithoutParams extends Action { - final String methodName; + private final class ViewContentNavigation extends Action { + final boolean mNext; - ReflectionActionWithoutParams(int viewId, String methodName) { + ViewContentNavigation(int viewId, boolean next) { this.viewId = viewId; - this.methodName = methodName; + this.mNext = next; } - ReflectionActionWithoutParams(Parcel in) { + ViewContentNavigation(Parcel in) { this.viewId = in.readInt(); - this.methodName = in.readString(); + this.mNext = in.readBoolean(); } public void writeToParcel(Parcel out, int flags) { - out.writeInt(SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG); out.writeInt(this.viewId); - out.writeString(this.methodName); + out.writeBoolean(this.mNext); } @Override @@ -1126,23 +1072,20 @@ public class RemoteViews implements Parcelable, Filter { if (view == null) return; try { - getMethod(view, this.methodName, null, false /* async */).invoke(view); + getMethod(view, + mNext ? "showNext" : "showPrevious", null, false /* async */).invoke(view); } catch (Throwable ex) { throw new ActionException(ex); } } public int mergeBehavior() { - // we don't need to build up showNext or showPrevious calls - if (methodName.equals("showNext") || methodName.equals("showPrevious")) { - return MERGE_IGNORE; - } else { - return MERGE_REPLACE; - } + return MERGE_IGNORE; } - public String getActionName() { - return "ReflectionActionWithoutParams"; + @Override + public int getActionTag() { + return VIEW_CONTENT_NAVIGATION_TAG; } } @@ -1156,12 +1099,7 @@ public class RemoteViews implements Parcelable, Filter { } public BitmapCache(Parcel source) { - int count = source.readInt(); - mBitmaps = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - Bitmap b = Bitmap.CREATOR.createFromParcel(source); - mBitmaps.add(b); - } + mBitmaps = source.createTypedArrayList(Bitmap.CREATOR); } public int getBitmapId(Bitmap b) { @@ -1187,11 +1125,7 @@ public class RemoteViews implements Parcelable, Filter { } public void writeBitmapsToParcel(Parcel dest, int flags) { - int count = mBitmaps.size(); - dest.writeInt(count); - for (int i = 0; i < count; i++) { - mBitmaps.get(i).writeToParcel(dest, flags); - } + dest.writeTypedList(mBitmaps, flags); } public int getBitmapMemory() { @@ -1227,7 +1161,6 @@ public class RemoteViews implements Parcelable, Filter { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(BITMAP_REFLECTION_ACTION_TAG); dest.writeInt(viewId); dest.writeString(methodName); dest.writeInt(bitmapId); @@ -1246,8 +1179,9 @@ public class RemoteViews implements Parcelable, Filter { bitmapId = bitmapCache.getBitmapId(bitmap); } - public String getActionName() { - return "BitmapReflectionAction"; + @Override + public int getActionTag() { + return BITMAP_REFLECTION_ACTION_TAG; } } @@ -1299,7 +1233,7 @@ public class RemoteViews implements Parcelable, Filter { // written to the parcel. switch (this.type) { case BOOLEAN: - this.value = in.readInt() != 0; + this.value = in.readBoolean(); break; case BYTE: this.value = in.readByte(); @@ -1329,39 +1263,28 @@ public class RemoteViews implements Parcelable, Filter { this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); break; case URI: - if (in.readInt() != 0) { - this.value = Uri.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(Uri.CREATOR); break; case BITMAP: - if (in.readInt() != 0) { - this.value = Bitmap.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(Bitmap.CREATOR); break; case BUNDLE: this.value = in.readBundle(); break; case INTENT: - if (in.readInt() != 0) { - this.value = Intent.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(Intent.CREATOR); break; case COLOR_STATE_LIST: - if (in.readInt() != 0) { - this.value = ColorStateList.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(ColorStateList.CREATOR); break; case ICON: - if (in.readInt() != 0) { - this.value = Icon.CREATOR.createFromParcel(in); - } + this.value = in.readTypedObject(Icon.CREATOR); default: break; } } public void writeToParcel(Parcel out, int flags) { - out.writeInt(REFLECTION_ACTION_TAG); out.writeInt(this.viewId); out.writeString(this.methodName); out.writeInt(this.type); @@ -1375,7 +1298,7 @@ public class RemoteViews implements Parcelable, Filter { // we have written a valid value to the parcel. switch (this.type) { case BOOLEAN: - out.writeInt((Boolean) this.value ? 1 : 0); + out.writeBoolean((Boolean) this.value); break; case BYTE: out.writeByte((Byte) this.value); @@ -1412,10 +1335,7 @@ public class RemoteViews implements Parcelable, Filter { case INTENT: case COLOR_STATE_LIST: case ICON: - out.writeInt(this.value != null ? 1 : 0); - if (this.value != null) { - ((Parcelable) this.value).writeToParcel(out, flags); - } + out.writeTypedObject((Parcelable) this.value, flags); break; default: break; @@ -1521,10 +1441,16 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { + @Override + public int getActionTag() { + return REFLECTION_ACTION_TAG; + } + + @Override + public String getUniqueKey() { // Each type of reflection action corresponds to a setter, so each should be seen as // unique from the standpoint of merging. - return "ReflectionAction" + this.methodName + this.type; + return super.getUniqueKey() + this.methodName + this.type; } @Override @@ -1586,7 +1512,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(VIEW_GROUP_ACTION_ADD_TAG); dest.writeInt(viewId); dest.writeInt(mIndex); mNestedViews.writeToParcel(dest, flags); @@ -1661,10 +1586,9 @@ public class RemoteViews implements Parcelable, Filter { return mNestedViews.prefersAsyncApply(); } - @Override - public String getActionName() { - return "ViewGroupActionAdd"; + public int getActionTag() { + return VIEW_GROUP_ACTION_ADD_TAG; } } @@ -1696,7 +1620,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(VIEW_GROUP_ACTION_REMOVE_TAG); dest.writeInt(viewId); dest.writeInt(mViewIdToKeep); } @@ -1762,8 +1685,8 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public String getActionName() { - return "ViewGroupActionRemove"; + public int getActionTag() { + return VIEW_GROUP_ACTION_REMOVE_TAG; } @Override @@ -1803,18 +1726,10 @@ public class RemoteViews implements Parcelable, Filter { isRelative = (parcel.readInt() != 0); useIcons = (parcel.readInt() != 0); if (useIcons) { - if (parcel.readInt() != 0) { - i1 = Icon.CREATOR.createFromParcel(parcel); - } - if (parcel.readInt() != 0) { - i2 = Icon.CREATOR.createFromParcel(parcel); - } - if (parcel.readInt() != 0) { - i3 = Icon.CREATOR.createFromParcel(parcel); - } - if (parcel.readInt() != 0) { - i4 = Icon.CREATOR.createFromParcel(parcel); - } + i1 = parcel.readTypedObject(Icon.CREATOR); + i2 = parcel.readTypedObject(Icon.CREATOR); + i3 = parcel.readTypedObject(Icon.CREATOR); + i4 = parcel.readTypedObject(Icon.CREATOR); } else { d1 = parcel.readInt(); d2 = parcel.readInt(); @@ -1824,35 +1739,14 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TEXT_VIEW_DRAWABLE_ACTION_TAG); dest.writeInt(viewId); dest.writeInt(isRelative ? 1 : 0); dest.writeInt(useIcons ? 1 : 0); if (useIcons) { - if (i1 != null) { - dest.writeInt(1); - i1.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } - if (i2 != null) { - dest.writeInt(1); - i2.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } - if (i3 != null) { - dest.writeInt(1); - i3.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } - if (i4 != null) { - dest.writeInt(1); - i4.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } + dest.writeTypedObject(i1, 0); + dest.writeTypedObject(i2, 0); + dest.writeTypedObject(i3, 0); + dest.writeTypedObject(i4, 0); } else { dest.writeInt(d1); dest.writeInt(d2); @@ -1923,8 +1817,9 @@ public class RemoteViews implements Parcelable, Filter { return useIcons; } - public String getActionName() { - return "TextViewDrawableAction"; + @Override + public int getActionTag() { + return TEXT_VIEW_DRAWABLE_ACTION_TAG; } boolean isRelative = false; @@ -1953,7 +1848,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TEXT_VIEW_SIZE_ACTION_TAG); dest.writeInt(viewId); dest.writeInt(units); dest.writeFloat(size); @@ -1966,8 +1860,9 @@ public class RemoteViews implements Parcelable, Filter { target.setTextSize(units, size); } - public String getActionName() { - return "TextViewSizeAction"; + @Override + public int getActionTag() { + return TEXT_VIEW_SIZE_ACTION_TAG; } int units; @@ -1995,7 +1890,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(VIEW_PADDING_ACTION_TAG); dest.writeInt(viewId); dest.writeInt(left); dest.writeInt(top); @@ -2010,8 +1904,9 @@ public class RemoteViews implements Parcelable, Filter { target.setPadding(left, top, right, bottom); } - public String getActionName() { - return "ViewPaddingAction"; + @Override + public int getActionTag() { + return VIEW_PADDING_ACTION_TAG; } int left, top, right, bottom; @@ -2028,6 +1923,9 @@ public class RemoteViews implements Parcelable, Filter { public static final int LAYOUT_WIDTH = 2; public static final int LAYOUT_MARGIN_BOTTOM_DIMEN = 3; + final int mProperty; + final int mValue; + /** * @param viewId ID of the view alter * @param property which layout parameter to alter @@ -2035,21 +1933,20 @@ public class RemoteViews implements Parcelable, Filter { */ public LayoutParamAction(int viewId, int property, int value) { this.viewId = viewId; - this.property = property; - this.value = value; + this.mProperty = property; + this.mValue = value; } public LayoutParamAction(Parcel parcel) { viewId = parcel.readInt(); - property = parcel.readInt(); - value = parcel.readInt(); + mProperty = parcel.readInt(); + mValue = parcel.readInt(); } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(LAYOUT_PARAM_ACTION_TAG); dest.writeInt(viewId); - dest.writeInt(property); - dest.writeInt(value); + dest.writeInt(mProperty); + dest.writeInt(mValue); } @Override @@ -2062,27 +1959,27 @@ public class RemoteViews implements Parcelable, Filter { if (layoutParams == null) { return; } - switch (property) { + switch (mProperty) { case LAYOUT_MARGIN_END_DIMEN: if (layoutParams instanceof ViewGroup.MarginLayoutParams) { - int resolved = resolveDimenPixelOffset(target, value); + int resolved = resolveDimenPixelOffset(target, mValue); ((ViewGroup.MarginLayoutParams) layoutParams).setMarginEnd(resolved); target.setLayoutParams(layoutParams); } break; case LAYOUT_MARGIN_BOTTOM_DIMEN: if (layoutParams instanceof ViewGroup.MarginLayoutParams) { - int resolved = resolveDimenPixelOffset(target, value); + int resolved = resolveDimenPixelOffset(target, mValue); ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = resolved; target.setLayoutParams(layoutParams); } break; case LAYOUT_WIDTH: - layoutParams.width = value; + layoutParams.width = mValue; target.setLayoutParams(layoutParams); break; default: - throw new IllegalArgumentException("Unknown property " + property); + throw new IllegalArgumentException("Unknown property " + mProperty); } } @@ -2093,79 +1990,15 @@ public class RemoteViews implements Parcelable, Filter { return target.getContext().getResources().getDimensionPixelOffset(value); } - public String getActionName() { - return "LayoutParamAction" + property + "."; - } - - int property; - int value; - } - - /** - * Helper action to set a color filter on a compound drawable on a TextView. Supports relative - * (s/t/e/b) or cardinal (l/t/r/b) arrangement. - */ - private class TextViewDrawableColorFilterAction extends Action { - public TextViewDrawableColorFilterAction(int viewId, boolean isRelative, int index, - int color, PorterDuff.Mode mode) { - this.viewId = viewId; - this.isRelative = isRelative; - this.index = index; - this.color = color; - this.mode = mode; - } - - public TextViewDrawableColorFilterAction(Parcel parcel) { - viewId = parcel.readInt(); - isRelative = (parcel.readInt() != 0); - index = parcel.readInt(); - color = parcel.readInt(); - mode = readPorterDuffMode(parcel); - } - - private PorterDuff.Mode readPorterDuffMode(Parcel parcel) { - int mode = parcel.readInt(); - if (mode >= 0 && mode < PorterDuff.Mode.values().length) { - return PorterDuff.Mode.values()[mode]; - } else { - return PorterDuff.Mode.CLEAR; - } - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG); - dest.writeInt(viewId); - dest.writeInt(isRelative ? 1 : 0); - dest.writeInt(index); - dest.writeInt(color); - dest.writeInt(mode.ordinal()); - } - @Override - public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { - final TextView target = root.findViewById(viewId); - if (target == null) return; - Drawable[] drawables = isRelative - ? target.getCompoundDrawablesRelative() - : target.getCompoundDrawables(); - if (index < 0 || index >= 4) { - throw new IllegalStateException("index must be in range [0, 3]."); - } - Drawable d = drawables[index]; - if (d != null) { - d.mutate(); - d.setColorFilter(color, mode); - } + public int getActionTag() { + return LAYOUT_PARAM_ACTION_TAG; } - public String getActionName() { - return "TextViewDrawableColorFilterAction"; + @Override + public String getUniqueKey() { + return super.getUniqueKey() + mProperty; } - - final boolean isRelative; - final int index; - final int color; - final PorterDuff.Mode mode; } /** @@ -2184,7 +2017,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(SET_REMOTE_INPUTS_ACTION_TAG); dest.writeInt(viewId); dest.writeTypedArray(remoteInputs, flags); } @@ -2197,8 +2029,9 @@ public class RemoteViews implements Parcelable, Filter { target.setTagInternal(R.id.remote_input_tag, remoteInputs); } - public String getActionName() { - return "SetRemoteInputsAction"; + @Override + public int getActionTag() { + return SET_REMOTE_INPUTS_ACTION_TAG; } final Parcelable[] remoteInputs; @@ -2220,7 +2053,6 @@ public class RemoteViews implements Parcelable, Filter { } public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(OVERRIDE_TEXT_COLORS_TAG); dest.writeInt(textColor); } @@ -2245,8 +2077,9 @@ public class RemoteViews implements Parcelable, Filter { } } - public String getActionName() { - return "OverrideTextColorsAction"; + @Override + public int getActionTag() { + return OVERRIDE_TEXT_COLORS_TAG; } } @@ -2338,20 +2171,12 @@ public class RemoteViews implements Parcelable, Filter { } if (src.mActions != null) { - mActions = new ArrayList<>(); - Parcel p = Parcel.obtain(); - int count = src.mActions.size(); - for (int i = 0; i < count; i++) { - p.setDataPosition(0); - Action a = src.mActions.get(i); - a.writeToParcel( - p, a.hasSameAppInfo(mApplication) ? PARCELABLE_ELIDE_DUPLICATES : 0); - p.setDataPosition(0); - // Since src is already in memory, we do not care about stack overflow as it has - // already been read once. - mActions.add(getActionFromParcel(p, 0)); - } + src.writeActionsToParcel(p); + p.setDataPosition(0); + // Since src is already in memory, we do not care about stack overflow as it has + // already been read once. + readActionsFromParcel(p, 0); p.recycle(); } @@ -2392,13 +2217,7 @@ public class RemoteViews implements Parcelable, Filter { mLayoutId = parcel.readInt(); mIsWidgetCollectionChild = parcel.readInt() == 1; - int count = parcel.readInt(); - if (count > 0) { - mActions = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - mActions.add(getActionFromParcel(parcel, depth)); - } - } + readActionsFromParcel(parcel, depth); } else { // MODE_HAS_LANDSCAPE_AND_PORTRAIT mLandscape = new RemoteViews(parcel, mBitmapCache, info, depth); @@ -2409,21 +2228,31 @@ public class RemoteViews implements Parcelable, Filter { mReapplyDisallowed = parcel.readInt() == 0; } + private void readActionsFromParcel(Parcel parcel, int depth) { + int count = parcel.readInt(); + if (count > 0) { + mActions = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + mActions.add(getActionFromParcel(parcel, depth)); + } + } + } + private Action getActionFromParcel(Parcel parcel, int depth) { int tag = parcel.readInt(); switch (tag) { case SET_ON_CLICK_PENDING_INTENT_TAG: return new SetOnClickPendingIntent(parcel); - case SET_DRAWABLE_PARAMETERS_TAG: - return new SetDrawableParameters(parcel); + case SET_DRAWABLE_TINT_TAG: + return new SetDrawableTint(parcel); case REFLECTION_ACTION_TAG: return new ReflectionAction(parcel); case VIEW_GROUP_ACTION_ADD_TAG: return new ViewGroupActionAdd(parcel, mBitmapCache, mApplication, depth); case VIEW_GROUP_ACTION_REMOVE_TAG: return new ViewGroupActionRemove(parcel); - case SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG: - return new ReflectionActionWithoutParams(parcel); + case VIEW_CONTENT_NAVIGATION_TAG: + return new ViewContentNavigation(parcel); case SET_EMPTY_VIEW_ACTION_TAG: return new SetEmptyView(parcel); case SET_PENDING_INTENT_TEMPLATE_TAG: @@ -2442,8 +2271,6 @@ public class RemoteViews implements Parcelable, Filter { return new BitmapReflectionAction(parcel); case SET_REMOTE_VIEW_ADAPTER_LIST_TAG: return new SetRemoteViewsAdapterList(parcel); - case TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG: - return new TextViewDrawableColorFilterAction(parcel); case SET_REMOTE_INPUTS_ACTION_TAG: return new SetRemoteInputsAction(parcel); case LAYOUT_PARAM_ACTION_TAG: @@ -2600,7 +2427,7 @@ public class RemoteViews implements Parcelable, Filter { * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showNext()} */ public void showNext(int viewId) { - addAction(new ReflectionActionWithoutParams(viewId, "showNext")); + addAction(new ViewContentNavigation(viewId, true /* next */)); } /** @@ -2609,7 +2436,7 @@ public class RemoteViews implements Parcelable, Filter { * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showPrevious()} */ public void showPrevious(int viewId) { - addAction(new ReflectionActionWithoutParams(viewId, "showPrevious")); + addAction(new ViewContentNavigation(viewId, false /* next */)); } /** @@ -2683,28 +2510,6 @@ public class RemoteViews implements Parcelable, Filter { } /** - * Equivalent to applying a color filter on one of the drawables in - * {@link android.widget.TextView#getCompoundDrawablesRelative()}. - * - * @param viewId The id of the view whose text should change. - * @param index The index of the drawable in the array of - * {@link android.widget.TextView#getCompoundDrawablesRelative()} to set the color - * filter on. Must be in [0, 3]. - * @param color The color of the color filter. See - * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}. - * @param mode The mode of the color filter. See - * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}. - * @hide - */ - public void setTextViewCompoundDrawablesRelativeColorFilter(int viewId, - int index, int color, PorterDuff.Mode mode) { - if (index < 0 || index >= 4) { - throw new IllegalArgumentException("index must be in range [0, 3]."); - } - addAction(new TextViewDrawableColorFilterAction(viewId, true, index, color, mode)); - } - - /** * Equivalent to calling {@link * TextView#setCompoundDrawablesWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)} * using the drawables yielded by {@link Icon#loadDrawable(Context)}. @@ -2901,12 +2706,10 @@ public class RemoteViews implements Parcelable, Filter { /** * @hide - * Equivalent to calling a combination of {@link Drawable#setAlpha(int)}, + * Equivalent to calling * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}, - * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given - * view. + * on the {@link Drawable} of a given view. * <p> - * You can omit specific calls by marking their values with null or -1. * * @param viewId The id of the view that contains the target * {@link Drawable} @@ -2915,20 +2718,15 @@ public class RemoteViews implements Parcelable, Filter { * {@link android.view.View#getBackground()}. Otherwise, assume * the target view is an {@link ImageView} and apply them to * {@link ImageView#getDrawable()}. - * @param alpha Specify an alpha value for the drawable, or -1 to leave - * unchanged. * @param colorFilter Specify a color for a * {@link android.graphics.ColorFilter} for this drawable. This will be ignored if * {@code mode} is {@code null}. * @param mode Specify a PorterDuff mode for this drawable, or null to leave * unchanged. - * @param level Specify the level for the drawable, or -1 to leave - * unchanged. */ - public void setDrawableParameters(int viewId, boolean targetBackground, int alpha, - int colorFilter, PorterDuff.Mode mode, int level) { - addAction(new SetDrawableParameters(viewId, targetBackground, alpha, - colorFilter, mode, level)); + public void setDrawableTint(int viewId, boolean targetBackground, + int colorFilter, @NonNull PorterDuff.Mode mode) { + addAction(new SetDrawableTint(viewId, targetBackground, colorFilter, mode)); } /** @@ -3695,18 +3493,7 @@ public class RemoteViews implements Parcelable, Filter { } dest.writeInt(mLayoutId); dest.writeInt(mIsWidgetCollectionChild ? 1 : 0); - int count; - if (mActions != null) { - count = mActions.size(); - } else { - count = 0; - } - dest.writeInt(count); - for (int i=0; i<count; i++) { - Action a = mActions.get(i); - a.writeToParcel(dest, a.hasSameAppInfo(mApplication) - ? PARCELABLE_ELIDE_DUPLICATES : 0); - } + writeActionsToParcel(dest); } else { dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT); // We only write the bitmap cache if we are the root RemoteViews, as this cache @@ -3721,6 +3508,22 @@ public class RemoteViews implements Parcelable, Filter { dest.writeInt(mReapplyDisallowed ? 1 : 0); } + private void writeActionsToParcel(Parcel parcel) { + int count; + if (mActions != null) { + count = mActions.size(); + } else { + count = 0; + } + parcel.writeInt(count); + for (int i = 0; i < count; i++) { + Action a = mActions.get(i); + parcel.writeInt(a.getActionTag()); + a.writeToParcel(parcel, a.hasSameAppInfo(mApplication) + ? PARCELABLE_ELIDE_DUPLICATES : 0); + } + } + private static ApplicationInfo getApplicationInfo(String packageName, int userId) { if (packageName == null) { return null; diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java index 36dc3308..3be42a5b 100644 --- a/android/widget/SelectionActionModeHelper.java +++ b/android/widget/SelectionActionModeHelper.java @@ -43,9 +43,11 @@ import com.android.internal.util.Preconditions; import java.text.BreakIterator; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -58,11 +60,7 @@ import java.util.regex.Pattern; @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public final class SelectionActionModeHelper { - /** - * Maximum time (in milliseconds) to wait for a result before timing out. - */ - // TODO: Consider making this a ViewConfiguration. - private static final int TIMEOUT_DURATION = 200; + private static final String LOG_TAG = "SelectActionModeHelper"; private static final boolean SMART_SELECT_ANIMATION_ENABLED = true; @@ -83,7 +81,8 @@ public final class SelectionActionModeHelper { mEditor = Preconditions.checkNotNull(editor); mTextView = mEditor.getTextView(); mTextClassificationHelper = new TextClassificationHelper( - mTextView.getTextClassifier(), mTextView.getText(), + mTextView.getTextClassifier(), + getText(mTextView), 0, 1, mTextView.getTextLocales()); mSelectionTracker = new SelectionTracker(mTextView); @@ -97,7 +96,7 @@ public final class SelectionActionModeHelper { public void startActionModeAsync(boolean adjustSelection) { mSelectionTracker.onOriginalSelection( - mTextView.getText(), + getText(mTextView), mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextView.isTextEditable()); @@ -108,7 +107,7 @@ public final class SelectionActionModeHelper { resetTextClassificationHelper(); mTextClassificationAsyncTask = new TextClassificationAsyncTask( mTextView, - TIMEOUT_DURATION, + mTextClassificationHelper.getTimeoutDuration(), adjustSelection ? mTextClassificationHelper::suggestSelection : mTextClassificationHelper::classifyText, @@ -127,7 +126,7 @@ public final class SelectionActionModeHelper { resetTextClassificationHelper(); mTextClassificationAsyncTask = new TextClassificationAsyncTask( mTextView, - TIMEOUT_DURATION, + mTextClassificationHelper.getTimeoutDuration(), mTextClassificationHelper::classifyText, this::invalidateActionMode) .execute(); @@ -195,7 +194,7 @@ public final class SelectionActionModeHelper { } private void startActionMode(@Nullable SelectionResult result) { - final CharSequence text = mTextView.getText(); + final CharSequence text = getText(mTextView); if (result != null && text instanceof Spannable) { Selection.setSelection((Spannable) text, result.mStart, result.mEnd); mTextClassification = result.mClassification; @@ -229,7 +228,7 @@ public final class SelectionActionModeHelper { return; } - final List<RectF> selectionRectangles = + final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles = convertSelectionToRectangles(layout, result.mStart, result.mEnd); final PointF touchPoint = new PointF( @@ -237,7 +236,8 @@ public final class SelectionActionModeHelper { mEditor.getLastUpPositionY()); final PointF animationStartPoint = - movePointInsideNearestRectangle(touchPoint, selectionRectangles); + movePointInsideNearestRectangle(touchPoint, selectionRectangles, + SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle); mSmartSelectSprite.startAnimation( animationStartPoint, @@ -245,38 +245,58 @@ public final class SelectionActionModeHelper { onAnimationEndCallback); } - private List<RectF> convertSelectionToRectangles(final Layout layout, final int start, - final int end) { - final List<RectF> result = new ArrayList<>(); - layout.getSelection(start, end, (left, top, right, bottom, textSelectionLayout) -> - mergeRectangleIntoList(result, new RectF(left, top, right, bottom))); + private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles( + final Layout layout, final int start, final int end) { + final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>(); + + final Layout.SelectionRectangleConsumer consumer = + (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList( + result, + new RectF(left, top, right, bottom), + SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle, + r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r, + textSelectionLayout) + ); + + layout.getSelection(start, end, consumer); + + result.sort(Comparator.comparing( + SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle, + SmartSelectSprite.RECTANGLE_COMPARATOR)); - result.sort(SmartSelectSprite.RECTANGLE_COMPARATOR); return result; } + // TODO: Move public pure functions out of this class and make it package-private. /** - * Merges a {@link RectF} into an existing list of rectangles. While merging, this method - * makes sure that: + * Merges a {@link RectF} into an existing list of any objects which contain a rectangle. + * While merging, this method makes sure that: * * <ol> * <li>No rectangle is redundant (contained within a bigger rectangle)</li> * <li>Rectangles of the same height and vertical position that intersect get merged</li> * </ol> * - * @param list the list of rectangles to merge the new rectangle in + * @param list the list of rectangles (or other rectangle containers) to merge the new + * rectangle into * @param candidate the {@link RectF} to merge into the list + * @param extractor a function that can extract a {@link RectF} from an element of the given + * list + * @param packer a function that can wrap the resulting {@link RectF} into an element that + * the list contains * @hide */ @VisibleForTesting - public static void mergeRectangleIntoList(List<RectF> list, RectF candidate) { + public static <T> void mergeRectangleIntoList(final List<T> list, + final RectF candidate, final Function<T, RectF> extractor, + final Function<RectF, T> packer) { if (candidate.isEmpty()) { return; } final int elementCount = list.size(); for (int index = 0; index < elementCount; ++index) { - final RectF existingRectangle = list.get(index); + final RectF existingRectangle = extractor.apply(list.get(index)); if (existingRectangle.contains(candidate)) { return; } @@ -299,26 +319,27 @@ public final class SelectionActionModeHelper { } for (int index = elementCount - 1; index >= 0; --index) { - if (list.get(index).isEmpty()) { + final RectF rectangle = extractor.apply(list.get(index)); + if (rectangle.isEmpty()) { list.remove(index); } } - list.add(candidate); + list.add(packer.apply(candidate)); } /** @hide */ @VisibleForTesting - public static PointF movePointInsideNearestRectangle(final PointF point, - final List<RectF> rectangles) { + public static <T> PointF movePointInsideNearestRectangle(final PointF point, + final List<T> list, final Function<T, RectF> extractor) { float bestX = -1; float bestY = -1; double bestDistance = Double.MAX_VALUE; - final int elementCount = rectangles.size(); + final int elementCount = list.size(); for (int index = 0; index < elementCount; ++index) { - final RectF rectangle = rectangles.get(index); + final RectF rectangle = extractor.apply(list.get(index)); final float candidateY = rectangle.centerY(); final float candidateX; @@ -356,7 +377,9 @@ public final class SelectionActionModeHelper { } private void resetTextClassificationHelper() { - mTextClassificationHelper.reset(mTextView.getTextClassifier(), mTextView.getText(), + mTextClassificationHelper.reset( + mTextView.getTextClassifier(), + getText(mTextView), mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextView.getTextLocales()); } @@ -382,6 +405,7 @@ public final class SelectionActionModeHelper { private int mSelectionStart; private int mSelectionEnd; private boolean mAllowReset; + private final LogAbandonRunnable mDelayedLogAbandon = new LogAbandonRunnable(); SelectionTracker(TextView textView) { mTextView = Preconditions.checkNotNull(textView); @@ -393,6 +417,10 @@ public final class SelectionActionModeHelper { */ public void onOriginalSelection( CharSequence text, int selectionStart, int selectionEnd, boolean editableText) { + // 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(); + mOriginalStart = mSelectionStart = selectionStart; mOriginalEnd = mSelectionEnd = selectionEnd; mAllowReset = false; @@ -433,12 +461,7 @@ public final class SelectionActionModeHelper { public void onSelectionDestroyed() { mAllowReset = false; // Wait a few ms to see if the selection was destroyed because of a text change event. - mTextView.postDelayed(() -> { - mLogger.logSelectionAction( - mSelectionStart, mSelectionEnd, - SelectionEvent.ActionType.ABANDON, null /* classification */); - mSelectionStart = mSelectionEnd = -1; - }, 100 /* ms */); + mDelayedLogAbandon.schedule(100 /* ms */); } /** @@ -465,7 +488,7 @@ public final class SelectionActionModeHelper { if (isSelectionStarted() && mAllowReset && textIndex >= mSelectionStart && textIndex <= mSelectionEnd - && textView.getText() instanceof Spannable) { + && getText(textView) instanceof Spannable) { mAllowReset = false; boolean selected = editor.selectCurrentWord(); if (selected) { @@ -495,6 +518,38 @@ public final class SelectionActionModeHelper { private boolean isSelectionStarted() { return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd; } + + /** A helper for keeping track of pending abandon logging requests. */ + private final class LogAbandonRunnable implements Runnable { + private boolean mIsPending; + + /** Schedules an abandon to be logged with the given delay. Flush if necessary. */ + void schedule(int delayMillis) { + if (mIsPending) { + Log.e(LOG_TAG, "Force flushing abandon due to new scheduling request"); + flush(); + } + mIsPending = true; + mTextView.postDelayed(this, delayMillis); + } + + /** If there is a pending log request, execute it now. */ + void flush() { + mTextView.removeCallbacks(this); + run(); + } + + @Override + public void run() { + if (mIsPending) { + mLogger.logSelectionAction( + mSelectionStart, mSelectionEnd, + SelectionEvent.ActionType.ABANDON, null /* classification */); + mSelectionStart = mSelectionEnd = -1; + mIsPending = false; + } + } + } } // TODO: Write tests @@ -689,7 +744,7 @@ public final class SelectionActionModeHelper { mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier); mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback); // Make a copy of the original text. - mOriginalText = mTextView.getText().toString(); + mOriginalText = getText(mTextView).toString(); } @Override @@ -705,7 +760,7 @@ public final class SelectionActionModeHelper { @Override @UiThread protected void onPostExecute(SelectionResult result) { - result = TextUtils.equals(mOriginalText, mTextView.getText()) ? result : null; + result = TextUtils.equals(mOriginalText, getText(mTextView)) ? result : null; mSelectionResultCallback.accept(result); } @@ -752,6 +807,9 @@ public final class SelectionActionModeHelper { private LocaleList mLastClassificationLocales; private SelectionResult mLastClassificationResult; + /** Whether the TextClassifier has been initialized. */ + private boolean mHot; + TextClassificationHelper(TextClassifier textClassifier, CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) { reset(textClassifier, text, selectionStart, selectionEnd, locales); @@ -771,11 +829,13 @@ public final class SelectionActionModeHelper { @WorkerThread public SelectionResult classifyText() { + mHot = true; return performClassification(null /* selection */); } @WorkerThread public SelectionResult suggestSelection() { + mHot = true; trimText(); final TextSelection selection = mTextClassifier.suggestSelection( mTrimmedText, mRelativeStart, mRelativeEnd, mLocales); @@ -784,6 +844,22 @@ public final class SelectionActionModeHelper { return performClassification(selection); } + /** + * Maximum time (in milliseconds) to wait for a textclassifier result before timing out. + */ + // TODO: Consider making this a ViewConfiguration. + public int getTimeoutDuration() { + if (mHot) { + return 200; + } else { + // Return a slightly larger number than usual when the TextClassifier is first + // initialized. Initialization would usually take longer than subsequent calls to + // the TextClassifier. The impact of this on the UI is that we do not show the + // selection handles or toolbar until after this timeout. + return 500; + } + } + private SelectionResult performClassification(@Nullable TextSelection selection) { if (!Objects.equals(mText, mLastClassificationText) || mSelectionStart != mLastClassificationSelectionStart @@ -854,4 +930,14 @@ public final class SelectionActionModeHelper { return SelectionEvent.ActionType.OTHER; } } + + private static CharSequence getText(TextView textView) { + // Extracts the textView's text. + // TODO: Investigate why/when TextView.getText() is null. + final CharSequence text = textView.getText(); + if (text != null) { + return text; + } + return ""; + } } diff --git a/android/widget/SmartSelectSprite.java b/android/widget/SmartSelectSprite.java index 27b93bc7..a391c6ee 100644 --- a/android/widget/SmartSelectSprite.java +++ b/android/widget/SmartSelectSprite.java @@ -35,6 +35,7 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.Shape; +import android.text.Layout; import android.util.TypedValue; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -42,9 +43,9 @@ import android.view.animation.Interpolator; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.LinkedList; import java.util.List; /** @@ -76,6 +77,26 @@ final class SmartSelectSprite { private Drawable mExistingDrawable = null; private RectangleList mExistingRectangleList = null; + static final class RectangleWithTextSelectionLayout { + private final RectF mRectangle; + @Layout.TextSelectionLayout + private final int mTextSelectionLayout; + + RectangleWithTextSelectionLayout(RectF rectangle, int textSelectionLayout) { + mRectangle = Preconditions.checkNotNull(rectangle); + mTextSelectionLayout = textSelectionLayout; + } + + public RectF getRectangle() { + return mRectangle; + } + + @Layout.TextSelectionLayout + public int getTextSelectionLayout() { + return mTextSelectionLayout; + } + } + /** * A rounded rectangle with a configurable corner radius and the ability to expand outside of * its bounding rectangle and clip against it. @@ -84,12 +105,23 @@ final class SmartSelectSprite { private static final String PROPERTY_ROUND_RATIO = "roundRatio"; + /** + * The direction in which the rectangle will perform its expansion. A rectangle can expand + * from its left edge, its right edge or from the center (or, more precisely, the user's + * touch point). For example, in left-to-right text, a selection spanning two lines with the + * user's action being on the first line will have the top rectangle and expansion direction + * of CENTER, while the bottom one will have an expansion direction of RIGHT. + */ @Retention(SOURCE) @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT}) private @interface ExpansionDirection { - int LEFT = 0; - int CENTER = 1; - int RIGHT = 2; + int LEFT = -1; + int CENTER = 0; + int RIGHT = 1; + } + + private static @ExpansionDirection int invert(@ExpansionDirection int expansionDirection) { + return expansionDirection * -1; } @Retention(SOURCE) @@ -114,20 +146,33 @@ final class SmartSelectSprite { private final RectF mClipRect = new RectF(); private final Path mClipPath = new Path(); - /** How far offset the left edge of the rectangle is from the bounding box. */ + /** How offset the left edge of the rectangle is from the left side of the bounding box. */ private float mLeftBoundary = 0; - /** How far offset the right edge of the rectangle is from the bounding box. */ + /** How offset the right edge of the rectangle is from the left side of the bounding box. */ private float mRightBoundary = 0; + /** Whether the horizontal bounds are inverted (for RTL scenarios). */ + private final boolean mInverted; + + private final float mBoundingWidth; + private RoundedRectangleShape( final RectF boundingRectangle, final @ExpansionDirection int expansionDirection, final @RectangleBorderType int rectangleBorderType, + final boolean inverted, final float strokeWidth) { mBoundingRectangle = new RectF(boundingRectangle); - mExpansionDirection = expansionDirection; + mBoundingWidth = boundingRectangle.width(); mRectangleBorderType = rectangleBorderType; mStrokeWidth = strokeWidth; + mInverted = inverted && expansionDirection != ExpansionDirection.CENTER; + + if (inverted) { + mExpansionDirection = invert(expansionDirection); + } else { + mExpansionDirection = expansionDirection; + } if (boundingRectangle.height() > boundingRectangle.width()) { setRoundRatio(0.0f); @@ -148,6 +193,10 @@ final class SmartSelectSprite { */ @Override public void draw(Canvas canvas, Paint paint) { + if (mLeftBoundary == mRightBoundary) { + return; + } + final float cornerRadius = getCornerRadius(); final float adjustedCornerRadius = getAdjustedCornerRadius(); @@ -157,7 +206,7 @@ final class SmartSelectSprite { if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) { mDrawRect.left -= cornerRadius / 2; - mDrawRect.right -= cornerRadius / 2; + mDrawRect.right += cornerRadius / 2; } else { switch (mExpansionDirection) { case ExpansionDirection.CENTER: @@ -173,7 +222,7 @@ final class SmartSelectSprite { canvas.save(); mClipRect.set(mBoundingRectangle); - mClipRect.inset(-mStrokeWidth, -mStrokeWidth); + mClipRect.inset(-mStrokeWidth / 2, -mStrokeWidth / 2); canvas.clipRect(mClipRect); canvas.drawRoundRect(mDrawRect, adjustedCornerRadius, adjustedCornerRadius, paint); canvas.restore(); @@ -190,20 +239,28 @@ final class SmartSelectSprite { canvas.restore(); } - public void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) { + void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) { mRoundRatio = roundRatio; } - public float getRoundRatio() { + float getRoundRatio() { return mRoundRatio; } - private void setLeftBoundary(final float leftBoundary) { - mLeftBoundary = leftBoundary; + private void setStartBoundary(final float startBoundary) { + if (mInverted) { + mRightBoundary = mBoundingWidth - startBoundary; + } else { + mLeftBoundary = startBoundary; + } } - private void setRightBoundary(final float rightBoundary) { - mRightBoundary = rightBoundary; + private void setEndBoundary(final float endBoundary) { + if (mInverted) { + mLeftBoundary = mBoundingWidth - endBoundary; + } else { + mRightBoundary = endBoundary; + } } private float getCornerRadius() { @@ -247,8 +304,8 @@ final class SmartSelectSprite { private @DisplayType int mDisplayType = DisplayType.RECTANGLES; private RectangleList(final List<RoundedRectangleShape> rectangles) { - mRectangles = new LinkedList<>(rectangles); - mReversedRectangles = new LinkedList<>(rectangles); + mRectangles = new ArrayList<>(rectangles); + mReversedRectangles = new ArrayList<>(rectangles); Collections.reverse(mReversedRectangles); mOutlinePolygonPath = generateOutlinePolygonPath(rectangles); } @@ -258,11 +315,11 @@ final class SmartSelectSprite { for (RoundedRectangleShape rectangle : mReversedRectangles) { final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth(); if (leftBoundary < rectangleLeftBoundary) { - rectangle.setLeftBoundary(0); + rectangle.setStartBoundary(0); } else if (leftBoundary > boundarySoFar) { - rectangle.setLeftBoundary(rectangle.getBoundingWidth()); + rectangle.setStartBoundary(rectangle.getBoundingWidth()); } else { - rectangle.setLeftBoundary( + rectangle.setStartBoundary( rectangle.getBoundingWidth() - boundarySoFar + leftBoundary); } @@ -275,11 +332,11 @@ final class SmartSelectSprite { for (RoundedRectangleShape rectangle : mRectangles) { final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar; if (rectangleRightBoundary < rightBoundary) { - rectangle.setRightBoundary(rectangle.getBoundingWidth()); + rectangle.setEndBoundary(rectangle.getBoundingWidth()); } else if (boundarySoFar > rightBoundary) { - rectangle.setRightBoundary(0); + rectangle.setEndBoundary(0); } else { - rectangle.setRightBoundary(rightBoundary - boundarySoFar); + rectangle.setEndBoundary(rightBoundary - boundarySoFar); } boundarySoFar = rectangleRightBoundary; @@ -331,8 +388,8 @@ final class SmartSelectSprite { } /** - * @param context The {@link Context} in which the animation will run - * @param invalidator A {@link Runnable} which will be called every time the animation updates, + * @param context the {@link Context} in which the animation will run + * @param invalidator a {@link Runnable} which will be called every time the animation updates, * indicating that the view drawing the animation should invalidate itself */ SmartSelectSprite(final Context context, final Runnable invalidator) { @@ -356,67 +413,97 @@ final class SmartSelectSprite { * "selection" and finally join them into a single polygon. In * order to get the correct visual behavior, these rectangles * should be sorted according to {@link #RECTANGLE_COMPARATOR}. - * @param onAnimationEnd The callback which will be invoked once the whole animation - * completes. + * @param onAnimationEnd the callback which will be invoked once the whole animation + * completes * @throws IllegalArgumentException if the given start point is not in any of the - * destinationRectangles. + * destinationRectangles * @see #cancelAnimation() */ + // TODO nullability checks on parameters public void startAnimation( final PointF start, - final List<RectF> destinationRectangles, - final Runnable onAnimationEnd) throws IllegalArgumentException { + final List<RectangleWithTextSelectionLayout> destinationRectangles, + final Runnable onAnimationEnd) { cancelAnimation(); final ValueAnimator.AnimatorUpdateListener updateListener = valueAnimator -> mInvalidator.run(); - final List<RoundedRectangleShape> shapes = new LinkedList<>(); - final List<Animator> cornerAnimators = new LinkedList<>(); + final int rectangleCount = destinationRectangles.size(); + + final List<RoundedRectangleShape> shapes = new ArrayList<>(rectangleCount); + final List<Animator> cornerAnimators = new ArrayList<>(rectangleCount); - final RectF centerRectangle = destinationRectangles - .stream() - .filter((r) -> contains(r, start)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException( - "Center point is not inside any of the rectangles!")); + RectangleWithTextSelectionLayout centerRectangle = null; int startingOffset = 0; - for (RectF rectangle : destinationRectangles) { - if (rectangle.equals(centerRectangle)) { + int startingRectangleIndex = 0; + for (int index = 0; index < rectangleCount; ++index) { + final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = + destinationRectangles.get(index); + final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); + if (contains(rectangle, start)) { + centerRectangle = rectangleWithTextSelectionLayout; break; } startingOffset += rectangle.width(); + ++startingRectangleIndex; } - startingOffset += start.x - centerRectangle.left; + if (centerRectangle == null) { + throw new IllegalArgumentException("Center point is not inside any of the rectangles!"); + } - final float centerRectangleHalfHeight = centerRectangle.height() / 2; - final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight; - final float startingOffsetRight = startingOffset + centerRectangleHalfHeight; + startingOffset += start.x - centerRectangle.getRectangle().left; final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections = generateDirections(centerRectangle, destinationRectangles); final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes = - generateBorderTypes(destinationRectangles); - - int index = 0; + generateBorderTypes(rectangleCount); - for (RectF rectangle : destinationRectangles) { + for (int index = 0; index < rectangleCount; ++index) { + final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = + destinationRectangles.get(index); + final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); final RoundedRectangleShape shape = new RoundedRectangleShape( rectangle, expansionDirections[index], rectangleBorderTypes[index], + rectangleWithTextSelectionLayout.getTextSelectionLayout() + == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, mStrokeWidth); cornerAnimators.add(createCornerAnimator(shape, updateListener)); shapes.add(shape); - index++; } final RectangleList rectangleList = new RectangleList(shapes); final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList); + final float startingOffsetLeft; + final float startingOffsetRight; + + final RoundedRectangleShape startingRectangleShape = shapes.get(startingRectangleIndex); + final float cornerRadius = startingRectangleShape.getCornerRadius(); + if (startingRectangleShape.mRectangleBorderType + == RoundedRectangleShape.RectangleBorderType.FIT) { + switch (startingRectangleShape.mExpansionDirection) { + case RoundedRectangleShape.ExpansionDirection.LEFT: + startingOffsetLeft = startingOffsetRight = startingOffset - cornerRadius / 2; + break; + case RoundedRectangleShape.ExpansionDirection.RIGHT: + startingOffsetLeft = startingOffsetRight = startingOffset + cornerRadius / 2; + break; + case RoundedRectangleShape.ExpansionDirection.CENTER: // fall through + default: + startingOffsetLeft = startingOffset - cornerRadius / 2; + startingOffsetRight = startingOffset + cornerRadius / 2; + break; + } + } else { + startingOffsetLeft = startingOffsetRight = startingOffset; + } + final Paint paint = shapeDrawable.getPaint(); paint.setColor(mStrokeColor); paint.setStyle(Paint.Style.STROKE); @@ -511,7 +598,8 @@ final class SmartSelectSprite { } private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections( - final RectF centerRectangle, final List<RectF> rectangles) { + final RectangleWithTextSelectionLayout centerRectangle, + final List<RectangleWithTextSelectionLayout> rectangles) { final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()]; final int centerRectangleIndex = rectangles.indexOf(centerRectangle); @@ -538,8 +626,8 @@ final class SmartSelectSprite { } private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes( - final List<RectF> rectangles) { - final @RoundedRectangleShape.RectangleBorderType int[] result = new int[rectangles.size()]; + final int numberOfRectangles) { + final @RoundedRectangleShape.RectangleBorderType int[] result = new int[numberOfRectangles]; for (int i = 1; i < result.length - 1; ++i) { result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT; diff --git a/android/widget/Switch.java b/android/widget/Switch.java index 2e1e9636..604575fa 100644 --- a/android/widget/Switch.java +++ b/android/widget/Switch.java @@ -248,10 +248,7 @@ public class Switch extends CompoundButton { com.android.internal.R.styleable.Switch_switchPadding, 0); mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false); - // TODO: replace CUR_DEVELOPMENT with P once P is added to android.os.Build.VERSION_CODES. - // STOPSHIP if the above TODO is not done. - mUseFallbackLineSpacing = - context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.CUR_DEVELOPMENT; + mUseFallbackLineSpacing = context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.P; ColorStateList thumbTintList = a.getColorStateList( com.android.internal.R.styleable.Switch_thumbTint); diff --git a/android/widget/TabWidget.java b/android/widget/TabWidget.java index 05f7c0a1..f8b6837e 100644 --- a/android/widget/TabWidget.java +++ b/android/widget/TabWidget.java @@ -61,7 +61,10 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { // This value will be set to 0 as soon as the first tab is added to TabHost. private int mSelectedTab = -1; + @Nullable private Drawable mLeftStrip; + + @Nullable private Drawable mRightStrip; private boolean mDrawBottomStrips = true; @@ -374,23 +377,36 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { final Drawable leftStrip = mLeftStrip; final Drawable rightStrip = mRightStrip; - leftStrip.setState(selectedChild.getDrawableState()); - rightStrip.setState(selectedChild.getDrawableState()); + if (leftStrip != null) { + leftStrip.setState(selectedChild.getDrawableState()); + } + if (rightStrip != null) { + rightStrip.setState(selectedChild.getDrawableState()); + } if (mStripMoved) { final Rect bounds = mBounds; bounds.left = selectedChild.getLeft(); bounds.right = selectedChild.getRight(); final int myHeight = getHeight(); - leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()), - myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight); - rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(), - Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight); + if (leftStrip != null) { + leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()), + myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight); + } + if (rightStrip != null) { + rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(), + Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), + myHeight); + } mStripMoved = false; } - leftStrip.draw(canvas); - rightStrip.draw(canvas); + if (leftStrip != null) { + leftStrip.draw(canvas); + } + if (rightStrip != null) { + rightStrip.draw(canvas); + } } /** diff --git a/android/widget/TextView.java b/android/widget/TextView.java index efcc3a2f..24ae03c3 100644 --- a/android/widget/TextView.java +++ b/android/widget/TextView.java @@ -1256,9 +1256,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; mUseInternationalizedInput = targetSdkVersion >= VERSION_CODES.O; - // TODO: replace CUR_DEVELOPMENT with P once P is added to android.os.Build.VERSION_CODES. - // STOPSHIP if the above TODO is not done. - mUseFallbackLineSpacing = targetSdkVersion >= VERSION_CODES.CUR_DEVELOPMENT; + mUseFallbackLineSpacing = targetSdkVersion >= VERSION_CODES.P; if (inputMethod != null) { Class<?> c; @@ -5549,7 +5547,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public final void setHint(CharSequence hint) { setHintInternal(hint); - if (isInputMethodTarget()) { + if (mEditor != null && isInputMethodTarget()) { mEditor.reportExtractedText(); } } @@ -6283,7 +6281,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int horizontalPadding = getCompoundPaddingLeft(); final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true); - if (mEditor.mCursorDrawable == null) { + if (mEditor.mDrawableForCursor == null) { synchronized (TEMP_RECTF) { /* * The reason for this concern about the thickness of the @@ -6310,7 +6308,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener (int) Math.ceil(verticalPadding + TEMP_RECTF.bottom + thick)); } } else { - final Rect bounds = mEditor.mCursorDrawable.getBounds(); + final Rect bounds = mEditor.mDrawableForCursor.getBounds(); invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding, bounds.right + horizontalPadding, bounds.bottom + verticalPadding); } @@ -6362,8 +6360,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int bottom = mLayout.getLineBottom(lineEnd); // mEditor can be null in case selection is set programmatically. - if (invalidateCursor && mEditor != null && mEditor.mCursorDrawable != null) { - final Rect bounds = mEditor.mCursorDrawable.getBounds(); + if (invalidateCursor && mEditor != null && mEditor.mDrawableForCursor != null) { + final Rect bounds = mEditor.mDrawableForCursor.getBounds(); top = Math.min(top, bounds.top); bottom = Math.max(bottom, bounds.bottom); } diff --git a/android/widget/TextViewSetTextLocalePerfTest.java b/android/widget/TextViewSetTextLocalePerfTest.java index 7fc5e4f8..e95676b2 100644 --- a/android/widget/TextViewSetTextLocalePerfTest.java +++ b/android/widget/TextViewSetTextLocalePerfTest.java @@ -16,27 +16,21 @@ package android.widget; -import android.app.Activity; -import android.os.Bundle; -import android.perftests.utils.PerfStatusReporter; -import android.util.Log; - import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; import android.perftests.utils.StubActivity; import android.support.test.filters.LargeTest; -import android.support.test.runner.AndroidJUnit4; import android.support.test.rule.ActivityTestRule; -import android.support.test.InstrumentationRegistry; - -import java.util.Locale; -import java.util.Collection; -import java.util.Arrays; -import org.junit.Test; import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; -import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; @LargeTest @RunWith(Parameterized.class) |